diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 33a9904f..e996083a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,6 +9,7 @@ pub mod test; pub mod util; pub mod wasm; pub mod web_ui; +pub mod print; #[cfg(test)] mod tests; diff --git a/cli/src/main.rs b/cli/src/main.rs index 59d04a97..7a5adbf1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -83,7 +83,8 @@ fn run() -> error::Result<()> { .takes_value(true), ) .arg(Arg::with_name("debug").long("debug").short("d")) - .arg(Arg::with_name("debug-graph").long("debug-graph").short("D")), + .arg(Arg::with_name("debug-graph").long("debug-graph").short("D")) + .arg(Arg::with_name("update").long("update").short("u")), ) .subcommand( SubCommand::with_name("highlight") @@ -150,9 +151,10 @@ fn run() -> error::Result<()> { let debug = matches.is_present("debug"); let debug_graph = matches.is_present("debug-graph"); let filter = matches.value_of("filter"); + let update = matches.is_present("update"); let corpus_path = current_dir.join("corpus"); if let Some(language) = loader.languages_at_path(¤t_dir)?.first() { - test::run_tests_at_path(*language, &corpus_path, debug, debug_graph, filter)?; + test::run_tests_at_path(*language, &corpus_path, debug, debug_graph, filter, update)?; } else { eprintln!("No language found"); } diff --git a/cli/src/parse.rs b/cli/src/parse.rs index d1ddb499..065ffb02 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -1,4 +1,5 @@ use super::error::{Error, Result}; +use super::print::print_tree; use super::util; use std::io::{self, Write}; use std::path::Path; @@ -81,57 +82,7 @@ pub fn parse_file_at_path( let mut cursor = tree.walk(); if !quiet { - let mut needs_newline = false; - let mut indent_level = 0; - let mut did_visit_children = false; - loop { - let node = cursor.node(); - let is_named = node.is_named(); - if did_visit_children { - if is_named { - stdout.write(b")")?; - needs_newline = true; - } - if cursor.goto_next_sibling() { - did_visit_children = false; - } else if cursor.goto_parent() { - did_visit_children = true; - indent_level -= 1; - } else { - break; - } - } else { - if is_named { - if needs_newline { - stdout.write(b"\n")?; - } - for _ in 0..indent_level { - stdout.write(b" ")?; - } - let start = node.start_position(); - let end = node.end_position(); - if let Some(field_name) = cursor.field_name() { - write!(&mut stdout, "{}: ", field_name)?; - } - write!( - &mut stdout, - "({} [{}, {}] - [{}, {}]", - node.kind(), - start.row, - start.column, - end.row, - end.column - )?; - needs_newline = true; - } - if cursor.goto_first_child() { - did_visit_children = false; - indent_level += 1; - } else { - did_visit_children = true; - } - } - } + print_tree(&mut stdout, &mut cursor, true)?; cursor.reset(tree.root_node()); println!(""); } diff --git a/cli/src/print.rs b/cli/src/print.rs new file mode 100644 index 00000000..7a57c625 --- /dev/null +++ b/cli/src/print.rs @@ -0,0 +1,60 @@ +use super::error::{Result}; +use std::io::{Write}; +use tree_sitter::{TreeCursor}; + +pub fn print_tree(output: &mut Write, cursor: &mut TreeCursor, prints_position: bool) -> Result<()> { + let mut needs_newline = false; + let mut indent_level = 0; + let mut did_visit_children = false; + loop { + let node = cursor.node(); + let is_named = node.is_named(); + if did_visit_children { + if is_named { + output.write(b")")?; + needs_newline = true; + } + if cursor.goto_next_sibling() { + did_visit_children = false; + } else if cursor.goto_parent() { + did_visit_children = true; + indent_level -= 1; + } else { + break; + } + } else { + if is_named { + if needs_newline { + output.write(b"\n")?; + } + for _ in 0..indent_level { + output.write(b" ")?; + } + if let Some(field_name) = cursor.field_name() { + write!(output, "{}: ", field_name)?; + } + write!(output, "({}", node.kind())?; + if prints_position { + let start = node.start_position(); + let end = node.end_position(); + write!( + output, + " [{}, {}] - [{}, {}]", + start.row, + start.column, + end.row, + end.column + )?; + } + needs_newline = true; + } + if cursor.goto_first_child() { + did_visit_children = false; + indent_level += 1; + } else { + did_visit_children = true; + } + } + } + return Ok(()); +} diff --git a/cli/src/test.rs b/cli/src/test.rs index f742e887..7a08e805 100644 --- a/cli/src/test.rs +++ b/cli/src/test.rs @@ -1,4 +1,5 @@ use super::error::{Error, Result}; +use super::print::print_tree; use super::util; use ansi_term::Colour; use difference::{Changeset, Difference}; @@ -8,7 +9,7 @@ use regex::Regex; use std::char; use std::fs; use std::io::{self, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::str; use tree_sitter::{Language, LogType, Parser}; @@ -30,6 +31,7 @@ pub enum TestEntry { Group { name: String, children: Vec, + file_path: Option, }, Example { name: String, @@ -44,6 +46,7 @@ impl Default for TestEntry { TestEntry::Group { name: String::new(), children: Vec::new(), + file_path: None, } } } @@ -54,43 +57,52 @@ pub fn run_tests_at_path( debug: bool, debug_graph: bool, filter: Option<&str>, + update: bool, ) -> Result<()> { - let test_entry = parse_tests(path)?; + let test_entry = parse_tests(path, false)?; let mut _log_session = None; let mut parser = Parser::new(); parser.set_language(language).map_err(|e| e.to_string())?; - if debug_graph { - _log_session = Some(util::log_graphs(&mut parser, "log.html")?); - } else if debug { - parser.set_logger(Some(Box::new(|log_type, message| { - if log_type == LogType::Lex { - io::stderr().write(b" ").unwrap(); - } - write!(&mut io::stderr(), "{}\n", message).unwrap(); - }))); - } - - let mut failures = Vec::new(); - if let TestEntry::Group { children, .. } = test_entry { - for child in children { - run_tests(&mut parser, child, filter, 0, &mut failures)?; + if !update { + if debug_graph { + _log_session = Some(util::log_graphs(&mut parser, "log.html")?); + } else if debug { + parser.set_logger(Some(Box::new(|log_type, message| { + if log_type == LogType::Lex { + io::stderr().write(b" ").unwrap(); + } + write!(&mut io::stderr(), "{}\n", message).unwrap(); + }))); } } - if failures.len() > 0 { + let mut diffs = Vec::new(); + let mut update_entries = Vec::new(); + run_tests(&mut parser, test_entry, filter, update, &mut update_entries, -1, &mut diffs)?; + + if diffs.len() > 0 { println!(""); - if failures.len() == 1 { - println!("1 failure:") + let diff_name = if update { "update" } else { "failure" }; + if diffs.len() == 1 { + println!("1 {}:", diff_name) } else { - println!("{} failures:", failures.len()) + println!("{} {}s:", diffs.len(), diff_name) } - print_diff_key(); - for (i, (name, actual, expected)) in failures.iter().enumerate() { + if update { + print_update_diff_key(); + } else { + print_diff_key(); + } + for (i, (name, parsed, provided)) in diffs.iter().enumerate() { println!("\n {}. {}:", i + 1, name); - print_diff(actual, expected); + if update { + print_update_diff(provided, parsed); + } else { + print_diff(parsed, provided); + } } Error::err(String::new()) } else { @@ -99,14 +111,40 @@ pub fn run_tests_at_path( } pub fn print_diff_key() { + print_diff_key_with_colors("actual", "expected", Colour::Red, Colour::Green); +} + +fn print_update_diff_key() { + print_diff_key_with_colors("original", "updated", Colour::Yellow, Colour::Green); +} + +fn print_diff_key_with_colors( + actual_name: &str, + expected_name: &str, + actual_color: Colour, + expected_color: Colour, +) { println!( "\n{} / {}", - Colour::Green.paint("expected"), - Colour::Red.paint("actual") + expected_color.paint(expected_name), + actual_color.paint(actual_name) ); } pub fn print_diff(actual: &String, expected: &String) { + print_diff_with_colors(actual, expected, Colour::Red, Colour::Green); +} + +fn print_update_diff(actual: &String, expected: &String) { + print_diff_with_colors(actual, expected, Colour::Yellow, Colour::Green); +} + +fn print_diff_with_colors( + actual: &String, + expected: &String, + actual_color: Colour, + expected_color: Colour, +) { let changeset = Changeset::new(actual, expected, " "); print!(" "); for diff in &changeset.diffs { @@ -115,10 +153,10 @@ pub fn print_diff(actual: &String, expected: &String) { print!("{}{}", part, changeset.split); } Difference::Add(part) => { - print!("{}{}", Colour::Green.paint(part), changeset.split); + print!("{}{}", expected_color.paint(part), changeset.split); } Difference::Rem(part) => { - print!("{}{}", Colour::Red.paint(part), changeset.split); + print!("{}{}", actual_color.paint(part), changeset.split); } } } @@ -129,8 +167,10 @@ fn run_tests( parser: &mut Parser, test_entry: TestEntry, filter: Option<&str>, + update: bool, + update_entries: &mut Vec<(String, String, String)>, mut indent_level: i32, - failures: &mut Vec<(String, String, String)>, + diffs: &mut Vec<(String, String, String)>, ) -> Result<()> { match test_entry { TestEntry::Example { @@ -141,39 +181,97 @@ fn run_tests( } => { if let Some(filter) = filter { if !name.contains(filter) { + if update { + let input = String::from_utf8(input).unwrap(); + update_entries.push((name, input, output)); + } return Ok(()); } } let tree = parser.parse(&input, None).unwrap(); - let mut actual = tree.root_node().to_sexp(); + let mut parsed = tree.root_node().to_sexp(); if !has_fields { - actual = strip_sexp_fields(actual); + parsed = strip_sexp_fields(parsed); } for _ in 0..indent_level { print!(" "); } - if actual == output { + let provided = normalize_sexp(&output); + if parsed == provided { println!("✓ {}", Colour::Green.paint(&name)); + if update { + let input = String::from_utf8(input).unwrap(); + update_entries.push((name, input, output)); + } } else { - println!("✗ {}", Colour::Red.paint(&name)); - failures.push((name, actual, output)); + if update { + let input = String::from_utf8(input).unwrap(); + let mut fixed_output = Vec::new(); + let mut cursor = tree.walk(); + print_tree(&mut fixed_output, &mut cursor, false)?; + let fixed_output = String::from_utf8(fixed_output).unwrap(); + update_entries.push((name.clone(), input, fixed_output)); + println!("✓ {}", Colour::Yellow.paint(&name)); + } else { + println!("✗ {}", Colour::Red.paint(&name)); + } + diffs.push((name, parsed, provided)); } } - TestEntry::Group { name, children } => { - for _ in 0..indent_level { - print!(" "); + TestEntry::Group { name, children, file_path } => { + if indent_level >= 0 { + for _ in 0..indent_level { + print!(" "); + } + println!("{}:", name); } - println!("{}:", name); + + let diff_count = diffs.len(); + indent_level += 1; for child in children { - run_tests(parser, child, filter, indent_level, failures)?; + run_tests(parser, child, filter, update, update_entries, indent_level, diffs)?; + } + + if let Some(file_path) = file_path { + if update && diffs.len() - diff_count > 0 { + write_tests(&file_path, &update_entries)?; + } + update_entries.clear(); } } } Ok(()) } -pub fn parse_tests(path: &Path) -> io::Result { +fn write_tests(file_path: &Path, update_entries: &Vec<(String, String, String)>) -> Result<()> { + let mut buffer = fs::File::create(file_path)?; + write_tests_to_buffer(&mut buffer, update_entries) +} + +fn write_tests_to_buffer( + buffer: &mut Write, + update_entries: &Vec<(String, String, String)>, +) -> Result<()> { + for (i, (name, input, output)) in update_entries.iter().enumerate() { + if i > 0 { + write!(buffer, "\n")?; + } + write!( + buffer, + "{}\n{}\n{}\n{}\n{}\n\n{}\n", + "=".repeat(80), + name, + "=".repeat(80), + input, + "-".repeat(80), + output.trim() + )?; + } + Ok(()) +} + +pub fn parse_tests(path: &Path, norm_sexp: bool) -> io::Result { let name = path .file_stem() .and_then(|s| s.to_str()) @@ -189,13 +287,13 @@ pub fn parse_tests(path: &Path) -> io::Result { .unwrap_or("") .starts_with("."); if !hidden { - children.push(parse_tests(&entry.path())?); + children.push(parse_tests(&entry.path(), norm_sexp)?); } } - Ok(TestEntry::Group { name, children }) + Ok(TestEntry::Group { name, children, file_path: None }) } else { let content = fs::read_to_string(path)?; - Ok(parse_test_content(name, content)) + Ok(parse_test_content(name, content, Some(path.to_path_buf()), norm_sexp)) } } @@ -203,7 +301,12 @@ pub fn strip_sexp_fields(sexp: String) -> String { SEXP_FIELD_REGEX.replace_all(&sexp, " (").to_string() } -fn parse_test_content(name: String, content: String) -> TestEntry { +fn parse_test_content( + name: String, + content: String, + file_path: Option, + norm_sexp: bool, +) -> TestEntry { let mut children = Vec::new(); let bytes = content.as_bytes(); let mut previous_name = String::new(); @@ -224,8 +327,11 @@ fn parse_test_content(name: String, content: String) -> TestEntry { ); if let Ok(output) = str::from_utf8(&bytes[divider_end..header_start]) { let input = bytes[previous_header_end..divider_start].to_vec(); - let output = WHITESPACE_REGEX.replace_all(output.trim(), " ").to_string(); - let output = output.replace(" )", ")"); + let output = if norm_sexp { + normalize_sexp(output) + } else { + output.to_owned() + }; let has_fields = SEXP_FIELD_REGEX.is_match(&output); children.push(TestEntry::Example { name: previous_name, @@ -241,7 +347,13 @@ fn parse_test_content(name: String, content: String) -> TestEntry { .to_string(); previous_header_end = header_end; } - TestEntry::Group { name, children } + TestEntry::Group { name, children, file_path } +} + +fn normalize_sexp(sexp: &str) -> String { + let sexp = WHITESPACE_REGEX.replace_all(sexp.trim(), " ").to_string(); + let sexp = sexp.replace(" )", ")"); + return sexp; } #[cfg(test)] @@ -273,6 +385,8 @@ d "# .trim() .to_string(), + None, + true, ); assert_eq!( @@ -292,8 +406,49 @@ d output: "(d)".to_string(), has_fields: false, }, - ] + ], + file_path: None } ); } + + #[test] + fn test_write_tests_to_buffer() { + let mut buffer = Vec::new(); + let update_entries = vec![ + ( + "title 1".to_string(), + "input 1".to_string(), + "output 1".to_string(), + ), + ( + "title 2".to_string(), + "input 2".to_string(), + "output 2".to_string(), + ), + ]; + write_tests_to_buffer(&mut buffer, &update_entries).unwrap(); + assert_eq!( + String::from_utf8(buffer).unwrap(), + r#" +================================================================================ +title 1 +================================================================================ +input 1 +-------------------------------------------------------------------------------- + +output 1 + +================================================================================ +title 2 +================================================================================ +input 2 +-------------------------------------------------------------------------------- + +output 2 +"# + .trim_start() + .to_string() + ); + } } diff --git a/cli/src/tests/corpus_test.rs b/cli/src/tests/corpus_test.rs index a8adce5f..dae87542 100644 --- a/cli/src/tests/corpus_test.rs +++ b/cli/src/tests/corpus_test.rs @@ -59,8 +59,8 @@ fn test_real_language_corpus_files() { let language = get_language(language_name); let corpus_dir = grammars_dir.join(language_name).join("corpus"); let error_corpus_file = error_corpus_dir.join(&format!("{}_errors.txt", language_name)); - let main_tests = parse_tests(&corpus_dir).unwrap(); - let error_tests = parse_tests(&error_corpus_file).unwrap_or(TestEntry::default()); + let main_tests = parse_tests(&corpus_dir, true).unwrap(); + let error_tests = parse_tests(&error_corpus_file, true).unwrap_or(TestEntry::default()); let mut tests = flatten_tests(main_tests); tests.extend(flatten_tests(error_tests)); @@ -243,7 +243,7 @@ fn test_feature_corpus_files() { let corpus_path = test_path.join("corpus.txt"); let c_code = generate_result.unwrap().1; let language = get_test_language(language_name, &c_code, Some(&test_path)); - let test = parse_tests(&corpus_path).unwrap(); + let test = parse_tests(&corpus_path, true).unwrap(); let tests = flatten_tests(test); if !tests.is_empty() { @@ -381,7 +381,7 @@ fn flatten_tests(test: TestEntry) -> Vec<(String, Vec, String, bool)> { } result.push((name, input, output, has_fields)); } - TestEntry::Group { mut name, children } => { + TestEntry::Group { mut name, children, .. } => { if !prefix.is_empty() { name.insert_str(0, " - "); name.insert_str(0, prefix);