From 59915ed3154ee66e22f6648b873dfb0050aee50d Mon Sep 17 00:00:00 2001 From: d0cd Date: Fri, 3 Feb 2023 17:53:50 -0800 Subject: [PATCH] Initial implementation of Execute namespace; fmt --- compiler/compiler/tests/compile.rs | 1 - compiler/compiler/tests/execute.rs | 210 +++++++++++++++++++++++++++++ tests/execution/chain.leo | 29 ++++ tests/test-framework/src/error.rs | 6 + tests/test-framework/src/test.rs | 5 + 5 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 compiler/compiler/tests/execute.rs create mode 100644 tests/execution/chain.leo diff --git a/compiler/compiler/tests/compile.rs b/compiler/compiler/tests/compile.rs index 1078c76623..2ab7377806 100644 --- a/compiler/compiler/tests/compile.rs +++ b/compiler/compiler/tests/compile.rs @@ -51,7 +51,6 @@ impl Namespace for CompileNamespace { } } - #[derive(Deserialize, PartialEq, Eq, Serialize)] struct CompileOutput { pub initial_ast: String, diff --git a/compiler/compiler/tests/execute.rs b/compiler/compiler/tests/execute.rs new file mode 100644 index 0000000000..190b9d0592 --- /dev/null +++ b/compiler/compiler/tests/execute.rs @@ -0,0 +1,210 @@ +// Copyright (C) 2019-2022 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod utilities; +use utilities::{compile_and_process, hash_content, hash_file, parse_program, temp_dir, BufferEmitter}; + +use leo_errors::{emitter::Handler, LeoError}; +use leo_span::symbol::create_session_if_not_set_then; +use leo_test_framework::{ + runner::{Namespace, ParseType, Runner}, + Test, +}; + +use snarkvm::console; +use snarkvm::file::Manifest; +use snarkvm::package::Package; +use snarkvm::prelude::*; + +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; +use std::collections::BTreeMap; +use std::{fs, path::Path, rc::Rc}; +use std::{fs::File, io::Write}; + +type Network = Testnet3; +pub type Aleo = snarkvm::circuit::AleoV0; + +struct ExecuteNamespace; + +impl Namespace for ExecuteNamespace { + fn parse_type(&self) -> ParseType { + ParseType::Whole + } + + fn run_test(&self, test: Test) -> Result { + let buf = BufferEmitter(Rc::default(), Rc::default()); + let handler = Handler::new(Box::new(buf.clone())); + + create_session_if_not_set_then(|_| run_test(test, &handler, &buf).map_err(|()| buf.0.take().to_string())) + } +} + +// TODO: Format this better. +#[derive(Deserialize, PartialEq, Eq, Serialize)] +struct ExecuteOutput { + pub initial_ast: String, + pub unrolled_ast: String, + pub ssa_ast: String, + pub flattened_ast: String, + pub results: BTreeMap>, +} + +fn run_test(test: Test, handler: &Handler, _err_buf: &BufferEmitter) -> Result { + // Check for CWD option: + // ``` cwd: import ``` + // When set, uses different working directory for current file. + // If not, uses file path as current working directory. + let cwd = test.config.get("cwd").map(|val| { + let mut cwd = test.path.clone(); + cwd.pop(); + cwd.join(val.as_str().unwrap()) + }); + + // Parse the program. + let mut parsed = handler.extend_if_error(parse_program(handler, &test.content, cwd))?; + + // Compile the program to bytecode. + let program_name = format!("{}.{}", parsed.program_name, parsed.network); + let bytecode = handler.extend_if_error(compile_and_process(&mut parsed))?; + + // Extract the cases from the test config. + let all_cases = test + .config + .get("cases") + .expect("An `Execute` config must have a `cases` field.") + .as_mapping() + .unwrap(); + + // Initialize a map for the expected results. + let mut results = BTreeMap::new(); + + // Run snarkvm package. + { + // Initialize a temporary directory. + let directory = temp_dir(); + + // Create the program id. + let program_id = ProgramID::::from_str(&program_name).unwrap(); + + // Write the program string to a file in the temporary directory. + let path = directory.join("main.aleo"); + let mut file = File::create(path).unwrap(); + file.write_all(bytecode.as_bytes()).unwrap(); + + // Create the manifest file. + let manifest_file = Manifest::create(&directory, &program_id).unwrap(); + + // Create the build directory. + let build_directory = directory.join("build"); + std::fs::create_dir_all(build_directory).unwrap(); + + // Open the package at the temporary directory. + let package = handler.extend_if_error(Package::::open(&directory).map_err(LeoError::Anyhow))?; + + // Initialize an rng. + let rng = &mut rand::thread_rng(); + + // Run each test case for each function. + for (function_name, function_cases) in all_cases { + let function_name = Identifier::from_str(function_name.as_str().unwrap()).unwrap(); + let cases = function_cases.as_sequence().unwrap(); + let mut function_results = Vec::with_capacity(cases.len()); + + for case in cases { + let case = case.as_mapping().unwrap(); + let inputs: Vec<_> = case + .get(&Value::from("inputs")) + .unwrap() + .as_sequence() + .unwrap() + .iter() + .map(|input| console::program::Value::::from_str(input.as_str().unwrap()).unwrap()) + .collect(); + let inputs_hash = hash_content(&format!( + "[{}]", + inputs.iter().map(|input| input.to_string()).join(", ") + )); + + let outputs: Vec<_> = case + .get(&Value::from("expected")) + .unwrap() + .as_sequence() + .unwrap() + .iter() + .map(|output| console::program::Value::::from_str(output.as_str().unwrap()).unwrap()) + .collect(); + let outputs_hash = hash_content(&format!( + "[{}]", + outputs.iter().map(|output| output.to_string()).join(", ") + )); + + // Execute the program and get the outputs. + let (_response, _, _, _) = handler.extend_if_error( + package + .run::( + None, + manifest_file.development_private_key(), + function_name, + &inputs, + rng, + ) + .map_err(LeoError::Anyhow), + )?; + + // TODO: Check that outputs match + + // Add the hashes of the inputs and outputs to the function results. + function_results.push((inputs_hash, outputs_hash)); + } + results.insert(function_name.to_string(), function_results); + } + } + + let initial_ast = hash_file("/tmp/output/test.initial_ast.json"); + let unrolled_ast = hash_file("/tmp/output/test.unrolled_ast.json"); + let ssa_ast = hash_file("/tmp/output/test.ssa_ast.json"); + let flattened_ast = hash_file("/tmp/output/test.flattened_ast.json"); + + if fs::read_dir("/tmp/output").is_ok() { + fs::remove_dir_all(Path::new("/tmp/output")).expect("Error failed to clean up output dir."); + } + + let final_output = ExecuteOutput { + initial_ast, + unrolled_ast, + ssa_ast, + flattened_ast, + results, + }; + Ok(serde_yaml::to_value(&final_output).expect("serialization failed")) +} + +struct TestRunner; + +impl Runner for TestRunner { + fn resolve_namespace(&self, name: &str) -> Option> { + Some(match name { + "Execute" => Box::new(ExecuteNamespace), + _ => return None, + }) + } +} + +#[test] +pub fn execution_tests() { + leo_test_framework::run_tests(&TestRunner, "execution"); +} diff --git a/tests/execution/chain.leo b/tests/execution/chain.leo new file mode 100644 index 0000000000..40b922a331 --- /dev/null +++ b/tests/execution/chain.leo @@ -0,0 +1,29 @@ +/* +namespace: Execute +expectation: Match +cases: + main: + - inputs: ["1u32"] + expected: ["true"] + - inputs: ["2u32"] + expected: ["true"] + - inputs: ["3u32"] + expected: ["true"] + - inputs: ["4u32"] + expected: ["false"] +*/ + +program test.aleo { + transition main(x: u32) -> bool { + let c: u32 = 0u32; + + if x == 1u32 { + c = 1u32; + } else if x == 2u32 { + c = 2u32; + } else { + c = 3u32; + } + return c == x; + } +} diff --git a/tests/test-framework/src/error.rs b/tests/test-framework/src/error.rs index 81880e631c..1373e4e361 100644 --- a/tests/test-framework/src/error.rs +++ b/tests/test-framework/src/error.rs @@ -152,6 +152,9 @@ pub fn emit_errors( } None } + (Ok(Ok(output)), TestExpectationMode::Match) => { + todo!() + } (Ok(Ok(_tokens)), TestExpectationMode::Fail) => Some(TestError::PassedAndShouldntHave { test: test.to_string(), index: test_index, @@ -161,6 +164,9 @@ pub fn emit_errors( error: err.to_string(), index: test_index, }), + (Ok(Err(err)), TestExpectationMode::Match) => { + todo!() + } (Ok(Err(err)), TestExpectationMode::Fail) => { let expected_output: Option = expected_output.map(|x| serde_yaml::from_value(x).expect("test expectation deserialize failed")); diff --git a/tests/test-framework/src/test.rs b/tests/test-framework/src/test.rs index c965be41f5..19d43a0c7d 100644 --- a/tests/test-framework/src/test.rs +++ b/tests/test-framework/src/test.rs @@ -18,9 +18,14 @@ use std::collections::BTreeMap; #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)] pub enum TestExpectationMode { + /// The test should pass. Pass, + /// The test should fail. Fail, + /// The test should be skipped. Skip, + /// The test should pass and match the expected output. + Match, } #[derive(Debug, serde::Serialize, serde::Deserialize)]