Merge pull request #442 from ikatyang/feat/snapshot-testing
feat(cli): support snapshot testing with `--update` flag
This commit is contained in:
commit
bd5a9a813c
5 changed files with 250 additions and 32 deletions
|
|
@ -12,7 +12,7 @@ use std::i32;
|
|||
|
||||
lazy_static! {
|
||||
static ref CURLY_BRACE_REGEX: Regex =
|
||||
Regex::new(r#"(^|[^\\])\{([^}]*[^0-9A-F,}][^}]*)\}"#).unwrap();
|
||||
Regex::new(r#"(^|[^\\])\{([^}]*[^0-9A-Fa-f,}][^}]*)\}"#).unwrap();
|
||||
}
|
||||
|
||||
const ALLOWED_REDUNDANT_ESCAPED_CHARS: [char; 4] = ['!', '\'', '"', '/'];
|
||||
|
|
@ -653,12 +653,15 @@ mod tests {
|
|||
Rule::pattern(r#"\{[ab]{3}\}"#),
|
||||
// Unicode codepoints
|
||||
Rule::pattern(r#"\u{1000A}"#),
|
||||
// Unicode codepoints (lowercase)
|
||||
Rule::pattern(r#"\u{1000b}"#),
|
||||
],
|
||||
separators: vec![],
|
||||
examples: vec![
|
||||
("u{1234} ok", Some((0, "u{1234}"))),
|
||||
("{aba}}", Some((1, "{aba}"))),
|
||||
("\u{1000A}", Some((2, "\u{1000A}"))),
|
||||
("\u{1000b}", Some((3, "\u{1000b}"))),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ fn run() -> error::Result<()> {
|
|||
.short("f")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(Arg::with_name("update").long("update").short("u"))
|
||||
.arg(Arg::with_name("debug").long("debug").short("d"))
|
||||
.arg(Arg::with_name("debug-graph").long("debug-graph").short("D")),
|
||||
)
|
||||
|
|
@ -193,6 +194,7 @@ fn run() -> error::Result<()> {
|
|||
} else if let Some(matches) = matches.subcommand_matches("test") {
|
||||
let debug = matches.is_present("debug");
|
||||
let debug_graph = matches.is_present("debug-graph");
|
||||
let update = matches.is_present("update");
|
||||
let filter = matches.value_of("filter");
|
||||
let languages = loader.languages_at_path(¤t_dir)?;
|
||||
let language = languages
|
||||
|
|
@ -206,7 +208,7 @@ fn run() -> error::Result<()> {
|
|||
test_corpus_dir = current_dir.join("corpus");
|
||||
}
|
||||
if test_corpus_dir.is_dir() {
|
||||
test::run_tests_at_path(*language, &test_corpus_dir, debug, debug_graph, filter)?;
|
||||
test::run_tests_at_path(*language, &test_corpus_dir, debug, debug_graph, filter, update)?;
|
||||
}
|
||||
|
||||
// Check that all of the queries are valid.
|
||||
|
|
|
|||
268
cli/src/test.rs
268
cli/src/test.rs
|
|
@ -6,9 +6,10 @@ use lazy_static::lazy_static;
|
|||
use regex::bytes::{Regex as ByteRegex, RegexBuilder as ByteRegexBuilder};
|
||||
use regex::Regex;
|
||||
use std::char;
|
||||
use std::fmt::Write as FmtWrite;
|
||||
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, Query};
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ pub enum TestEntry {
|
|||
Group {
|
||||
name: String,
|
||||
children: Vec<TestEntry>,
|
||||
file_path: Option<PathBuf>,
|
||||
},
|
||||
Example {
|
||||
name: String,
|
||||
|
|
@ -44,6 +46,7 @@ impl Default for TestEntry {
|
|||
TestEntry::Group {
|
||||
name: String::new(),
|
||||
children: Vec::new(),
|
||||
file_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +57,7 @@ pub fn run_tests_at_path(
|
|||
debug: bool,
|
||||
debug_graph: bool,
|
||||
filter: Option<&str>,
|
||||
update: bool,
|
||||
) -> Result<()> {
|
||||
let test_entry = parse_tests(path)?;
|
||||
let mut _log_session = None;
|
||||
|
|
@ -72,27 +76,45 @@ pub fn run_tests_at_path(
|
|||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
let mut corrected_entries = Vec::new();
|
||||
run_tests(
|
||||
&mut parser,
|
||||
test_entry,
|
||||
filter,
|
||||
0,
|
||||
&mut failures,
|
||||
update,
|
||||
&mut corrected_entries,
|
||||
)?;
|
||||
|
||||
if failures.len() > 0 {
|
||||
println!("");
|
||||
|
||||
if failures.len() == 1 {
|
||||
println!("1 failure:")
|
||||
} else {
|
||||
println!("{} failures:", failures.len())
|
||||
}
|
||||
if update {
|
||||
if failures.len() == 1 {
|
||||
println!("1 update:\n")
|
||||
} else {
|
||||
println!("{} updates:\n", failures.len())
|
||||
}
|
||||
|
||||
print_diff_key();
|
||||
for (i, (name, actual, expected)) in failures.iter().enumerate() {
|
||||
println!("\n {}. {}:", i + 1, name);
|
||||
print_diff(actual, expected);
|
||||
for (i, (name, ..)) in failures.iter().enumerate() {
|
||||
println!(" {}. {}", i + 1, name);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
if failures.len() == 1 {
|
||||
println!("1 failure:")
|
||||
} else {
|
||||
println!("{} failures:", failures.len())
|
||||
}
|
||||
|
||||
print_diff_key();
|
||||
for (i, (name, actual, expected)) in failures.iter().enumerate() {
|
||||
println!("\n {}. {}:", i + 1, name);
|
||||
print_diff(actual, expected);
|
||||
}
|
||||
Error::err(String::new())
|
||||
}
|
||||
Error::err(String::new())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -149,6 +171,8 @@ fn run_tests(
|
|||
filter: Option<&str>,
|
||||
mut indent_level: i32,
|
||||
failures: &mut Vec<(String, String, String)>,
|
||||
update: bool,
|
||||
corrected_entries: &mut Vec<(String, String, String)>,
|
||||
) -> Result<()> {
|
||||
match test_entry {
|
||||
TestEntry::Example {
|
||||
|
|
@ -159,6 +183,11 @@ fn run_tests(
|
|||
} => {
|
||||
if let Some(filter) = filter {
|
||||
if !name.contains(filter) {
|
||||
if update {
|
||||
let input = String::from_utf8(input).unwrap();
|
||||
let output = format_sexp(&output);
|
||||
corrected_entries.push((name, input, output));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
|
@ -172,25 +201,138 @@ fn run_tests(
|
|||
}
|
||||
if actual == output {
|
||||
println!("✓ {}", Colour::Green.paint(&name));
|
||||
if update {
|
||||
let input = String::from_utf8(input).unwrap();
|
||||
let output = format_sexp(&output);
|
||||
corrected_entries.push((name, input, output));
|
||||
}
|
||||
} else {
|
||||
println!("✗ {}", Colour::Red.paint(&name));
|
||||
if update {
|
||||
let input = String::from_utf8(input).unwrap();
|
||||
let output = format_sexp(&actual);
|
||||
corrected_entries.push((name.clone(), input, output));
|
||||
println!("✓ {}", Colour::Blue.paint(&name));
|
||||
} else {
|
||||
println!("✗ {}", Colour::Red.paint(&name));
|
||||
}
|
||||
failures.push((name, actual, output));
|
||||
}
|
||||
}
|
||||
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 failure_count = failures.len();
|
||||
|
||||
indent_level += 1;
|
||||
for child in children {
|
||||
run_tests(parser, child, filter, indent_level, failures)?;
|
||||
run_tests(
|
||||
parser,
|
||||
child,
|
||||
filter,
|
||||
indent_level,
|
||||
failures,
|
||||
update,
|
||||
corrected_entries,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(file_path) = file_path {
|
||||
if update && failures.len() - failure_count > 0 {
|
||||
write_tests(&file_path, corrected_entries)?;
|
||||
}
|
||||
corrected_entries.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_sexp(sexp: &String) -> String {
|
||||
let mut formatted = String::new();
|
||||
|
||||
let mut indent_level = 0;
|
||||
let mut has_field = false;
|
||||
let mut s_iter = sexp.split(|c| c == ' ' || c == ')');
|
||||
while let Some(s) = s_iter.next() {
|
||||
if s.is_empty() {
|
||||
// ")"
|
||||
indent_level -= 1;
|
||||
write!(formatted, ")").unwrap();
|
||||
} else if s.starts_with('(') {
|
||||
if has_field {
|
||||
has_field = false;
|
||||
} else {
|
||||
if indent_level > 0 {
|
||||
writeln!(formatted, "").unwrap();
|
||||
for _ in 0..indent_level {
|
||||
write!(formatted, " ").unwrap();
|
||||
}
|
||||
}
|
||||
indent_level += 1;
|
||||
}
|
||||
|
||||
// "(node_name"
|
||||
write!(formatted, "{}", s).unwrap();
|
||||
|
||||
let mut c_iter = s.chars();
|
||||
c_iter.next();
|
||||
let second_char = c_iter.next().unwrap();
|
||||
if second_char == 'M' || second_char == 'U' {
|
||||
// "(MISSING node_name" or "(UNEXPECTED 'x'"
|
||||
let s = s_iter.next().unwrap();
|
||||
write!(formatted, " {}", s).unwrap();
|
||||
}
|
||||
} else if s.ends_with(':') {
|
||||
// "field:"
|
||||
writeln!(formatted, "").unwrap();
|
||||
for _ in 0..indent_level {
|
||||
write!(formatted, " ").unwrap();
|
||||
}
|
||||
write!(formatted, "{} ", s).unwrap();
|
||||
has_field = true;
|
||||
indent_level += 1;
|
||||
}
|
||||
}
|
||||
|
||||
formatted
|
||||
}
|
||||
|
||||
fn write_tests(file_path: &Path, corrected_entries: &Vec<(String, String, String)>) -> Result<()> {
|
||||
let mut buffer = fs::File::create(file_path)?;
|
||||
write_tests_to_buffer(&mut buffer, corrected_entries)
|
||||
}
|
||||
|
||||
fn write_tests_to_buffer(
|
||||
buffer: &mut impl Write,
|
||||
corrected_entries: &Vec<(String, String, String)>,
|
||||
) -> Result<()> {
|
||||
for (i, (name, input, output)) in corrected_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) -> io::Result<TestEntry> {
|
||||
let name = path
|
||||
.file_stem()
|
||||
|
|
@ -206,10 +348,14 @@ pub fn parse_tests(path: &Path) -> io::Result<TestEntry> {
|
|||
children.push(parse_tests(&entry.path())?);
|
||||
}
|
||||
}
|
||||
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())))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +363,7 @@ 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<PathBuf>) -> TestEntry {
|
||||
let mut children = Vec::new();
|
||||
let bytes = content.as_bytes();
|
||||
let mut prev_name = String::new();
|
||||
|
|
@ -268,7 +414,11 @@ fn parse_test_content(name: String, content: String) -> TestEntry {
|
|||
.to_string();
|
||||
prev_header_end = header_end;
|
||||
}
|
||||
TestEntry::Group { name, children }
|
||||
TestEntry::Group {
|
||||
name,
|
||||
children,
|
||||
file_path,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -300,6 +450,7 @@ d
|
|||
"#
|
||||
.trim()
|
||||
.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -319,7 +470,8 @@ d
|
|||
output: "(d)".to_string(),
|
||||
has_fields: false,
|
||||
},
|
||||
]
|
||||
],
|
||||
file_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -352,6 +504,7 @@ abc
|
|||
"#
|
||||
.trim()
|
||||
.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -371,8 +524,67 @@ abc
|
|||
output: "(c (d))".to_string(),
|
||||
has_fields: false,
|
||||
},
|
||||
]
|
||||
],
|
||||
file_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_sexp() {
|
||||
assert_eq!(
|
||||
format_sexp(&"(a b: (c) (d) e: (f (g (h (MISSING i)))))".to_string()),
|
||||
r#"
|
||||
(a
|
||||
b: (c)
|
||||
(d)
|
||||
e: (f
|
||||
(g
|
||||
(h
|
||||
(MISSING i)))))
|
||||
"#
|
||||
.trim()
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_tests_to_buffer() {
|
||||
let mut buffer = Vec::new();
|
||||
let corrected_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, &corrected_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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ fn flatten_tests(test: TestEntry) -> Vec<(String, Vec<u8>, 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);
|
||||
|
|
|
|||
|
|
@ -148,7 +148,8 @@ const h = `i ${j(k} l`
|
|||
(lexical_declaration
|
||||
(variable_declarator
|
||||
(identifier)
|
||||
(template_string (template_substitution (identifier) (ERROR)))))
|
||||
(template_string (template_substitution
|
||||
(augmented_assignment_expression (identifier) (MISSING identifier))))))
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
(identifier)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue