leo_lang/cli/commands/
test.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18
19use leo_ast::{NetworkName, TEST_PRIVATE_KEY};
20use leo_compiler::run_with_ledger;
21use leo_package::{Package, ProgramData};
22use leo_span::Symbol;
23
24use snarkvm::prelude::TestnetV0;
25
26use colored::Colorize as _;
27use std::fs;
28
29/// Test a leo program.
30#[derive(Parser, Debug)]
31pub struct LeoTest {
32    #[clap(
33        name = "TEST_NAME",
34        help = "If specified, run only tests whose qualified name matches against this string.",
35        default_value = ""
36    )]
37    pub(crate) test_name: String,
38
39    #[clap(flatten)]
40    pub(crate) compiler_options: BuildOptions,
41    #[clap(flatten)]
42    pub(crate) env_override: EnvOptions,
43}
44
45impl Command for LeoTest {
46    type Input = <LeoBuild as Command>::Output;
47    type Output = ();
48
49    fn log_span(&self) -> Span {
50        tracing::span!(tracing::Level::INFO, "Leo")
51    }
52
53    fn prelude(&self, context: Context) -> Result<Self::Input> {
54        let mut options = self.compiler_options.clone();
55        options.build_tests = true;
56        (LeoBuild { env_override: self.env_override.clone(), options }).execute(context)
57    }
58
59    fn apply(self, _: Context, input: Self::Input) -> Result<Self::Output> {
60        handle_test(self, input)
61    }
62}
63
64fn handle_test(command: LeoTest, package: Package) -> Result<()> {
65    // Get the private key.
66    let private_key = PrivateKey::<TestnetV0>::from_str(TEST_PRIVATE_KEY)?;
67
68    let leo_paths = collect_leo_paths(&package);
69    let aleo_paths = collect_aleo_paths(&package);
70
71    let (native_test_functions, interpreter_result) = leo_interpreter::find_and_run_tests(
72        &leo_paths,
73        &aleo_paths,
74        private_key.to_string(),
75        0u32,
76        chrono::Utc::now().timestamp(),
77        &command.test_name,
78        NetworkName::TestnetV0,
79    )?;
80
81    // Now for native tests.
82    let program_name = package.manifest.program.strip_suffix(".aleo").unwrap();
83    let program_name_symbol = Symbol::intern(program_name);
84    let build_directory = package.build_directory();
85
86    let credits = Symbol::intern("credits");
87
88    // Get bytecode and name for all programs, either directly or from the filesystem if they were compiled.
89    let programs: Vec<run_with_ledger::Program> = package
90        .programs
91        .iter()
92        .filter_map(|program| {
93            // Skip credits.aleo so we don't try to deploy it again.
94            if program.name == credits {
95                return None;
96            }
97            let bytecode = match &program.data {
98                ProgramData::Bytecode(c) => c.clone(),
99                ProgramData::SourcePath { .. } => {
100                    // This was not a network dependency, so get its bytecode from the filesystem.
101                    let aleo_path = if program.name == program_name_symbol {
102                        build_directory.join("main.aleo")
103                    } else {
104                        package.imports_directory().join(format!("{}.aleo", program.name))
105                    };
106                    fs::read_to_string(&aleo_path)
107                        .unwrap_or_else(|e| panic!("Failed to read Aleo file at {}: {}", aleo_path.display(), e))
108                }
109            };
110            Some(run_with_ledger::Program { bytecode, name: program.name.to_string() })
111        })
112        .collect();
113
114    let should_fails: Vec<bool> = native_test_functions.iter().map(|test_function| test_function.should_fail).collect();
115    let cases: Vec<Vec<run_with_ledger::Case>> = native_test_functions
116        .into_iter()
117        .map(|test_function| {
118            // Note. We wrap each individual test in its own vector, so that they are run in insolation.
119            vec![run_with_ledger::Case {
120                program_name: format!("{}.aleo", test_function.program),
121                function: test_function.function,
122                private_key: test_function.private_key,
123                input: Vec::new(),
124            }]
125        })
126        .collect();
127
128    let outcomes =
129        run_with_ledger::run_with_ledger(&run_with_ledger::Config { seed: 0, start_height: None, programs }, &cases)?
130            .into_iter()
131            .flatten()
132            .collect::<Vec<_>>();
133
134    let native_results: Vec<_> = outcomes
135        .into_iter()
136        .zip(should_fails)
137        .map(|(outcome, should_fail)| {
138            let message = match (&outcome.status, should_fail) {
139                (run_with_ledger::Status::Accepted, false) => None,
140                (run_with_ledger::Status::Accepted, true) => {
141                    Some("Test succeeded when failure was expected.".to_string())
142                }
143                (_, true) => None,
144                (_, false) => Some(format!("{} -- {}", outcome.status, outcome.output)),
145            };
146            (outcome.program_name, outcome.function, message)
147        })
148        .collect();
149
150    // All tests are run. Report results.
151    let total = interpreter_result.iter().count() + native_results.len();
152    let total_passed = interpreter_result.iter().filter(|(_, test_result)| matches!(test_result, Ok(()))).count()
153        + native_results.iter().filter(|(_, _, x)| x.is_none()).count();
154
155    if total == 0 {
156        println!("No tests run.");
157        Ok(())
158    } else {
159        println!("{total_passed} / {total} tests passed.");
160        let failed = "FAILED".bold().red();
161        let passed = "PASSED".bold().green();
162        for (id, id_result) in interpreter_result.iter() {
163            // Wasteful to make this, but fill will work.
164            let str_id = format!("{id}");
165            if let Err(err) = id_result {
166                println!("{failed}: {str_id:<30} | {err}");
167            } else {
168                println!("{passed}: {str_id}");
169            }
170        }
171
172        for (program, function, case_result) in native_results {
173            let str_id = format!("{program}/{function}");
174            if let Some(err_str) = case_result {
175                println!("{failed}: {str_id:<30} | {err_str}");
176            } else {
177                println!("{passed}: {str_id}");
178            }
179        }
180
181        Ok(())
182    }
183}