feat(test): display test results in JSON format

This commit is contained in:
WillLillis 2025-09-28 22:57:56 -04:00 committed by Will Lillis
parent 6a8676f335
commit f02d7e7e33
7 changed files with 799 additions and 419 deletions

View file

@ -24,11 +24,12 @@ use tree_sitter_cli::{
input::{get_input, get_tmp_source_file, CliInput},
logger,
parse::{self, ParseDebugType, ParseFileOptions, ParseOutput, ParseTheme},
playground, query,
playground,
query::{self, QueryFileOptions},
tags::{self, TagsOptions},
test::{self, TestOptions, TestStats},
test_highlight, test_tags, util, version,
version::BumpLevel,
test::{self, TestOptions, TestStats, TestSummary},
test_highlight, test_tags, util,
version::{self, BumpLevel},
wasm,
};
use tree_sitter_config::Config;
@ -328,6 +329,9 @@ struct Test {
/// Show only the pass-fail overview tree
#[arg(long)]
pub overview_only: bool,
/// Output the test summary in a JSON output
#[arg(long)]
pub json_summary: bool,
}
#[derive(Args)]
@ -1150,6 +1154,28 @@ impl Parse {
}
}
/// In case an error is encountered, prints out the contents of `test_summary` and
/// propagates the error
fn check_test(
test_result: Result<()>,
test_summary: &TestSummary,
json_summary: bool,
) -> Result<()> {
if let Err(e) = test_result {
if json_summary {
let json_summary = serde_json::to_string_pretty(test_summary)
.expect("Failed to encode summary to JSON");
println!("{json_summary}");
} else {
println!("{test_summary}");
}
Err(e)?;
}
Ok(())
}
impl Test {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
@ -1194,15 +1220,18 @@ impl Test {
parser.set_language(language)?;
let test_dir = current_dir.join("test");
let mut stats = parse::Stats::default();
let mut test_summary = TestSummary::new(
color,
stat,
self.update,
self.overview_only,
self.json_summary,
);
// Run the corpus tests. Look for them in `test/corpus`.
let test_corpus_dir = test_dir.join("corpus");
if test_corpus_dir.is_dir() {
let mut output = String::new();
let mut rates = Vec::new();
let mut opts = TestOptions {
output: &mut output,
let opts = TestOptions {
path: test_corpus_dir,
debug: self.debug,
debug_graph: self.debug_graph,
@ -1213,51 +1242,67 @@ impl Test {
open_log: self.open_log,
languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(),
color,
test_num: 1,
parse_rates: &mut rates,
stat_display: stat,
stats: &mut stats,
show_fields: self.show_fields,
overview_only: self.overview_only,
};
test::run_tests_at_path(&mut parser, &mut opts)?;
println!("\n{stats}");
check_test(
test::run_tests_at_path(&mut parser, &opts, &mut test_summary),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
}
// Check that all of the queries are valid.
test::check_queries_at_path(language, &current_dir.join("queries"))?;
let query_dir = current_dir.join("queries");
check_test(
test::check_queries_at_path(language, &query_dir),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
// Run the syntax highlighting tests.
let test_highlight_dir = test_dir.join("highlight");
if test_highlight_dir.is_dir() {
let mut highlighter = Highlighter::new();
highlighter.parser = parser;
test_highlight::test_highlights(
&loader,
&config.get()?,
&mut highlighter,
&test_highlight_dir,
color,
check_test(
test_highlight::test_highlights(
&loader,
&config.get()?,
&mut highlighter,
&test_highlight_dir,
&mut test_summary,
),
&test_summary,
self.json_summary,
)?;
parser = highlighter.parser;
test_summary.test_num = 1;
}
let test_tag_dir = test_dir.join("tags");
if test_tag_dir.is_dir() {
let mut tags_context = TagsContext::new();
tags_context.parser = parser;
test_tags::test_tags(
&loader,
&config.get()?,
&mut tags_context,
&test_tag_dir,
color,
check_test(
test_tags::test_tags(
&loader,
&config.get()?,
&mut tags_context,
&test_tag_dir,
&mut test_summary,
),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
}
// For the rest of the queries, find their tests and run them
for entry in walkdir::WalkDir::new(current_dir.join("queries"))
for entry in walkdir::WalkDir::new(&query_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
@ -1280,27 +1325,41 @@ impl Test {
})
.collect::<Vec<_>>();
if !entries.is_empty() {
println!("{stem}:");
test_summary.query_results.add_group(stem);
}
for entry in entries {
test_summary.test_num = 1;
let opts = QueryFileOptions::default();
for entry in &entries {
let path = entry.path();
query::query_file_at_path(
language,
path,
&path.display().to_string(),
path,
false,
None,
None,
true,
false,
false,
false,
check_test(
query::query_file_at_path(
language,
path,
&path.display().to_string(),
path,
&opts,
Some(&mut test_summary),
),
&test_summary,
self.json_summary,
)?;
}
if !entries.is_empty() {
test_summary.query_results.pop_traversal();
}
}
}
test_summary.test_num = 1;
if self.json_summary {
let json_summary = serde_json::to_string_pretty(&test_summary)
.expect("Failed to encode test summary to JSON");
println!("{json_summary}");
} else {
println!("{test_summary}");
}
Ok(())
}
}
@ -1407,19 +1466,22 @@ impl Query {
lib_info.as_ref(),
)?;
let opts = QueryFileOptions {
ordered_captures: self.captures,
byte_range,
point_range,
quiet: self.quiet,
print_time: self.time,
stdin: false,
};
for path in paths {
query::query_file_at_path(
&language,
&path,
&path.display().to_string(),
query_path,
self.captures,
byte_range.clone(),
point_range.clone(),
self.test,
self.quiet,
self.time,
false,
&opts,
None,
)?;
}
}
@ -1447,19 +1509,15 @@ impl Query {
.map(|(l, _)| l.clone())
.ok_or_else(|| anyhow!("No language found"))?
};
query::query_file_at_path(
language,
&path,
&name,
query_path,
self.captures,
let opts = QueryFileOptions {
ordered_captures: self.captures,
byte_range,
point_range,
self.test,
self.quiet,
self.time,
true,
)?;
quiet: self.quiet,
print_time: self.time,
stdin: true,
};
query::query_file_at_path(language, &path, &name, query_path, &opts, None)?;
fs::remove_file(path)?;
}
CliInput::Stdin(contents) => {
@ -1469,19 +1527,15 @@ impl Query {
let path = get_tmp_source_file(&contents)?;
let language =
loader.select_language(&path, current_dir, None, lib_info.as_ref())?;
query::query_file_at_path(
&language,
&path,
"stdin",
query_path,
self.captures,
let opts = QueryFileOptions {
ordered_captures: self.captures,
byte_range,
point_range,
self.test,
self.quiet,
self.time,
true,
)?;
quiet: self.quiet,
print_time: self.time,
stdin: true,
};
query::query_file_at_path(&language, &path, "stdin", query_path, &opts, None)?;
fs::remove_file(path)?;
}
}