From 81504a7ee24f22820c6bc85823c879d488710d11 Mon Sep 17 00:00:00 2001 From: Nadrieril Date: Sun, 9 Feb 2020 17:39:01 +0000 Subject: Massively deduplicate test harness --- dhall/build.rs | 63 +++++--- dhall/src/lib.rs | 11 ++ dhall/src/semantics/tck/typecheck.rs | 5 +- dhall/src/tests.rs | 279 ++++++++++++++++++----------------- 4 files changed, 199 insertions(+), 159 deletions(-) diff --git a/dhall/build.rs b/dhall/build.rs index 7733581..3955b3a 100644 --- a/dhall/build.rs +++ b/dhall/build.rs @@ -2,15 +2,19 @@ use std::env; use std::ffi::OsString; use std::fs::{read_to_string, File}; use std::io::{BufRead, BufReader, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use walkdir::WalkDir; use abnf_to_pest::render_rules_to_pest; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FileType { + /// Dhall source file Text, + /// Dhall binary file Binary, + /// Text file with expected text output + UI, } impl FileType { @@ -18,8 +22,23 @@ impl FileType { match self { FileType::Text => "dhall", FileType::Binary => "dhallb", + FileType::UI => "txt", } } + fn constructor(self) -> &'static str { + match self { + FileType::Text => "TestFile::Source", + FileType::Binary => "TestFile::Binary", + FileType::UI => "TestFile::UI", + } + } + fn construct(self, path: &str) -> String { + // e.g. with + // path = "tests/foor/barA" + // returns something like: + // TestFile::Source("tests/foor/barA.dhall") + format!(r#"{}("{}.{}")"#, self.constructor(), path, self.to_ext()) + } } fn dhall_files_in_dir<'a>( @@ -72,7 +91,8 @@ fn make_test_module( mut feature: TestFeature, ) -> std::io::Result<()> { writeln!(w, "mod {} {{", feature.module_name)?; - let take_ab_suffix = feature.output_type.is_some(); + let take_ab_suffix = feature.output_type.is_some() + && feature.output_type != Some(FileType::UI); for base_path in base_paths { let tests_dir = base_path.join(feature.directory); for (name, path) in @@ -85,22 +105,26 @@ fn make_test_module( let path = path.to_string_lossy(); let test = match feature.output_type { None => { - let input_file = - format!("\"{}.{}\"", path, feature.input_type.to_ext()); - format!("{}({})", feature.variant, input_file) + let input = feature.input_type.construct(&path); + format!("{}({})", feature.variant, input) + } + Some(output_type @ FileType::UI) => { + let input = feature.input_type.construct(&path); + let output_file = PathBuf::from(path.as_ref()) + .strip_prefix(base_path) + .unwrap() + .strip_prefix(feature.directory) + .unwrap() + .to_string_lossy() + .into_owned(); + let output = output_type.construct(&output_file); + format!("{}({}, {})", feature.variant, input, output) } Some(output_type) => { - let input_file = format!( - "\"{}A.{}\"", - path, - feature.input_type.to_ext() - ); - let output_file = - format!("\"{}B.{}\"", path, output_type.to_ext()); - format!( - "{}({}, {})", - feature.variant, input_file, output_file - ) + let input = + feature.input_type.construct(&format!("{}A", path)); + let output = output_type.construct(&format!("{}B", path)); + format!("{}({}, {})", feature.variant, input, output) } }; writeln!(w, "make_spec_test!({}, {});", test, name)?; @@ -259,7 +283,7 @@ fn generate_tests() -> std::io::Result<()> { || path == "customHeadersUsingBoundVariable" }), input_type: FileType::Text, - output_type: None, + output_type: Some(FileType::UI), }, TestFeature { module_name: "beta_normalize", @@ -281,6 +305,7 @@ fn generate_tests() -> std::io::Result<()> { || path == "prelude/JSON/number/1" // TODO: doesn't typecheck || path == "unit/RightBiasedRecordMergeWithinRecordProjection" + || path == "unit/Sort" // // TODO: Further record simplifications || path == "simplifications/rightBiasedMergeWithinRecordProjectionWithinFieldSelection0" || path == "simplifications/rightBiasedMergeWithinRecordProjectionWithinFieldSelection1" @@ -342,7 +367,7 @@ fn generate_tests() -> std::io::Result<()> { || path == "unit/MergeHandlerFreeVar" }), input_type: FileType::Text, - output_type: None, + output_type: Some(FileType::UI), }, ]; diff --git a/dhall/src/lib.rs b/dhall/src/lib.rs index dbf1fc0..7725b28 100644 --- a/dhall/src/lib.rs +++ b/dhall/src/lib.rs @@ -234,6 +234,17 @@ impl std::hash::Hash for Normalized { } } +impl From for NormalizedExpr { + fn from(other: Parsed) -> Self { + other.to_expr() + } +} +impl From for NormalizedExpr { + fn from(other: Normalized) -> Self { + other.to_expr() + } +} + impl Eq for Typed {} impl PartialEq for Typed { fn eq(&self, other: &Self) -> bool { diff --git a/dhall/src/semantics/tck/typecheck.rs b/dhall/src/semantics/tck/typecheck.rs index 6817712..dd9a8fa 100644 --- a/dhall/src/semantics/tck/typecheck.rs +++ b/dhall/src/semantics/tck/typecheck.rs @@ -847,7 +847,10 @@ pub(crate) fn type_with( /// Typecheck an expression and return the expression annotated with types if type-checking /// succeeded, or an error if type-checking failed. pub(crate) fn typecheck(e: &Expr) -> Result { - type_with(&TyEnv::new(), e) + let res = type_with(&TyEnv::new(), e)?; + // Ensure that the inferred type exists (i.e. this is not Sort) + res.get_type()?; + Ok(res) } /// Like `typecheck`, but additionally checks that the expression's type matches the provided type. diff --git a/dhall/src/tests.rs b/dhall/src/tests.rs index 449e7ee..659317f 100644 --- a/dhall/src/tests.rs +++ b/dhall/src/tests.rs @@ -48,51 +48,124 @@ use std::io::{Read, Write}; use std::path::PathBuf; use crate::error::{Error, Result}; -use crate::Parsed; +use crate::syntax::binary; +use crate::{Normalized, NormalizedExpr, Parsed, Resolved}; #[allow(dead_code)] -#[derive(Clone)] -pub enum Test<'a> { - ParserSuccess(&'a str, &'a str), - ParserFailure(&'a str), - Printer(&'a str, &'a str), - BinaryEncoding(&'a str, &'a str), - BinaryDecodingSuccess(&'a str, &'a str), - BinaryDecodingFailure(&'a str), - ImportSuccess(&'a str, &'a str), - ImportFailure(&'a str), - ImportError(&'a str), - TypeInferenceSuccess(&'a str, &'a str), - TypeInferenceFailure(&'a str), - TypeError(&'a str), - Normalization(&'a str, &'a str), - AlphaNormalization(&'a str, &'a str), +enum Test { + ParserSuccess(TestFile, TestFile), + ParserFailure(TestFile), + Printer(TestFile, TestFile), + BinaryEncoding(TestFile, TestFile), + BinaryDecodingSuccess(TestFile, TestFile), + BinaryDecodingFailure(TestFile), + ImportSuccess(TestFile, TestFile), + ImportFailure(TestFile), + ImportError(TestFile, TestFile), + TypeInferenceSuccess(TestFile, TestFile), + TypeInferenceFailure(TestFile), + TypeError(TestFile, TestFile), + Normalization(TestFile, TestFile), + AlphaNormalization(TestFile, TestFile), } -fn parse_file_str(file_path: &str) -> Result { - Parsed::parse_file(&PathBuf::from(file_path)) +#[allow(dead_code)] +enum TestFile { + Source(&'static str), + Binary(&'static str), + UI(&'static str), +} + +impl TestFile { + pub fn path(&self) -> PathBuf { + match self { + TestFile::Source(path) + | TestFile::Binary(path) + | TestFile::UI(path) => PathBuf::from(path), + } + } + + /// Parse the target file + pub fn parse(&self) -> Result { + match self { + TestFile::Source(_) => Parsed::parse_file(&self.path()), + TestFile::Binary(_) => Parsed::parse_binary_file(&self.path()), + TestFile::UI(_) => panic!("Can't parse a UI test file"), + } + } + /// Parse and resolve the target file + pub fn resolve(&self) -> Result { + Ok(self.parse()?.resolve()?) + } + /// Parse, resolve, tck and normalize the target file + pub fn normalize(&self) -> Result { + Ok(self.resolve()?.typecheck()?.normalize()) + } + + /// Check that the provided expression matches the file contents. + pub fn compare(&self, expr: impl Into) -> Result<()> { + let expr = expr.into(); + let expected = self.parse()?.to_expr(); + assert_eq_display!(expr, expected); + Ok(()) + } + /// Check that the provided expression matches the file contents. + pub fn compare_debug(&self, expr: impl Into) -> Result<()> { + let expr = expr.into(); + let expected = self.parse()?.to_expr(); + assert_eq_pretty!(expr, expected); + Ok(()) + } + /// Check that the provided expression matches the file contents. + pub fn compare_binary( + &self, + expr: impl Into, + ) -> Result<()> { + match self { + TestFile::Binary(_) => {} + _ => panic!("This is not a binary file"), + } + let expr_data = binary::encode(&expr.into())?; + let expected_data = { + let mut data = Vec::new(); + File::open(&self.path())?.read_to_end(&mut data)?; + data + }; + + // Compare bit-by-bit + if expr_data != expected_data { + use serde_cbor::de::from_slice; + use serde_cbor::value::Value; + // use std::io::Write; + // File::create(&expected)?.write_all(&expr_data)?; + // Pretty-print difference + assert_eq_pretty!( + from_slice::(&expr_data).unwrap(), + from_slice::(&expected_data).unwrap() + ); + // If difference was not visible in the cbor::Value, compare normally. + assert_eq!(expr_data, expected_data); + } + Ok(()) + } } #[allow(dead_code)] -pub fn run_test_stringy_error( - test: Test<'_>, -) -> std::result::Result<(), String> { - run_test(test).map_err(|e| e.to_string()).map(|_| ()) +fn run_test_stringy_error(test: Test) -> std::result::Result<(), String> { + run_test(test).map_err(|e| e.to_string())?; + Ok(()) } -pub fn run_test(test: Test<'_>) -> Result<()> { +fn run_test(test: Test) -> Result<()> { use self::Test::*; match test { - ParserSuccess(expr_file_path, expected_file_path) => { - let expr = parse_file_str(&expr_file_path)?; + ParserSuccess(expr, expected) => { + let expr = expr.parse()?; // This exercices both parsing and binary decoding - // Compare parse/decoded - let expected = - Parsed::parse_binary_file(&PathBuf::from(expected_file_path))?; - assert_eq_pretty!(expr, expected); + expected.compare_debug(expr)?; } - ParserFailure(file_path) => { - let err = parse_file_str(&file_path).unwrap_err(); + ParserFailure(expr) => { + let err = expr.parse().unwrap_err(); match &err { Error::Parse(_) => {} Error::IO(e) if e.kind() == std::io::ErrorKind::InvalidData => { @@ -100,79 +173,39 @@ pub fn run_test(test: Test<'_>) -> Result<()> { e => panic!("Expected parse error, got: {:?}", e), } } - BinaryEncoding(expr_file_path, expected_file_path) => { - let expr = parse_file_str(&expr_file_path)?; - let mut expected_data = Vec::new(); - { - File::open(&PathBuf::from(&expected_file_path))? - .read_to_end(&mut expected_data)?; - } - let expr_data = expr.encode()?; - - // Compare bit-by-bit - if expr_data != expected_data { - // use std::io::Write; - // File::create(&expected_file_path)?.write_all(&expr_data)?; - // Pretty-print difference - assert_eq_pretty!( - serde_cbor::de::from_slice::( - &expr_data - ) - .unwrap(), - serde_cbor::de::from_slice::( - &expected_data - ) - .unwrap() - ); - // If difference was not visible in the cbor::Value - assert_eq!(expr_data, expected_data); - } + BinaryEncoding(expr, expected) => { + let expr = expr.parse()?; + expected.compare_binary(expr)?; } - BinaryDecodingSuccess(expr_file_path, expected_file_path) => { - let expr = - Parsed::parse_binary_file(&PathBuf::from(expr_file_path))?; - let expected = parse_file_str(&expected_file_path)?; - assert_eq_pretty!(expr, expected); + BinaryDecodingSuccess(expr, expected) => { + let expr = expr.parse()?; + expected.compare_debug(expr)?; } - BinaryDecodingFailure(file_path) => { - Parsed::parse_binary_file(&PathBuf::from(file_path)).unwrap_err(); + BinaryDecodingFailure(expr) => { + expr.parse().unwrap_err(); } - Printer(expr_file_path, _) => { - let expected = parse_file_str(&expr_file_path)?; + Printer(expr, _) => { + let expected = expr.parse()?; // Round-trip pretty-printer let expr: Parsed = Parsed::parse_str(&expected.to_string())?; assert_eq!(expr, expected); } - ImportSuccess(expr_file_path, expected_file_path) => { - let expr = parse_file_str(&expr_file_path)? - .resolve()? - .typecheck()? - .normalize(); - let expected = parse_file_str(&expected_file_path)? - .resolve()? - .typecheck()? - .normalize(); - - assert_eq_display!(expr, expected); + ImportSuccess(expr, expected) => { + let expr = expr.normalize()?; + expected.compare(expr)?; } - ImportFailure(file_path) => { - parse_file_str(&file_path)?.resolve().unwrap_err(); + ImportFailure(expr) => { + expr.parse()?.resolve().unwrap_err(); } // Checks the output of the type error against a text file. If the text file doesn't exist, // we instead write to it the output we got. This makes it easy to update those files: just // `rm -r dhall/tests/type-errors` and run the tests again. - ImportError(file_path) => { - let err: Error = - parse_file_str(&file_path)?.resolve().unwrap_err().into(); - let file_path = PathBuf::from(file_path); - let error_file_path = file_path - .strip_prefix("../dhall-lang/tests/import/failure/") - .or_else(|_| file_path.strip_prefix("tests/import/failure/")) - .unwrap(); + ImportError(expr, expected) => { + let base_path = expected.path(); let error_file_path = - PathBuf::from("tests/errors/import/").join(error_file_path); - let error_file_path = error_file_path.with_extension("txt"); + PathBuf::from("tests/errors/import/").join(base_path); + let err: Error = expr.parse()?.resolve().unwrap_err().into(); if error_file_path.is_file() { let expected_msg = std::fs::read_to_string(error_file_path)?; let msg = format!("{}\n", err); @@ -183,42 +216,22 @@ pub fn run_test(test: Test<'_>) -> Result<()> { writeln!(file, "{}", err)?; } } - TypeInferenceSuccess(expr_file_path, expected_file_path) => { - let expr = - parse_file_str(&expr_file_path)?.resolve()?.typecheck()?; - let ty = expr.get_type()?.to_expr(); - let expected = parse_file_str(&expected_file_path)?.to_expr(); - assert_eq_display!(ty, expected); + TypeInferenceSuccess(expr, expected) => { + let ty = expr.resolve()?.typecheck()?.get_type()?; + expected.compare(ty)?; } - TypeInferenceFailure(file_path) => { - let res = parse_file_str(&file_path)?.skip_resolve()?.typecheck(); - if let Ok(e) = &res { - // If e did typecheck, check that get_type fails - e.get_type().unwrap_err(); - } + TypeInferenceFailure(expr) => { + expr.resolve()?.typecheck().unwrap_err(); } // Checks the output of the type error against a text file. If the text file doesn't exist, // we instead write to it the output we got. This makes it easy to update those files: just // `rm -r dhall/tests/type-errors` and run the tests again. - TypeError(file_path) => { - let res = parse_file_str(&file_path)?.skip_resolve()?.typecheck(); - let file_path = PathBuf::from(file_path); - let error_file_path = file_path - .strip_prefix("../dhall-lang/tests/type-inference/failure/") - .or_else(|_| { - file_path.strip_prefix("tests/type-inference/failure/") - }) - .unwrap(); + TypeError(expr, expected) => { + let base_path = expected.path(); let error_file_path = - PathBuf::from("tests/type-errors/").join(error_file_path); - let error_file_path = error_file_path.with_extension("txt"); - let err: Error = match res { - Ok(e) => { - // If e did typecheck, check that get_type fails - e.get_type().unwrap_err().into() - } - Err(e) => e.into(), - }; + PathBuf::from("tests/type-errors/").join(base_path); + + let err: Error = expr.resolve()?.typecheck().unwrap_err().into(); if error_file_path.is_file() { let expected_msg = std::fs::read_to_string(error_file_path)?; @@ -230,25 +243,13 @@ pub fn run_test(test: Test<'_>) -> Result<()> { writeln!(file, "{}", err)?; } } - Normalization(expr_file_path, expected_file_path) => { - let expr = parse_file_str(&expr_file_path)? - .resolve()? - .typecheck()? - .normalize() - .to_expr(); - let expected = parse_file_str(&expected_file_path)?.to_expr(); - - assert_eq_display!(expr, expected); + Normalization(expr, expected) => { + let expr = expr.normalize()?; + expected.compare(expr)?; } - AlphaNormalization(expr_file_path, expected_file_path) => { - let expr = parse_file_str(&expr_file_path)? - .resolve()? - .typecheck()? - .normalize() - .to_expr_alpha(); - let expected = parse_file_str(&expected_file_path)?.to_expr(); - - assert_eq_display!(expr, expected); + AlphaNormalization(expr, expected) => { + let expr = expr.normalize()?.to_expr_alpha(); + expected.compare(expr)?; } } Ok(()) -- cgit v1.2.3