feat(cli): add test listing and allow users to parse a specific test number

This commit is contained in:
WillLillis 2024-02-22 20:23:24 -05:00 committed by Amaan Qureshi
parent 33045ee898
commit 577d333849
2 changed files with 161 additions and 17 deletions

View file

@ -195,6 +195,9 @@ struct Parse {
pub open_log: bool,
#[arg(long, help = "The path to an alternative config.json file")]
pub config_path: Option<PathBuf>,
#[arg(long, short = 'n', help = "Parse the contents of a specific test")]
#[clap(conflicts_with = "paths", conflicts_with = "paths_file")]
pub test_number: Option<u32>,
}
#[derive(Args)]
@ -410,6 +413,8 @@ fn run() -> Result<()> {
let current_dir = env::current_dir().unwrap();
let mut loader = loader::Loader::new()?;
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
match command {
Commands::InitConfig(_) => {
if let Ok(Some(config_path)) = Config::find_config_file() {
@ -576,7 +581,22 @@ fn run() -> Result<()> {
let timeout = parse_options.timeout.unwrap_or_default();
let paths = collect_paths(parse_options.paths_file.as_deref(), parse_options.paths)?;
let (paths, language) = if let Some(target_test) = parse_options.test_number {
let (test_path, language_names) = test::get_tmp_test_file(target_test, color)?;
let languages = loader.languages_at_path(&current_dir)?;
let language = languages
.iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
.map(|(l, _)| l.clone());
let paths =
collect_paths(None, Some(vec![test_path.to_str().unwrap().to_owned()]))?;
(paths, language)
} else {
(
collect_paths(parse_options.paths_file.as_deref(), parse_options.paths)?,
None,
)
};
let max_path_length = paths.iter().map(|p| p.chars().count()).max().unwrap_or(0);
let mut has_error = false;
@ -586,11 +606,14 @@ fn run() -> Result<()> {
let should_track_stats = parse_options.stat;
let mut stats = parse::Stats::default();
for path in paths {
for path in &paths {
let path = Path::new(&path);
let language =
loader.select_language(path, &current_dir, parse_options.scope.as_deref())?;
let language = if let Some(ref language) = language {
language.clone()
} else {
loader.select_language(path, &current_dir, parse_options.scope.as_deref())?
};
parser
.set_language(&language)
.context("incompatible language")?;
@ -645,8 +668,6 @@ fn run() -> Result<()> {
env::set_var("TREE_SITTER_DEBUG", "1");
}
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
loader.use_debug_build(test_options.debug_build);
let mut parser = Parser::new();
@ -683,6 +704,7 @@ fn run() -> Result<()> {
open_log: test_options.open_log,
languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(),
color,
test_num: 1,
};
test::run_tests_at_path(&mut parser, &mut opts)?;

View file

@ -1,5 +1,5 @@
use std::{
collections::BTreeMap,
collections::{BTreeMap, HashSet},
ffi::OsStr,
fs,
io::{self, Write},
@ -105,6 +105,7 @@ pub struct TestOptions<'a> {
pub open_log: bool,
pub languages: BTreeMap<&'a str, &'a Language>,
pub color: bool,
pub test_num: usize,
}
pub fn run_tests_at_path(parser: &mut Parser, opts: &mut TestOptions) -> Result<()> {
@ -186,6 +187,65 @@ pub fn run_tests_at_path(parser: &mut Parser, opts: &mut TestOptions) -> Result<
}
}
#[allow(clippy::type_complexity)]
pub fn get_test_info<'test>(
test_entry: &'test TestEntry,
target_test: u32,
test_num: &mut u32,
) -> Option<(&'test str, &'test [u8], Vec<Box<str>>)> {
match test_entry {
TestEntry::Example {
name,
input,
attributes,
..
} => {
if *test_num == target_test {
return Some((name, input, attributes.languages.clone()));
} else {
*test_num += 1;
}
}
TestEntry::Group { children, .. } => {
for child in children {
if let Some((name, input, languages)) = get_test_info(child, target_test, test_num)
{
return Some((name, input, languages));
}
}
}
}
None
}
/// Writes the input of `target_test` to a temporary file and returns the path
pub fn get_tmp_test_file(target_test: u32, color: bool) -> Result<(PathBuf, Vec<Box<str>>)> {
let current_dir = std::env::current_dir().unwrap();
let test_dir = current_dir.join("test").join("corpus");
// Get the input of the target test
let test_entry = parse_tests(&test_dir)?;
let mut test_num = 0;
let Some((test_name, test_contents, languages)) =
get_test_info(&test_entry, target_test - 1, &mut test_num)
else {
return Err(anyhow!("Failed to fetch contents of test #{target_test}"));
};
// Write the test contents to a temporary file
let test_path = std::env::temp_dir().join(".tree-sitter-test");
let mut test_file = std::fs::File::create(&test_path)?;
test_file.write_all(test_contents)?;
println!(
"{target_test}. {}\n",
opt_color(color, Colour::Green, test_name)
);
Ok((test_path, languages))
}
pub fn check_queries_at_path(language: &Language, path: &Path) -> Result<()> {
if path.exists() {
for entry in WalkDir::new(path)
@ -253,6 +313,7 @@ pub fn opt_color(use_color: bool, color: ansi_term::Colour, text: &str) -> Strin
}
}
#[allow(clippy::too_many_arguments)]
fn run_tests(
parser: &mut Parser,
test_entry: TestEntry,
@ -275,12 +336,20 @@ fn run_tests(
print!("{}", " ".repeat(indent_level as usize));
if attributes.skip {
println!("{}", opt_color(opts.color, Colour::Yellow, &name));
println!(
"{:>3}.  {}",
opts.test_num,
opt_color(opts.color, Colour::Yellow, &name),
);
return Ok(true);
}
if !attributes.platform {
println!("{}", opt_color(opts.color, Colour::Purple, &name));
println!(
"{:>3}.  {}",
opts.test_num,
opt_color(opts.color, Colour::Purple, &name)
);
return Ok(true);
}
@ -296,9 +365,17 @@ fn run_tests(
if attributes.error {
if tree.root_node().has_error() {
println!("{}", opt_color(opts.color, Colour::Green, &name));
println!(
"{:>3}.  {}",
opts.test_num,
opt_color(opts.color, Colour::Green, &name)
);
} else {
println!("{}", opt_color(opts.color, Colour::Red, &name));
println!(
"{:>3}.  {}",
opts.test_num,
opt_color(opts.color, Colour::Red, &name)
);
}
if attributes.fail_fast {
@ -311,7 +388,11 @@ fn run_tests(
}
if actual == output {
println!("{}", opt_color(opts.color, Colour::Green, &name));
println!(
"{:>3}. ✓ {}",
opts.test_num,
opt_color(opts.color, Colour::Green, &name),
);
if opts.update {
let input = String::from_utf8(input.clone()).unwrap();
let output = format_sexp(&output, 0);
@ -353,10 +434,18 @@ fn run_tests(
header_delim_len,
divider_delim_len,
));
println!("{}", opt_color(opts.color, Colour::Blue, &name));
println!(
"{:>3}. ✓ {}",
opts.test_num,
opt_color(opts.color, Colour::Blue, &name)
);
}
} else {
println!("{}", opt_color(opts.color, Colour::Red, &name));
println!(
"{:>3}. ✗ {}",
opts.test_num,
opt_color(opts.color, Colour::Red, &name)
);
}
failures.push((name.clone(), actual, output.clone()));
@ -372,34 +461,51 @@ fn run_tests(
}
}
}
opts.test_num += 1;
}
TestEntry::Group {
name,
mut children,
file_path,
} => {
children.retain(|child| {
if let TestEntry::Example { name, .. } = child {
// track which tests are being skipped to maintain consistent numbering while using
// filters
let mut skipped_tests = HashSet::new();
let mut advance_counter = opts.test_num;
children.retain(|child| match child {
TestEntry::Example { name, .. } => {
if let Some(filter) = opts.filter {
if !name.contains(filter) {
skipped_tests.insert(advance_counter);
advance_counter += 1;
return false;
}
}
if let Some(include) = &opts.include {
if !include.is_match(name) {
skipped_tests.insert(advance_counter);
advance_counter += 1;
return false;
}
}
if let Some(exclude) = &opts.exclude {
if exclude.is_match(name) {
skipped_tests.insert(advance_counter);
advance_counter += 1;
return false;
}
}
advance_counter += 1;
true
}
TestEntry::Group { .. } => {
advance_counter += count_subtests(child);
true
}
true
});
if children.is_empty() {
opts.test_num = advance_counter;
return Ok(true);
}
@ -412,6 +518,11 @@ fn run_tests(
indent_level += 1;
for child in children {
if let TestEntry::Example { .. } = child {
while skipped_tests.remove(&opts.test_num) {
opts.test_num += 1;
}
}
if !run_tests(
parser,
child,
@ -426,6 +537,8 @@ fn run_tests(
}
}
opts.test_num += skipped_tests.len();
if let Some(file_path) = file_path {
if opts.update && failures.len() - failure_count > 0 {
write_tests(&file_path, corrected_entries)?;
@ -437,6 +550,15 @@ fn run_tests(
Ok(true)
}
fn count_subtests(test_entry: &TestEntry) -> usize {
match test_entry {
TestEntry::Example { .. } => 1,
TestEntry::Group { children, .. } => children
.iter()
.fold(0, |count, child| count + count_subtests(child)),
}
}
fn write_tests(
file_path: &Path,
corrected_entries: &[(String, String, String, usize, usize)],