use crate::ast;
use crate::operation::ExecutableDocumentBuilder;
use crate::operation::FragmentRegistry;
use crate::operation::FragmentRegistryBuilder;
use crate::schema::Schema;
use crate::schema::SchemaBuilder;
use crate::test::snapshot_tests::utils;
use crate::test::snapshot_tests::ExpectedErrorPattern;
use crate::test::snapshot_tests::OperationSnapshotTestCase;
use rayon::prelude::IntoParallelRefIterator;
use rayon::prelude::ParallelIterator;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use super::snapshot_test_case::SchemaSnapshotTestCase;
const SCHEMA_ERROR_SNIPPET_LINES: usize = 3;
const OPERATION_ERROR_SNIPPET_LINES: usize = 5;
const EXPECTED_ERROR_PREVIEW_LINES: usize = 10;
fn format_expected_actual_error(expected: &str, actual: &str) -> String {
format!("Expected: {expected}\nGot: {actual}")
}
fn format_false_negative_error(what: &str, expected_patterns: &[ExpectedErrorPattern]) -> String {
let patterns_text = if expected_patterns.is_empty() {
String::new()
} else {
format!(
"\n\nExpected error patterns:\n{}",
expected_patterns
.iter()
.map(|p| format!(" - {p}"))
.collect::<Vec<_>>()
.join("\n")
)
};
format!("Expected: Should fail validation{patterns_text}\nGot: {what} validated successfully (false negative!)")
}
fn format_unmatched_patterns_error(unmatched: &[&ExpectedErrorPattern], actual_errors: &[String]) -> String {
format!(
"Expected: All error patterns must match\nGot: Not all expected errors matched\n\nUnmatched patterns:\n{}\n\nActual errors:\n{}",
unmatched
.iter()
.map(|p| format!(" ✗ {p}"))
.collect::<Vec<_>>()
.join("\n"),
actual_errors
.iter()
.map(|e| format!(" - {e}"))
.collect::<Vec<_>>()
.join("\n")
)
}
#[derive(Debug)]
pub struct SnapshotTestResult {
pub test_name: String,
pub passed: bool,
pub error_message: Option<String>,
pub file_path: PathBuf,
pub file_snippet: Option<String>,
}
#[derive(Debug)]
pub struct SnapshotTestResults {
pub results: Vec<SnapshotTestResult>,
}
impl SnapshotTestResults {
pub fn new() -> Self {
Self {
results: Vec::new(),
}
}
pub fn all_passed(&self) -> bool {
self.results.iter().all(|r| r.passed)
}
pub fn extend(&mut self, results: Vec<SnapshotTestResult>) {
self.results.extend(results);
}
pub fn failure_report(&self) -> String {
let failures: Vec<_> = self.results.iter().filter(|r| !r.passed).collect();
let failures_len = failures.len();
let results_len = self.results.len();
let failures_text = failures
.iter()
.map(|r| format_detailed_failure(r))
.collect::<Vec<_>>()
.join("\n\n");
format!("{failures_len} of {results_len} snapshot tests failed:\n\n{failures_text}")
}
pub fn summary(&self) -> String {
let all_passed = self.all_passed();
let emoji = if all_passed { "✅" } else { "❌" };
let banner = format!("{emoji} ========================================");
let header = format!("{emoji} GOLDEN TEST SUMMARY");
let total = self.results.len();
if all_passed {
format!("{banner}\n{header}\n{banner}\nTotal tests: {total}\nPassed: {total}\nFailed: 0\n\nAll snapshot tests passed!\n{banner}")
} else {
let failures: Vec<_> = self.results.iter().filter(|r| !r.passed).collect();
let failures_len = failures.len();
let passed = total - failures_len;
let failed_list = failures
.iter()
.map(|r| {
let test_name = &r.test_name;
let error = r.error_message.as_deref().unwrap_or("error");
format!(" - {test_name} ({error})")
})
.collect::<Vec<_>>()
.join("\n");
format!("{banner}\n{header}\n{banner}\nTotal tests: {total}\nPassed: {passed}\nFailed: {failures_len}\n\nFailed snapshot tests:\n{failed_list}\n\nSee details above for each failure.\n{banner}")
}
}
}
impl std::default::Default for SnapshotTestResults {
fn default() -> Self {
Self::new()
}
}
fn format_detailed_failure(result: &SnapshotTestResult) -> String {
let mut output = String::new();
let test_name = &result.test_name;
let file_path = result.file_path.display();
output.push_str(&format!("❌ {test_name}\n"));
output.push_str(&format!(" File: {file_path}\n"));
if let Some(msg) = &result.error_message {
output.push_str(&format!(" {msg}\n"));
}
if let Some(snippet) = &result.file_snippet {
output.push('\n');
output.push_str(snippet);
}
output
}
pub fn run_schema_tests(fixtures_dir: &Path) -> SnapshotTestResults {
let test_cases = SchemaSnapshotTestCase::discover_all(fixtures_dir);
let test_results: Vec<SnapshotTestResult> = test_cases
.par_iter()
.map(|test_case| {
if test_case.schema_expected_errors.is_empty() {
test_valid_schema(test_case)
} else {
test_invalid_schema(test_case)
}
})
.collect();
let mut results = SnapshotTestResults::new();
results.extend(test_results);
results
}
fn test_valid_schema(test_case: &SchemaSnapshotTestCase) -> SnapshotTestResult {
let name = &test_case.name;
let test_name = format!("{name}/schema");
let mut builder = SchemaBuilder::new();
for schema_path in &test_case.schema_paths {
builder = match builder.load_file(schema_path) {
Ok(b) => b,
Err(e) => {
return SnapshotTestResult {
test_name,
passed: false,
error_message: Some(format_expected_actual_error("Valid schema", &format!("{e:?}"))),
file_path: schema_path.clone(),
file_snippet: None,
};
}
};
}
match builder.build() {
Ok(_) => SnapshotTestResult {
test_name,
passed: true,
error_message: None,
file_path: test_case.schema_paths[0].clone(),
file_snippet: None,
},
Err(e) => {
let error_str = format!("{e:?}");
let file_path = test_case.schema_paths[0].clone();
let (snippet, snippet_error) = match extract_snippet_with_error_marker(&file_path, SCHEMA_ERROR_SNIPPET_LINES) {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(format!("Could not extract code snippet: {e}"))),
};
let mut error_message = format_expected_actual_error("Valid schema", &error_str);
if let Some(snip_err) = snippet_error {
error_message = format!("{error_message}\n\n{snip_err}");
}
SnapshotTestResult {
test_name,
passed: false,
error_message: Some(error_message),
file_path,
file_snippet: snippet,
}
}
}
}
fn test_invalid_schema(test_case: &SchemaSnapshotTestCase) -> SnapshotTestResult {
let name = &test_case.name;
let test_name = format!("{name}/schema");
let mut builder = SchemaBuilder::new();
for schema_path in &test_case.schema_paths {
builder = match builder.load_file(schema_path) {
Ok(b) => b,
Err(e) => {
let error_str = format!("{e:?}");
let errors = [error_str.clone()];
if !test_case.schema_expected_errors.is_empty() {
let all_match = test_case
.schema_expected_errors
.iter()
.all(|pattern| errors.iter().any(|e| utils::error_matches_pattern(e, pattern)));
if all_match {
return SnapshotTestResult {
test_name,
passed: true,
error_message: None,
file_path: schema_path.clone(),
file_snippet: None,
};
}
}
return SnapshotTestResult {
test_name,
passed: false,
error_message: Some(format!(
"Expected: Specific error patterns\nGot: Error occurred but didn't match expected patterns\n\nActual error:\n{error_str}"
)),
file_path: schema_path.clone(),
file_snippet: None,
};
}
};
}
match builder.build() {
Ok(_) => {
let file_path = test_case.schema_paths[0].clone();
let (snippet, snippet_error) = match create_missing_error_snippet(&file_path) {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(format!("Could not extract code snippet: {e}"))),
};
let mut error_message = format_false_negative_error("Schema", &test_case.schema_expected_errors);
if let Some(snip_err) = snippet_error {
error_message = format!("{error_message}\n\n{snip_err}");
}
SnapshotTestResult {
test_name,
passed: false,
error_message: Some(error_message),
file_path,
file_snippet: snippet,
}
}
Err(e) => {
let error_str = format!("{e:?}");
let errors = [error_str.clone()];
if test_case.schema_expected_errors.is_empty() {
return SnapshotTestResult {
test_name,
passed: true,
error_message: None,
file_path: test_case.schema_paths[0].clone(),
file_snippet: None,
};
}
let all_match = test_case
.schema_expected_errors
.iter()
.all(|pattern| errors.iter().any(|e| utils::error_matches_pattern(e, pattern)));
if all_match {
SnapshotTestResult {
test_name,
passed: true,
error_message: None,
file_path: test_case.schema_paths[0].clone(),
file_snippet: None,
}
} else {
let unmatched: Vec<_> = test_case
.schema_expected_errors
.iter()
.filter(|pattern| !errors.iter().any(|e| utils::error_matches_pattern(e, pattern)))
.collect();
let file_path = test_case.schema_paths[0].clone();
let (snippet, snippet_error) = match create_missing_error_snippet(&file_path) {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(format!("Could not extract code snippet: {e}"))),
};
let mut error_message = format!(
"{}\n\nActual error:\n{error_str}",
format_unmatched_patterns_error(&unmatched, &errors)
);
if let Some(snip_err) = snippet_error {
error_message = format!("{error_message}\n\n{snip_err}");
}
SnapshotTestResult {
test_name,
passed: false,
error_message: Some(error_message),
file_path,
file_snippet: snippet,
}
}
}
}
}
fn extract_snippet_with_error_marker(
file_path: &Path,
context_lines: usize,
) -> Result<String, std::io::Error> {
let content = fs::read_to_string(file_path)?;
let lines: Vec<&str> = content.lines().collect();
let start_line = 0;
let end_line = context_lines.min(lines.len());
let mut snippet = String::new();
let line_num_width = (end_line + 1).to_string().len();
for (idx, line) in lines[start_line..end_line].iter().enumerate() {
let line_num = start_line + idx + 1;
snippet.push_str(&format!(" {line_num:>line_num_width$} │ {line}\n"));
}
Ok(snippet)
}
fn create_missing_error_snippet(file_path: &Path) -> Result<String, std::io::Error> {
let content = fs::read_to_string(file_path)?;
let lines: Vec<&str> = content.lines().collect();
let mut snippet = String::new();
snippet.push_str(" Expected errors based on comments:\n");
for (idx, line) in lines.iter().enumerate().take(EXPECTED_ERROR_PREVIEW_LINES) {
let line_num = idx + 1;
let trimmed = line.trim_start();
if trimmed.starts_with("# EXPECTED_ERROR_TYPE:") || trimmed.starts_with("# EXPECTED_ERROR_CONTAINS:") {
snippet.push_str(&format!(" {line_num:>3} → {line} ⚠️ (error not raised!)\n"));
} else if idx < 5 {
snippet.push_str(&format!(" {line_num:>3} │ {line}\n"));
}
}
Ok(snippet)
}
pub fn run_operation_tests(fixtures_dir: &Path) -> SnapshotTestResults {
let test_cases = SchemaSnapshotTestCase::discover_all(fixtures_dir);
let test_results: Vec<SnapshotTestResult> = test_cases
.par_iter()
.filter(|test_case| test_case.schema_expected_errors.is_empty())
.filter_map(|test_case| {
let schema = try_build_schema(&test_case.schema_paths)?;
let fragment_registry = match build_suite_fragment_registry(&schema, &test_case.valid_operations) {
Ok(reg) => reg,
Err(err) => {
return Some(vec![SnapshotTestResult {
test_name: format!("{}/fragment_registry", test_case.name),
passed: false,
error_message: Some(format!("Failed to build suite fragment registry: {err}")),
file_path: test_case.schema_paths.first().cloned().unwrap_or_default(),
file_snippet: None,
}]);
}
};
let mut results = test_valid_operations(test_case, &schema, &fragment_registry);
let invalid_results = test_invalid_operations(test_case, &schema, &fragment_registry);
results.extend(invalid_results);
Some(results)
})
.flatten()
.collect();
let mut results = SnapshotTestResults::new();
results.extend(test_results);
results
}
fn try_build_schema(schema_paths: &[PathBuf]) -> Option<Schema> {
let mut builder = SchemaBuilder::new();
for schema_path in schema_paths {
builder = match builder.load_file(schema_path) {
Ok(b) => b,
Err(_) => return None,
};
}
builder.build().ok()
}
fn test_valid_operations(
test_case: &SchemaSnapshotTestCase,
schema: &Schema,
fragment_registry: &FragmentRegistry,
) -> Vec<SnapshotTestResult> {
let mut results = Vec::new();
if test_case.valid_operations.is_empty() {
return results;
}
for op_test in &test_case.valid_operations {
let test_name = format!(
"{}/valid_operations/{}",
test_case.name,
op_test.path.file_name().unwrap().to_str().unwrap()
);
let exec_doc_result =
ExecutableDocumentBuilder::from_file(schema, fragment_registry, &op_test.path);
match exec_doc_result {
Ok(_) => {
results.push(SnapshotTestResult {
test_name,
passed: true,
error_message: None,
file_path: op_test.path.clone(),
file_snippet: None,
});
}
Err(errors) => {
let error_str = format!("{errors:?}");
let (snippet, snippet_error) = match extract_snippet_with_error_marker(&op_test.path, OPERATION_ERROR_SNIPPET_LINES) {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(format!("Could not extract code snippet: {e}"))),
};
let mut error_message = format_expected_actual_error("Valid operation", &error_str);
if let Some(snip_err) = snippet_error {
error_message = format!("{error_message}\n\n{snip_err}");
}
results.push(SnapshotTestResult {
test_name,
passed: false,
error_message: Some(error_message),
file_path: op_test.path.clone(),
file_snippet: snippet,
});
}
}
}
results
}
fn test_invalid_operations(
test_case: &SchemaSnapshotTestCase,
schema: &Schema,
fragment_registry: &FragmentRegistry,
) -> Vec<SnapshotTestResult> {
let mut results = Vec::new();
if test_case.invalid_operations.is_empty() {
return results;
}
for op_test in &test_case.invalid_operations {
let test_name = format!(
"{}/invalid_operations/{}",
test_case.name,
op_test.path.file_name().unwrap().to_str().unwrap()
);
let exec_doc_result =
ExecutableDocumentBuilder::from_file(schema, fragment_registry, &op_test.path);
match exec_doc_result {
Ok(_) => {
let (snippet, snippet_error) = match create_missing_error_snippet(&op_test.path) {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(format!("Could not extract code snippet: {e}"))),
};
let mut error_message = format_false_negative_error("Operation", &op_test.expected_errors);
if let Some(snip_err) = snippet_error {
error_message = format!("{error_message}\n\n{snip_err}");
}
results.push(SnapshotTestResult {
test_name,
passed: false,
error_message: Some(error_message),
file_path: op_test.path.clone(),
file_snippet: snippet,
});
}
Err(errors) => {
let error_strs: Vec<String> = errors.iter().map(|e| format!("{e:?}")).collect();
if op_test.all_expected_errors_match(&error_strs) {
results.push(SnapshotTestResult {
test_name,
passed: true,
error_message: None,
file_path: op_test.path.clone(),
file_snippet: None,
});
} else {
let unmatched: Vec<_> = op_test
.expected_errors
.iter()
.filter(|pattern| !error_strs.iter().any(|e| utils::error_matches_pattern(e, pattern)))
.collect();
let (snippet, snippet_error) = match create_missing_error_snippet(&op_test.path) {
Ok(s) => (Some(s), None),
Err(e) => (None, Some(format!("Could not extract code snippet: {e}"))),
};
let mut error_message = format!(
"{}\n\nActual errors:\n{}",
format_unmatched_patterns_error(&unmatched, &error_strs),
error_strs.iter().map(|e| format!(" - {e}")).collect::<Vec<_>>().join("\n")
);
if let Some(snip_err) = snippet_error {
error_message = format!("{error_message}\n\n{snip_err}");
}
results.push(SnapshotTestResult {
test_name,
passed: false,
error_message: Some(error_message),
file_path: op_test.path.clone(),
file_snippet: snippet,
});
}
}
}
}
results
}
fn build_suite_fragment_registry<'schema>(
schema: &'schema Schema,
valid_operations: &[OperationSnapshotTestCase],
) -> Result<FragmentRegistry<'schema>, String> {
let mut registry_builder = FragmentRegistryBuilder::new();
for op_test in valid_operations {
let content = fs::read_to_string(&op_test.path)
.map_err(|e| format!("Failed to read file {}: {}", op_test.path.display(), e))?;
let ast_doc = graphql_parser::query::parse_query::<String>(&content)
.map_err(|e| format!("Failed to parse GraphQL in {}: {}", op_test.path.display(), e))?
.into_static();
registry_builder
.add_from_document_ast(
schema,
&ast::operation::Document::from(ast_doc),
Some(&op_test.path),
)
.map_err(|e| {
format!(
"Failed to add fragments from {}: {:?}",
op_test.path.display(),
e
)
})?;
}
registry_builder
.build()
.map_err(|e| format!("Failed to build suite fragment registry: {e:?}"))
}