use super::*;
use leo_ast::{NetworkName, TEST_PRIVATE_KEY};
use leo_compiler::run;
use leo_package::{Package, ProgramData};
use leo_span::{Symbol, sym};
use snarkvm::prelude::TestnetV0;
use colored::Colorize as _;
use std::fs;
#[derive(Parser, Debug)]
pub struct LeoTest {
#[clap(
name = "TEST_NAME",
help = "If specified, run only tests whose qualified name matches against this string.",
default_value = ""
)]
pub(crate) test_name: String,
#[clap(long, help = "Run all tests with full proof generation.", default_value = "false")]
pub(crate) prove: bool,
#[clap(flatten)]
pub(crate) compiler_options: BuildOptions,
#[clap(flatten)]
pub(crate) env_override: EnvOptions,
}
impl Command for LeoTest {
type Input = <LeoBuild as Command>::Output;
type Output = TestOutput;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, context: Context) -> Result<Self::Input> {
let mut options = self.compiler_options.clone();
options.build_tests = true;
(LeoBuild { env_override: self.env_override.clone(), options, rename: None }).execute(context)
}
fn apply(self, _: Context, input: Self::Input) -> Result<Self::Output> {
handle_test(self, input)
}
fn execute(self, context: Context) -> Result<Self::Output> {
match context.resolve_targets()? {
Some((_, targets)) if targets.len() > 1 => {
let mut aggregate = TestOutput::default();
for target in &targets {
let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
println!("\n--- workspace member '{member_name}' ---");
let member_ctx = context.with_path(target.clone());
let mut opts = self.compiler_options.clone();
opts.build_tests = true;
let package = (LeoBuild { env_override: self.env_override.clone(), options: opts, rename: None })
.execute(member_ctx)?;
let member_test = LeoTest {
test_name: self.test_name.clone(),
prove: self.prove,
compiler_options: self.compiler_options.clone(),
env_override: self.env_override.clone(),
};
let result = handle_test(member_test, package)?;
aggregate.passed += result.passed;
aggregate.failed += result.failed;
aggregate.tests.extend(result.tests);
}
Ok(aggregate)
}
_ => {
let input = self.prelude(context.clone())?;
let span = self.log_span();
let span = span.enter();
let out = self.apply(context, input);
drop(span);
out
}
}
}
}
struct TestFunction {
file: String,
program: String,
function: String,
should_fail: bool,
private_key: Option<String>,
}
fn discover_test_functions(package: &Package, match_str: &str, network: NetworkName) -> Result<Vec<TestFunction>> {
use indexmap::IndexMap;
use leo_ast::NodeBuilder;
use leo_compiler::Compiler;
use leo_errors::Handler;
use std::rc::Rc;
let private_key_symbol = Symbol::intern("private_key");
let mut test_functions = Vec::new();
for unit in &package.compilation_units {
let ProgramData::SourcePath { directory, source } = &unit.data else {
continue;
};
let file = source.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
let handler = Handler::default();
let node_builder = Rc::new(NodeBuilder::default());
let mut compiler =
Compiler::new(None, unit.kind.is_test(), handler, node_builder, None, IndexMap::new(), network);
let ast = if unit.kind.is_test() {
compiler.parse_program_from_file(source)
} else {
compiler.parse_program_from_directory(source, directory.join("src"))
};
let ast = match ast {
Ok(ast) => ast,
Err(_) => continue,
};
for scope in ast.program_scopes.values() {
let program_name = scope.program_id.name.to_string();
for (_, function) in &scope.functions {
let has_test = function.annotations.iter().any(|a| a.identifier.name == sym::test);
if !has_test {
continue;
}
if !function.variant.is_entry() {
continue;
}
if !match_str.is_empty() {
let program_qualified = format!("{program_name}/{}", function.identifier);
let file_qualified = format!("{file}::{}", function.identifier);
if !program_qualified.contains(match_str) && !file_qualified.contains(match_str) {
continue;
}
}
let should_fail = function.annotations.iter().any(|a| a.identifier.name == sym::should_fail);
let private_key = function
.annotations
.iter()
.find(|a| a.identifier.name == sym::test)
.and_then(|a| a.map.get(&private_key_symbol).cloned());
test_functions.push(TestFunction {
file: file.clone(),
program: program_name.clone(),
function: function.identifier.to_string(),
should_fail,
private_key,
});
}
}
}
Ok(test_functions)
}
fn handle_test(command: LeoTest, package: Package) -> Result<TestOutput> {
if package.compilation_units.last().map(|p| p.kind.is_library()).unwrap_or(false) {
return Err(crate::errors::custom("`leo test` is not supported for library packages.").into());
}
let _private_key = PrivateKey::<TestnetV0>::from_str(TEST_PRIVATE_KEY)?;
let network = command.env_override.network.unwrap_or(NetworkName::TestnetV0);
let test_functions = discover_test_functions(&package, &command.test_name, network)?;
let credits = Symbol::intern("credits.aleo");
let programs: Vec<run::Program> = package
.compilation_units
.iter()
.filter_map(|unit| {
if unit.name == credits {
return None;
}
if unit.kind.is_library() {
return None;
}
let bytecode = match &unit.data {
ProgramData::Bytecode(c) => c.clone(),
ProgramData::SourcePath { .. } => {
let aleo_path = package.unit_bytecode_path(&unit.name.to_string());
fs::read_to_string(&aleo_path)
.unwrap_or_else(|e| panic!("Failed to read Aleo file at {}: {}", aleo_path.display(), e))
}
};
Some(run::Program { bytecode, name: unit.name.to_string() })
})
.collect();
let test_metadata: Vec<_> = test_functions
.iter()
.map(|tf| {
(tf.should_fail, format!("{}::{}", tf.file, tf.function), format!("{}.aleo/{}", tf.program, tf.function))
})
.collect();
let cases: Vec<Vec<run::Case>> = test_functions
.into_iter()
.map(|tf| {
vec![run::Case {
program_name: format!("{}.aleo", tf.program),
function: tf.function,
private_key: tf.private_key,
input: Vec::new(),
seed_mapping: Vec::new(),
}]
})
.collect();
let total = cases.len();
if total == 0 {
println!("No tests run.");
return Ok(TestOutput::default());
}
let plural = if total == 1 { "" } else { "s" };
println!();
println!("{} {total} test{plural}", gutter("Running").green().bold());
let mut tests = Vec::with_capacity(total);
let mut passed = 0usize;
let mut failures: Vec<(usize, String)> = Vec::new();
run::run_with_ledger(
&run::Config { seed: 0, start_height: None, programs, skip_proving: !command.prove },
&cases,
|index, outcomes| {
let outcome = &outcomes[0];
let (should_fail, display, qualified_name) = &test_metadata[index];
let message = match (&outcome.status, *should_fail) {
(run::ExecutionStatus::Accepted, false) => None,
(run::ExecutionStatus::Accepted, true) => Some("test succeeded when failure was expected".to_string()),
(_, true) => None,
(_, false) => Some(format!("{} -- {}", outcome.status, outcome.outcome.output)),
};
match message {
Some(err) => {
println!("{} {display}", gutter("FAIL").red().bold());
failures.push((index, err.clone()));
tests.push(TestResult { name: qualified_name.clone(), passed: false, error: Some(err) });
}
None => {
passed += 1;
println!("{} {display}", gutter("PASS").green().bold());
tests.push(TestResult { name: qualified_name.clone(), passed: true, error: None });
}
}
},
)?;
let failed = total - passed;
println!("{}", "─".repeat(24).dimmed());
let summary = gutter("Summary");
let summary = if failed == 0 { summary.green().bold() } else { summary.red().bold() };
println!("{summary} {total} test{plural} run: {passed} passed, {failed} failed");
for (index, err) in &failures {
let (_, display, _) = &test_metadata[*index];
println!("{} {display}\n{:>14}{}", gutter("FAIL").red().bold(), "", err.dimmed());
}
Ok(TestOutput { passed, failed, tests })
}
fn gutter(word: &str) -> String {
format!("{word:>12}")
}