use apollo_compiler::ast;
use apollo_compiler::collections::IndexMap;
use apollo_compiler::name;
use apollo_compiler::parser::FileId;
use apollo_compiler::parser::Parser;
use apollo_compiler::schema;
use apollo_compiler::ty;
use apollo_compiler::validation::DiagnosticList;
use apollo_compiler::Schema;
use expect_test::expect_file;
use serial_test::serial;
use std::env;
use std::fmt::Write;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
#[test]
#[serial]
fn validation() {
dir_tests(&test_data_dir(), &["ok"], "txt", |text, path| {
let file_name = path.file_name().unwrap();
match Parser::new().parse_mixed_validate(text, file_name) {
Err(errors) => {
println!("{errors}");
panic!(
"There should be no diagnostics in the file {}",
path.display(),
);
}
Ok((schema, executable)) => {
format!("{:#?}\n{:#?}", schema.into_inner(), executable.into_inner())
}
}
});
dir_tests(&test_data_dir(), &["diagnostics"], "txt", |text, path| {
let filename = path.file_name().unwrap().to_str().unwrap();
let is_type_system = filename.contains("type_system_document");
let is_executable = filename.contains("executable_document");
let schema_validation_errors;
let executable_validation_errors;
if is_type_system {
schema_validation_errors = Schema::parse_and_validate(text, filename)
.err()
.map(|e| e.errors);
executable_validation_errors = None;
} else if is_executable {
schema_validation_errors = None;
executable_validation_errors = match ast::Document::parse(text, filename) {
Err(e) => Some(e.errors),
Ok(ast) => ast.validate_standalone_executable().err(),
};
} else {
schema_validation_errors = Parser::new().parse_mixed_validate(text, filename).err();
executable_validation_errors = None;
};
let mut formatted = String::new();
if let Some(errors) = &schema_validation_errors {
write!(&mut formatted, "{errors:#}").unwrap()
}
if let Some(errors) = &executable_validation_errors {
write!(&mut formatted, "{errors:#}").unwrap()
}
assert_diagnostics_are_present(
&schema_validation_errors,
&executable_validation_errors,
path,
);
formatted
});
}
fn assert_diagnostics_are_present(
schema_validation_errors: &Option<DiagnosticList>,
executable_validation_errors: &Option<DiagnosticList>,
path: &Path,
) {
assert!(
schema_validation_errors.is_some() || executable_validation_errors.is_some(),
"There should be diagnostics in the file {:?}",
path.display()
);
}
#[test]
#[serial]
fn serialize_and_reparse_ast() {
FileId::reset();
let test_data_dir = test_data_dir();
for subdir in ["ok", "diagnostics"] {
let output_dir = test_data_dir.join("serializer").join(subdir);
let collected = collect_graphql_files(&test_data_dir, &[subdir]);
for (input_path, input) in collected {
let output_path = output_dir.join(input_path.file_name().unwrap());
let original = ast::Document::parse(&input, "input.graphql")
.unwrap_or_else(|invalid| invalid.partial);
let serialized = original.to_string();
expect_file![output_path].assert_eq(&serialized);
let round_tripped = ast::Document::parse(&serialized, "serialized.graphql")
.unwrap_or_else(|invalid| invalid.partial);
if original != round_tripped {
panic!(
"Serialization does not round-trip for {input_path:?}:\n\
{input}\n=>\n{original:#?}\n=>\n{serialized}\n=>\n\
{round_tripped:#?}\n=>\n{round_tripped}\n"
);
}
}
}
}
fn dir_tests<F>(test_data_dir: &Path, paths: &[&str], outfile_extension: &str, f: F)
where
F: Fn(&str, &Path) -> String,
{
FileId::reset();
for (path, input_code) in collect_graphql_files(test_data_dir, paths) {
let mut actual = f(&input_code, &path);
actual.push('\n');
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)> {
let mut files = 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::<Vec<_>>();
files.sort_by(|a, b| a.0.cmp(&b.0));
files
}
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::with_hasher(Default::default());
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-compiler/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()
}
#[test]
fn test_invalid_synthetic_node() {
let mut schema = Schema::new();
schema.types.insert(
name!("Obj"),
schema::ObjectType {
description: Default::default(),
name: name!("Obj"),
implements_interfaces: Default::default(),
directives: Default::default(),
fields: [(
name!("field"),
schema::FieldDefinition {
description: Default::default(),
name: name!("field"),
arguments: Default::default(),
ty: ty!(UndefinedType),
directives: Default::default(),
}
.into(),
)]
.into_iter()
.collect(),
}
.into(),
);
schema.schema_definition.make_mut().query = Some(name!("Obj").into());
let expected = expect_test::expect![[r#"
Error: cannot find type `UndefinedType` in this document
"#]];
expected.assert_eq(&schema.validate().unwrap_err().errors.to_string());
}