diff --git a/compiler/src/compiler.rs b/compiler/src/compiler.rs index 25afd5c4a5..db574f6db9 100644 --- a/compiler/src/compiler.rs +++ b/compiler/src/compiler.rs @@ -2,7 +2,7 @@ use crate::{ ast, - constraints::{generate_constraints, ConstrainedValue}, + constraints::{generate_constraints, generate_test_constraints, ConstrainedValue}, errors::CompilerError, GroupType, InputValue, Program, }; @@ -10,7 +10,7 @@ use crate::{ use snarkos_errors::gadgets::SynthesisError; use snarkos_models::{ curves::{Field, PrimeField}, - gadgets::r1cs::{ConstraintSynthesizer, ConstraintSystem}, + gadgets::r1cs::{ConstraintSynthesizer, ConstraintSystem, TestConstraintSystem}, }; use from_pest::FromPest; @@ -68,6 +68,13 @@ impl> Compiler { generate_constraints(cs, self.program, self.program_inputs) } + pub fn compile_test_constraints( + self, + cs: &mut TestConstraintSystem, + ) -> Result<(), CompilerError> { + generate_test_constraints::(cs, self.program) + } + // pub fn compile(&self) -> Result { // // Read in the main file as string // let unparsed_file = fs::read_to_string(&self.main_file_path).map_err(|_| CompilerError::FileReadError(self.main_file_path.clone()))?; diff --git a/compiler/src/constraints/mod.rs b/compiler/src/constraints/mod.rs index 2cafbc1c89..4b90c4e701 100644 --- a/compiler/src/constraints/mod.rs +++ b/compiler/src/constraints/mod.rs @@ -38,7 +38,7 @@ use crate::{ use snarkos_models::{ curves::{Field, PrimeField}, - gadgets::r1cs::ConstraintSystem, + gadgets::r1cs::{ConstraintSystem, TestConstraintSystem}, }; pub fn generate_constraints, CS: ConstraintSystem>( @@ -66,3 +66,40 @@ pub fn generate_constraints, CS: Constrai _ => Err(CompilerError::NoMainFunction), } } + +pub fn generate_test_constraints>( + cs: &mut TestConstraintSystem, + program: Program, +) -> Result<(), CompilerError> { + let mut resolved_program = ConstrainedProgram::>::new(); + let program_name = program.get_name(); + + let tests = program.tests.clone(); + + resolved_program.resolve_definitions(cs, program)?; + + log::info!("Running {} tests", tests.len()); + + for (test_name, test_function) in tests.into_iter() { + let full_test_name = format!("{}::{}", program_name.clone(), test_name.to_string()); + + let result = resolved_program.enforce_main_function( + cs, + program_name.clone(), + test_function.0, + vec![], // test functions should not take any inputs + ); + + if result.is_ok() { + log::info!( + "test {} passed. Constraint system satisfied: {}", + full_test_name, + cs.is_satisfied() + ); + } else { + log::error!("test {} errored: {}", full_test_name, result.unwrap_err()); + } + } + + Ok(()) +} diff --git a/compiler/src/constraints/statement.rs b/compiler/src/constraints/statement.rs index 069c32038a..1850d5bbdb 100644 --- a/compiler/src/constraints/statement.rs +++ b/compiler/src/constraints/statement.rs @@ -414,14 +414,16 @@ impl, CS: ConstraintSystem> Constraine self.enforce_boolean_eq(cs, bool_1, bool_2)? } (ConstrainedValue::Integer(num_1), ConstrainedValue::Integer(num_2)) => { - num_1.enforce_equal(cs, &num_2)? - } - (ConstrainedValue::Field(fe_1), ConstrainedValue::Field(fe_2)) => { - fe_1.enforce_equal(cs, &fe_2)? - } - (ConstrainedValue::Group(ge_1), ConstrainedValue::Group(ge_2)) => { - ge_1.enforce_equal(cs, &ge_2)? + num_1.enforce_equal(cs, &num_2).map_err(|_| { + StatementError::AssertionFailed(num_1.to_string(), num_2.to_string()) + })? } + (ConstrainedValue::Field(fe_1), ConstrainedValue::Field(fe_2)) => fe_1 + .enforce_equal(cs, &fe_2) + .map_err(|_| StatementError::AssertionFailed(fe_1.to_string(), fe_2.to_string()))?, + (ConstrainedValue::Group(ge_1), ConstrainedValue::Group(ge_2)) => ge_1 + .enforce_equal(cs, &ge_2) + .map_err(|_| StatementError::AssertionFailed(ge_1.to_string(), ge_2.to_string()))?, (ConstrainedValue::Array(arr_1), ConstrainedValue::Array(arr_2)) => { for (left, right) in arr_1.into_iter().zip(arr_2.into_iter()) { self.enforce_assert_eq_statement(cs, left, right)?; diff --git a/compiler/src/errors/constraints/statement.rs b/compiler/src/errors/constraints/statement.rs index 6af4c74fe9..60c25c6008 100644 --- a/compiler/src/errors/constraints/statement.rs +++ b/compiler/src/errors/constraints/statement.rs @@ -34,6 +34,9 @@ pub enum StatementError { #[error("Cannot assert equality between {} == {}", _0, _1)] AssertEq(String, String), + #[error("Assertion {:?} == {:?} failed", _0, _1)] + AssertionFailed(String, String), + #[error("If, else conditional must resolve to a boolean, got {}", _0)] IfElseConditional(String), diff --git a/compiler/src/field/mod.rs b/compiler/src/field/mod.rs index 33e3cb04a4..f0de4db1f5 100644 --- a/compiler/src/field/mod.rs +++ b/compiler/src/field/mod.rs @@ -287,3 +287,9 @@ impl ToBytesGadget for FieldType { self_gadget.to_bytes_strict(cs) } } + +impl std::fmt::Display for FieldType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self.get_value().ok_or(std::fmt::Error)) + } +} diff --git a/compiler/src/group/edwards_bls12.rs b/compiler/src/group/edwards_bls12.rs index 7cbdcff643..dc29192125 100644 --- a/compiler/src/group/edwards_bls12.rs +++ b/compiler/src/group/edwards_bls12.rs @@ -304,3 +304,12 @@ impl ToBytesGadget for EdwardsGroupType { self_gadget.to_bytes_strict(cs) } } + +impl std::fmt::Display for EdwardsGroupType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + EdwardsGroupType::Constant(constant) => write!(f, "{:?}", constant), + EdwardsGroupType::Allocated(allocated) => write!(f, "{:?}", allocated), + } + } +} diff --git a/compiler/src/group/mod.rs b/compiler/src/group/mod.rs index c29973b4dc..f81865aea2 100644 --- a/compiler/src/group/mod.rs +++ b/compiler/src/group/mod.rs @@ -14,7 +14,7 @@ use snarkos_models::{ }, }, }; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; pub mod edwards_bls12; @@ -22,6 +22,7 @@ pub trait GroupType: Sized + Clone + Debug + + Display + EqGadget + ConditionalEqGadget + AllocGadget diff --git a/leo/commands/mod.rs b/leo/commands/mod.rs index 3e56d37b47..14bc6c664d 100644 --- a/leo/commands/mod.rs +++ b/leo/commands/mod.rs @@ -25,5 +25,8 @@ pub use self::run::*; pub mod setup; pub use self::setup::*; +pub mod test; +pub use self::test::*; + pub mod unload; pub use self::unload::*; diff --git a/leo/commands/test.rs b/leo/commands/test.rs new file mode 100644 index 0000000000..10b07c93ef --- /dev/null +++ b/leo/commands/test.rs @@ -0,0 +1,74 @@ +use crate::directories::source::SOURCE_DIRECTORY_NAME; +use crate::errors::{CLIError, TestError}; +use crate::files::{MainFile, Manifest, MAIN_FILE_NAME}; +use crate::{cli::*, cli_types::*}; +use leo_compiler::compiler::Compiler; +use leo_compiler::group::edwards_bls12::EdwardsGroupType; + +use snarkos_curves::edwards_bls12::Fq; +use snarkos_models::gadgets::r1cs::TestConstraintSystem; + +use clap::ArgMatches; +use std::convert::TryFrom; +use std::env::current_dir; + +#[derive(Debug)] +pub struct TestCommand; + +impl CLI for TestCommand { + type Options = (); + type Output = (); + + const NAME: NameType = "test"; + const ABOUT: AboutType = "Compile and run all tests in the current package"; + const ARGUMENTS: &'static [ArgumentType] = &[]; + const FLAGS: &'static [FlagType] = &[]; + const OPTIONS: &'static [OptionType] = &[]; + const SUBCOMMANDS: &'static [SubCommandType] = &[]; + + #[cfg_attr(tarpaulin, skip)] + fn parse(_arguments: &ArgMatches) -> Result { + Ok(()) + } + + #[cfg_attr(tarpaulin, skip)] + fn output(_options: Self::Options) -> Result { + let path = current_dir()?; + + // Get the package name + let manifest = Manifest::try_from(&path)?; + let package_name = manifest.get_package_name(); + + // Sanitize the package path to the root directory + let mut package_path = path.clone(); + if package_path.is_file() { + package_path.pop(); + } + + // Verify the main file exists + if !MainFile::exists_at(&package_path) { + return Err( + TestError::MainFileDoesNotExist(package_path.as_os_str().to_owned()).into(), + ); + } + + // Construct the path to the main file in the source directory + let mut main_file_path = package_path.clone(); + main_file_path.push(SOURCE_DIRECTORY_NAME); + main_file_path.push(MAIN_FILE_NAME); + + // Compute the current program checksum + let program = + Compiler::::init(package_name.clone(), main_file_path.clone())?; + + // Generate the program on the constraint system and verify correctness + { + let mut cs = TestConstraintSystem::::new(); + let temporary_program = program.clone(); + let output = temporary_program.compile_test_constraints(&mut cs)?; + log::debug!("Compiled constraints - {:#?}", output); + } + + Ok(()) + } +} diff --git a/leo/errors/cli.rs b/leo/errors/cli.rs index f2ce79f2c1..007d2cffaf 100644 --- a/leo/errors/cli.rs +++ b/leo/errors/cli.rs @@ -44,6 +44,9 @@ pub enum CLIError { #[error("{}", _0)] SourceDirectoryError(#[from] SourceDirectoryError), + #[error("{}", _0)] + TestError(#[from] TestError), + #[error("{}", _0)] VerificationKeyFileError(#[from] VerificationKeyFileError), } diff --git a/leo/errors/commands/mod.rs b/leo/errors/commands/mod.rs index 968a4b6e24..896bd35f68 100644 --- a/leo/errors/commands/mod.rs +++ b/leo/errors/commands/mod.rs @@ -9,3 +9,6 @@ pub use self::new::*; pub mod run; pub use self::run::*; + +pub mod test; +pub use self::test::*; diff --git a/leo/errors/commands/test.rs b/leo/errors/commands/test.rs new file mode 100644 index 0000000000..6e7552c029 --- /dev/null +++ b/leo/errors/commands/test.rs @@ -0,0 +1,12 @@ +use crate::errors::ManifestError; + +use std::ffi::OsString; + +#[derive(Debug, Error)] +pub enum TestError { + #[error("main file {:?} does not exist", _0)] + MainFileDoesNotExist(OsString), + + #[error("{}", _0)] + ManifestError(#[from] ManifestError), +} diff --git a/leo/main.rs b/leo/main.rs index a9fc7f3c80..ac97d41056 100644 --- a/leo/main.rs +++ b/leo/main.rs @@ -28,6 +28,7 @@ fn main() -> Result<(), CLIError> { RunCommand::new().display_order(7), PublishCommand::new().display_order(8), DeployCommand::new().display_order(9), + TestCommand::new().display_order(10), ]) .set_term_width(0) .get_matches(); @@ -52,6 +53,10 @@ fn main() -> Result<(), CLIError> { ("run", Some(arguments)) => RunCommand::output(RunCommand::parse(arguments)?), ("publish", Some(arguments)) => PublishCommand::output(PublishCommand::parse(arguments)?), ("deploy", Some(arguments)) => DeployCommand::output(DeployCommand::parse(arguments)?), + ("test", Some(arguments)) => { + TestCommand::output(TestCommand::parse(arguments)?)?; + Ok(()) + } _ => unreachable!(), } }