feat(xtask): generate JSON schema for cli TestSummary

This commit is contained in:
Will Lillis 2025-10-31 18:23:21 -04:00
parent d546e28abf
commit c7b5f89392
9 changed files with 376 additions and 7 deletions

65
Cargo.lock generated
View file

@ -576,6 +576,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
@ -1438,6 +1444,26 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "ref-cast"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "regalloc2"
version = "0.12.2"
@ -1603,6 +1629,31 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schemars"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
]
[[package]]
name = "semver"
version = "1.0.27"
@ -1643,6 +1694,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
@ -1947,6 +2009,7 @@ dependencies = [
"pretty_assertions",
"rand",
"regex",
"schemars",
"semver",
"serde",
"serde_json",
@ -2740,8 +2803,10 @@ dependencies = [
"notify",
"notify-debouncer-full",
"regex",
"schemars",
"semver",
"serde_json",
"tree-sitter-cli",
]
[[package]]

View file

@ -137,6 +137,7 @@ rand = "0.8.5"
regex = "1.11.3"
regex-syntax = "0.8.6"
rustc-hash = "2.1.1"
schemars = "1.0.4"
semver = { version = "1.0.27", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.145", features = ["preserve_order"] }

View file

@ -54,6 +54,7 @@ log.workspace = true
memchr.workspace = true
rand.workspace = true
regex.workspace = true
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -11,6 +11,7 @@ use anstyle::{AnsiColor, Color, RgbColor};
use anyhow::{anyhow, Context, Result};
use clap::ValueEnum;
use log::info;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tree_sitter::{
ffi, InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree,
@ -19,7 +20,7 @@ use tree_sitter::{
use crate::{fuzz::edits::Edit, logger::paint, util};
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, Serialize, JsonSchema)]
pub struct Stats {
pub successful_parses: usize,
pub total_parses: usize,

View file

@ -18,6 +18,7 @@ use regex::{
bytes::{Regex as ByteRegex, RegexBuilder as ByteRegexBuilder},
Regex,
};
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::Serialize;
use similar::{ChangeTag, TextDiff};
use tree_sitter::{format_sexp, Language, LogType, Parser, Query, Tree};
@ -139,36 +140,47 @@ pub struct TestOptions<'a> {
}
/// A stateful object used to collect results from running a grammar's test suite
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, Serialize, JsonSchema)]
pub struct TestSummary {
// Parse test results and associated data
#[schemars(schema_with = "schema_as_array")]
#[serde(serialize_with = "serialize_as_array")]
pub parse_results: TestResultHierarchy,
pub parse_failures: Vec<TestFailure>,
pub parse_stats: Stats,
#[schemars(skip)]
#[serde(skip)]
pub has_parse_errors: bool,
#[schemars(skip)]
#[serde(skip)]
pub parse_stat_display: TestStats,
// Other test results
#[schemars(schema_with = "schema_as_array")]
#[serde(serialize_with = "serialize_as_array")]
pub highlight_results: TestResultHierarchy,
#[schemars(schema_with = "schema_as_array")]
#[serde(serialize_with = "serialize_as_array")]
pub tag_results: TestResultHierarchy,
#[schemars(schema_with = "schema_as_array")]
#[serde(serialize_with = "serialize_as_array")]
pub query_results: TestResultHierarchy,
// Data used during construction
#[schemars(skip)]
#[serde(skip)]
pub test_num: usize,
// Options passed in from the CLI which control how the summary is displayed
#[schemars(skip)]
#[serde(skip)]
pub color: bool,
#[schemars(skip)]
#[serde(skip)]
pub overview_only: bool,
#[schemars(skip)]
#[serde(skip)]
pub update: bool,
#[schemars(skip)]
#[serde(skip)]
pub json: bool,
}
@ -194,7 +206,7 @@ impl TestSummary {
}
}
#[derive(Debug, Default)]
#[derive(Debug, Default, JsonSchema)]
pub struct TestResultHierarchy {
root_group: Vec<TestResult>,
traversal_idxs: Vec<usize>,
@ -207,6 +219,10 @@ where
results.root_group.serialize(serializer)
}
fn schema_as_array(gen: &mut SchemaGenerator) -> Schema {
gen.subschema_for::<Vec<TestResult>>()
}
/// Stores arbitrarily nested parent test groups and child cases. Supports creation
/// in DFS traversal order
impl TestResultHierarchy {
@ -313,14 +329,16 @@ impl<'a> Iterator for TestResultIterWithDepth<'a> {
}
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, JsonSchema)]
pub struct TestResult {
pub name: String,
#[schemars(flatten)]
#[serde(flatten)]
pub info: TestInfo,
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, JsonSchema)]
#[schemars(untagged)]
#[serde(untagged)]
pub enum TestInfo {
Group {
@ -329,6 +347,7 @@ pub enum TestInfo {
ParseTest {
outcome: TestOutcome,
// True parse rate, adjusted parse rate
#[schemars(schema_with = "parse_rate_schema")]
#[serde(serialize_with = "serialize_parse_rates")]
parse_rate: Option<(f64, f64)>,
test_num: usize,
@ -352,7 +371,11 @@ where
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
fn parse_rate_schema(gen: &mut SchemaGenerator) -> Schema {
gen.subschema_for::<Option<f64>>()
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema)]
pub enum TestOutcome {
// Parse outcomes
Passed,
@ -726,7 +749,7 @@ impl std::fmt::Display for TestDiff<'_> {
}
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, JsonSchema)]
pub struct TestFailure {
name: String,
actual: String,

View file

@ -21,7 +21,9 @@ bindgen = { version = "0.72.0" }
clap.workspace = true
indoc.workspace = true
regex.workspace = true
schemars.workspace = true
semver.workspace = true
serde_json.workspace = true
tree-sitter-cli = { path = "../cli/" }
notify = "8.2.0"
notify-debouncer-full = "0.6.0"

View file

@ -7,6 +7,7 @@ mod embed_sources;
mod fetch;
mod generate;
mod test;
mod test_schema;
mod upgrade_wasmtime;
use std::{path::Path, process::Command};
@ -40,6 +41,8 @@ enum Commands {
GenerateBindings,
/// Generates the fixtures for testing tree-sitter.
GenerateFixtures(GenerateFixtures),
/// Generates the JSON schema for the test runner summary.
GenerateTestSchema,
/// Generate the list of exports from Tree-sitter Wasm files.
GenerateWasmExports,
/// Run the test suite
@ -236,6 +239,7 @@ fn run() -> Result<()> {
Commands::GenerateFixtures(generate_fixtures_options) => {
generate::run_fixtures(&generate_fixtures_options)?;
}
Commands::GenerateTestSchema => test_schema::run_test_schema()?,
Commands::GenerateWasmExports => generate::run_wasm_exports()?,
Commands::Test(test_options) => test::run(&test_options)?,
Commands::TestWasm => test::run_wasm()?,

View file

@ -0,0 +1,25 @@
use std::path::PathBuf;
use anyhow::Result;
use serde_json::to_writer_pretty;
use tree_sitter_cli::test::TestSummary;
pub fn run_test_schema() -> Result<()> {
let schema = schemars::schema_for!(TestSummary);
let xtask_path: PathBuf = env!("CARGO_MANIFEST_DIR").into();
let schema_path = xtask_path
.parent()
.unwrap()
.parent()
.unwrap()
.join("docs")
.join("src")
.join("assets")
.join("schemas")
.join("test-summary.schema.json");
let mut file = std::fs::File::create(schema_path)?;
Ok(to_writer_pretty(&mut file, &schema)?)
}

View file

@ -0,0 +1,247 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "TestSummary",
"description": "A stateful object used to collect results from running a grammar's test suite",
"type": "object",
"properties": {
"parse_results": {
"type": "array",
"items": {
"$ref": "#/$defs/TestResult"
}
},
"parse_failures": {
"type": "array",
"items": {
"$ref": "#/$defs/TestFailure"
}
},
"parse_stats": {
"$ref": "#/$defs/Stats"
},
"highlight_results": {
"type": "array",
"items": {
"$ref": "#/$defs/TestResult"
}
},
"tag_results": {
"type": "array",
"items": {
"$ref": "#/$defs/TestResult"
}
},
"query_results": {
"type": "array",
"items": {
"$ref": "#/$defs/TestResult"
}
}
},
"required": [
"parse_results",
"parse_failures",
"parse_stats",
"highlight_results",
"tag_results",
"query_results"
],
"$defs": {
"TestResult": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"anyOf": [
{
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/$defs/TestResult"
}
}
},
"required": [
"children"
]
},
{
"type": "object",
"properties": {
"outcome": {
"$ref": "#/$defs/TestOutcome"
},
"parse_rate": {
"type": [
"number",
"null"
],
"format": "double"
},
"test_num": {
"type": "integer",
"format": "uint",
"minimum": 0
}
},
"required": [
"outcome",
"parse_rate",
"test_num"
]
},
{
"type": "object",
"properties": {
"outcome": {
"$ref": "#/$defs/TestOutcome"
},
"test_num": {
"type": "integer",
"format": "uint",
"minimum": 0
}
},
"required": [
"outcome",
"test_num"
]
}
]
},
"TestOutcome": {
"oneOf": [
{
"type": "string",
"enum": [
"Passed",
"Failed",
"Updated",
"Skipped",
"Platform"
]
},
{
"type": "object",
"properties": {
"AssertionPassed": {
"type": "object",
"properties": {
"assertion_count": {
"type": "integer",
"format": "uint",
"minimum": 0
}
},
"required": [
"assertion_count"
]
}
},
"required": [
"AssertionPassed"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"AssertionFailed": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
},
"required": [
"AssertionFailed"
],
"additionalProperties": false
}
]
},
"TestFailure": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"actual": {
"type": "string"
},
"expected": {
"type": "string"
},
"is_cst": {
"type": "boolean"
}
},
"required": [
"name",
"actual",
"expected",
"is_cst"
]
},
"Stats": {
"type": "object",
"properties": {
"successful_parses": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"total_parses": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"total_bytes": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"total_duration": {
"$ref": "#/$defs/Duration"
}
},
"required": [
"successful_parses",
"total_parses",
"total_bytes",
"total_duration"
]
},
"Duration": {
"type": "object",
"properties": {
"secs": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"nanos": {
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
"secs",
"nanos"
]
}
}
}