use crate::cst::CstNode;
use crate::Error;
use crate::Lexer;
use crate::Parser;
use expect_test::expect_file;
use indexmap::IndexMap;
use std::env;
use std::fmt::Write;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn lexer_tests() {
dir_tests(&test_data_dir(), &["lexer/ok"], "txt", |text, path| {
let (dumped, errors) = dump_tokens_and_errors(text);
assert_errors_are_absent(&errors, path);
dumped
});
dir_tests(&test_data_dir(), &["lexer/err"], "txt", |text, path| {
let (dumped, errors) = dump_tokens_and_errors(text);
assert_errors_are_present(&errors, path);
dumped
});
}
#[test]
fn parser_tests() {
dir_tests(&test_data_dir(), &["parser/ok"], "txt", |text, path| {
let parser = Parser::new(text);
let cst = parser.parse();
assert_errors_are_absent(&cst.errors().cloned().collect::<Vec<_>>(), path);
assert_cst_spans_are_valid_utf8_boundaries(text, cst.document().syntax());
format!("{cst:?}")
});
dir_tests(&test_data_dir(), &["parser/err"], "txt", |text, path| {
let parser = Parser::new(text);
let cst = parser.parse();
assert_errors_are_present(&cst.errors().cloned().collect::<Vec<_>>(), path);
assert_cst_spans_are_valid_utf8_boundaries(text, cst.document().syntax());
format!("{cst:?}")
});
}
fn assert_errors_are_present(errors: &[Error], path: &Path) {
assert!(
!errors.is_empty(),
"There should be errors in the file {:?}",
path.display()
);
}
fn assert_errors_are_absent(errors: &[Error], path: &Path) {
if !errors.is_empty() {
println!(
"errors: {}",
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
);
panic!("There should be no errors in the file {:?}", path.display(),);
}
}
fn dump_tokens_and_errors(text: &str) -> (String, Vec<Error>) {
let mut acc = String::new();
let mut errors = Vec::new();
for result in Lexer::new(text) {
match result {
Ok(token) => writeln!(acc, "{token:?}").unwrap(),
Err(err) => {
writeln!(acc, "{err:?}").unwrap();
errors.push(err);
}
}
}
(acc, errors)
}
fn dir_tests<F>(test_data_dir: &Path, paths: &[&str], outfile_extension: &str, f: F)
where
F: Fn(&str, &Path) -> String,
{
for (path, input_code) in collect_graphql_files(test_data_dir, paths) {
let actual = f(&input_code, &path);
let path = path.with_extension(outfile_extension);
expect_file![path].assert_eq(&actual)
}
}
fn collect_graphql_files(root_dir: &Path, paths: &[&str]) -> Vec<(PathBuf, String)> {
paths
.iter()
.flat_map(|path| {
let path = root_dir.to_owned().join(path);
graphql_files_in_dir(&path).into_iter()
})
.map(|path| {
let text = fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("File at {path:?} should be valid"));
(path, text)
})
.collect()
}
fn graphql_files_in_dir(dir: &Path) -> Vec<PathBuf> {
let mut paths = fs::read_dir(dir)
.unwrap()
.map(|file| {
let file = file?;
let path = file.path();
if path.extension().unwrap_or_default() == "graphql" {
Ok(Some(path))
} else {
Ok(None)
}
})
.filter_map(|result: std::io::Result<_>| result.transpose())
.collect::<Result<Vec<_>, _>>()
.unwrap();
paths.sort();
let mut seen = IndexMap::new();
let next_number = paths.len() + 1;
for path in &paths {
let file_name = path.file_name().unwrap().to_string_lossy();
let (number, name): (usize, _) = match file_name.split_once('_') {
Some((number, name)) => match number.parse() {
Ok(number) => (number, name),
Err(err) => {
panic!("Invalid test file name: {path:?} does not start with a number ({err})")
}
},
None => panic!("Invalid test file name: {path:?} does not start with a number"),
};
if let Some(existing) = seen.get(&number) {
let suggest = dir.join(format!("{next_number:03}_{name}"));
panic!("Conflicting test file: {path:?} has the same number as {existing:?}. Suggested name: {suggest:?}");
}
seen.insert(number, path);
}
paths
}
fn test_data_dir() -> PathBuf {
project_root().join("apollo-parser/test_data")
}
fn project_root() -> PathBuf {
Path::new(
&env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()),
)
.ancestors()
.nth(1)
.unwrap()
.to_path_buf()
}
fn assert_cst_spans_are_valid_utf8_boundaries(input: &str, node: &crate::SyntaxNode) {
use rowan::NodeOrToken;
let range = node.text_range();
let start: usize = range.start().into();
let end: usize = range.end().into();
assert!(
input.is_char_boundary(start),
"Node {:?} start {} is not a char boundary",
node.kind(),
start
);
assert!(
input.is_char_boundary(end),
"Node {:?} end {} is not a char boundary",
node.kind(),
end
);
for child in node.children_with_tokens() {
match child {
NodeOrToken::Node(child_node) => {
assert_cst_spans_are_valid_utf8_boundaries(input, &child_node)
}
NodeOrToken::Token(token) => {
let range = token.text_range();
let start: usize = range.start().into();
let end: usize = range.end().into();
assert!(
input.is_char_boundary(start),
"Token {:?} start {} is not a char boundary",
token.kind(),
start
);
assert!(
input.is_char_boundary(end),
"Token {:?} end {} is not a char boundary",
token.kind(),
end
);
}
}
}
}