diff options
Diffstat (limited to '')
-rw-r--r-- | dhall/src/lib.rs | 11 | ||||
-rw-r--r-- | dhall/src/semantics/tck/typecheck.rs | 5 | ||||
-rw-r--r-- | dhall/src/tests.rs | 373 |
3 files changed, 238 insertions, 151 deletions
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<Parsed> for NormalizedExpr { + fn from(other: Parsed) -> Self { + other.to_expr() + } +} +impl From<Normalized> 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<Normalized>) -> Result<TyExpr, TypeError> { - 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 8136625..6a67ddc 100644 --- a/dhall/src/tests.rs +++ b/dhall/src/tests.rs @@ -43,186 +43,259 @@ macro_rules! assert_eq_pretty_str { }; } -use std::fs::File; +use std::env; +use std::fs::{create_dir_all, read_to_string, File}; 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), - 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, TestFile), + Printer(TestFile, TestFile), + BinaryEncoding(TestFile, TestFile), + BinaryDecodingSuccess(TestFile, TestFile), + BinaryDecodingFailure(TestFile, TestFile), + ImportSuccess(TestFile, TestFile), + ImportFailure(TestFile, TestFile), + TypeInferenceSuccess(TestFile, TestFile), + TypeInferenceFailure(TestFile, TestFile), + Normalization(TestFile, TestFile), + AlphaNormalization(TestFile, TestFile), } -fn parse_file_str(file_path: &str) -> Result<Parsed> { - 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<Parsed> { + 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<Resolved> { + Ok(self.parse()?.resolve()?) + } + /// Parse, resolve, tck and normalize the target file + pub fn normalize(&self) -> Result<Normalized> { + Ok(self.resolve()?.typecheck()?.normalize()) + } + + /// If UPDATE_TEST_FILES=1, we overwrite the output files with our own output. + fn force_update() -> bool { + env::var("UPDATE_TEST_FILES") == Ok("1".to_string()) + } + /// Write the provided expression to the pointed file. + fn write_expr(&self, expr: impl Into<NormalizedExpr>) -> Result<()> { + let expr = expr.into(); + let path = self.path(); + create_dir_all(path.parent().unwrap())?; + let mut file = File::create(path)?; + match self { + TestFile::Source(_) => { + writeln!(file, "{}", expr)?; + } + TestFile::Binary(_) => { + let expr_data = binary::encode(&expr)?; + file.write_all(&expr_data)?; + } + TestFile::UI(_) => panic!("Can't write an expression to a UI file"), + } + Ok(()) + } + /// Write the provided error to the pointed file. + fn write_ui(&self, err: impl Into<Error>) -> Result<()> { + match self { + TestFile::UI(_) => {} + _ => panic!("Can't write an error to a non-UI file"), + } + let err = err.into(); + let path = self.path(); + create_dir_all(path.parent().unwrap())?; + let mut file = File::create(path)?; + writeln!(file, "{}", err)?; + Ok(()) + } + + /// Check that the provided expression matches the file contents. + pub fn compare(&self, expr: impl Into<NormalizedExpr>) -> Result<()> { + let expr = expr.into(); + if !self.path().is_file() { + return self.write_expr(expr); + } + + let expected = self.parse()?.to_expr(); + if expr != expected { + if Self::force_update() { + self.write_expr(expr)?; + } else { + assert_eq_display!(expr, expected); + } + } + Ok(()) + } + /// Check that the provided expression matches the file contents. + pub fn compare_debug(&self, expr: impl Into<NormalizedExpr>) -> Result<()> { + let expr = expr.into(); + if !self.path().is_file() { + return self.write_expr(expr); + } + + let expected = self.parse()?.to_expr(); + if expr != expected { + if Self::force_update() { + self.write_expr(expr)?; + } else { + assert_eq_pretty!(expr, expected); + } + } + Ok(()) + } + /// Check that the provided expression matches the file contents. + pub fn compare_binary( + &self, + expr: impl Into<NormalizedExpr>, + ) -> Result<()> { + let expr = expr.into(); + match self { + TestFile::Binary(_) => {} + _ => panic!("This is not a binary file"), + } + if !self.path().is_file() { + return self.write_expr(expr); + } + + let expr_data = binary::encode(&expr)?; + 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 { + if Self::force_update() { + self.write_expr(expr)?; + } else { + use serde_cbor::de::from_slice; + use serde_cbor::value::Value; + // Pretty-print difference + assert_eq_pretty!( + from_slice::<Value>(&expr_data).unwrap(), + from_slice::<Value>(&expected_data).unwrap() + ); + // If difference was not visible in the cbor::Value, compare normally. + assert_eq!(expr_data, expected_data); + } + } + Ok(()) + } + /// Check that the provided error matches the file contents. Writes to the corresponding file + /// if it is missing. + pub fn compare_ui(&self, err: impl Into<Error>) -> Result<()> { + let err = err.into(); + if !self.path().is_file() { + return self.write_ui(err); + } + + let expected = read_to_string(self.path())?; + let msg = format!("{}\n", err); + if msg != expected { + if Self::force_update() { + self.write_ui(err)?; + } else { + assert_eq_pretty_str!(msg, expected); + } + } + 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, expected) => { + use std::io::ErrorKind; + let err = expr.parse().unwrap_err(); match &err { Error::Parse(_) => {} - Error::IO(e) if e.kind() == std::io::ErrorKind::InvalidData => { - } + Error::IO(e) if e.kind() == ErrorKind::InvalidData => {} e => panic!("Expected parse error, got: {:?}", e), } + expected.compare_ui(err)?; } - 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::<serde_cbor::value::Value>( - &expr_data - ) - .unwrap(), - serde_cbor::de::from_slice::<serde_cbor::value::Value>( - &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, expected) => { + let err = expr.parse().unwrap_err(); + expected.compare_ui(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())?; + let expr = 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); - } - ImportFailure(file_path) => { - parse_file_str(&file_path)?.resolve().unwrap_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); - } - 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(); - } + ImportSuccess(expr, expected) => { + let expr = expr.normalize()?; + expected.compare(expr)?; } - // 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(); - 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(), - }; - - if error_file_path.is_file() { - let expected_msg = std::fs::read_to_string(error_file_path)?; - let msg = format!("{}\n", err); - assert_eq_pretty_str!(msg, expected_msg); - } else { - std::fs::create_dir_all(error_file_path.parent().unwrap())?; - let mut file = File::create(error_file_path)?; - writeln!(file, "{}", err)?; - } + ImportFailure(expr, expected) => { + let err = expr.parse()?.resolve().unwrap_err(); + expected.compare_ui(err)?; + } + TypeInferenceSuccess(expr, expected) => { + let ty = expr.resolve()?.typecheck()?.get_type()?; + expected.compare(ty)?; + } + TypeInferenceFailure(expr, expected) => { + let err = expr.resolve()?.typecheck().unwrap_err(); + expected.compare_ui(err)?; + } + Normalization(expr, expected) => { + let expr = expr.normalize()?; + expected.compare(expr)?; } - 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); - } - 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(()) |