1use super::*;
18
19use leo_ast::{NetworkName, TEST_PRIVATE_KEY};
20use leo_compiler::run;
21use leo_package::{Package, ProgramData};
22use leo_span::{Symbol, sym};
23
24use snarkvm::prelude::TestnetV0;
25
26use colored::Colorize as _;
27use std::fs;
28
29#[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(long, help = "Run all tests with full proof generation.", default_value = "false")]
40 pub(crate) prove: bool,
41
42 #[clap(flatten)]
43 pub(crate) compiler_options: BuildOptions,
44 #[clap(flatten)]
45 pub(crate) env_override: EnvOptions,
46}
47
48impl Command for LeoTest {
49 type Input = <LeoBuild as Command>::Output;
50 type Output = TestOutput;
51
52 fn log_span(&self) -> Span {
53 tracing::span!(tracing::Level::INFO, "Leo")
54 }
55
56 fn prelude(&self, context: Context) -> Result<Self::Input> {
57 let mut options = self.compiler_options.clone();
58 options.build_tests = true;
59 (LeoBuild { env_override: self.env_override.clone(), options, rename: None }).execute(context)
60 }
61
62 fn apply(self, _: Context, input: Self::Input) -> Result<Self::Output> {
63 handle_test(self, input)
64 }
65
66 fn execute(self, context: Context) -> Result<Self::Output> {
67 match context.resolve_targets()? {
71 Some((_, targets)) if targets.len() > 1 => {
72 let mut aggregate = TestOutput::default();
73 for target in &targets {
74 let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
75 println!("\n--- workspace member '{member_name}' ---");
76
77 let member_ctx = context.with_path(target.clone());
78
79 let mut opts = self.compiler_options.clone();
81 opts.build_tests = true;
82 let package = (LeoBuild { env_override: self.env_override.clone(), options: opts, rename: None })
83 .execute(member_ctx)?;
84
85 let member_test = LeoTest {
87 test_name: self.test_name.clone(),
88 prove: self.prove,
89 compiler_options: self.compiler_options.clone(),
90 env_override: self.env_override.clone(),
91 };
92 let result = handle_test(member_test, package)?;
93 aggregate.passed += result.passed;
94 aggregate.failed += result.failed;
95 aggregate.tests.extend(result.tests);
96 }
97 Ok(aggregate)
98 }
99 _ => {
100 let input = self.prelude(context.clone())?;
102 let span = self.log_span();
103 let span = span.enter();
104 let out = self.apply(context, input);
105 drop(span);
106 out
107 }
108 }
109 }
110}
111
112struct TestFunction {
113 file: String,
114 program: String,
115 function: String,
116 should_fail: bool,
117 private_key: Option<String>,
118}
119
120fn discover_test_functions(package: &Package, match_str: &str, network: NetworkName) -> Result<Vec<TestFunction>> {
125 use indexmap::IndexMap;
126 use leo_ast::NodeBuilder;
127 use leo_compiler::Compiler;
128 use leo_errors::Handler;
129 use std::rc::Rc;
130
131 let private_key_symbol = Symbol::intern("private_key");
132 let mut test_functions = Vec::new();
133
134 for unit in &package.compilation_units {
135 let ProgramData::SourcePath { directory, source } = &unit.data else {
136 continue;
137 };
138
139 let file = source.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
140
141 let handler = Handler::default();
142 let node_builder = Rc::new(NodeBuilder::default());
143
144 let mut compiler =
145 Compiler::new(None, unit.kind.is_test(), handler, node_builder, None, IndexMap::new(), network);
146
147 let ast = if unit.kind.is_test() {
150 compiler.parse_program_from_file(source)
151 } else {
152 compiler.parse_program_from_directory(source, directory.join("src"))
153 };
154 let ast = match ast {
155 Ok(ast) => ast,
156 Err(_) => continue,
157 };
158
159 for scope in ast.program_scopes.values() {
160 let program_name = scope.program_id.name.to_string();
161
162 for (_, function) in &scope.functions {
163 let has_test = function.annotations.iter().any(|a| a.identifier.name == sym::test);
164 if !has_test {
165 continue;
166 }
167
168 if !function.variant.is_entry() {
169 continue;
170 }
171
172 if !match_str.is_empty() {
173 let program_qualified = format!("{program_name}/{}", function.identifier);
174 let file_qualified = format!("{file}::{}", function.identifier);
175 if !program_qualified.contains(match_str) && !file_qualified.contains(match_str) {
176 continue;
177 }
178 }
179
180 let should_fail = function.annotations.iter().any(|a| a.identifier.name == sym::should_fail);
181
182 let private_key = function
183 .annotations
184 .iter()
185 .find(|a| a.identifier.name == sym::test)
186 .and_then(|a| a.map.get(&private_key_symbol).cloned());
187
188 test_functions.push(TestFunction {
189 file: file.clone(),
190 program: program_name.clone(),
191 function: function.identifier.to_string(),
192 should_fail,
193 private_key,
194 });
195 }
196 }
197 }
198
199 Ok(test_functions)
200}
201
202fn handle_test(command: LeoTest, package: Package) -> Result<TestOutput> {
203 if package.compilation_units.last().map(|p| p.kind.is_library()).unwrap_or(false) {
204 return Err(crate::errors::custom("`leo test` is not supported for library packages.").into());
205 }
206
207 let _private_key = PrivateKey::<TestnetV0>::from_str(TEST_PRIVATE_KEY)?;
209
210 let network = command.env_override.network.unwrap_or(NetworkName::TestnetV0);
211 let test_functions = discover_test_functions(&package, &command.test_name, network)?;
212
213 let credits = Symbol::intern("credits.aleo");
214
215 let programs: Vec<run::Program> = package
217 .compilation_units
218 .iter()
219 .filter_map(|unit| {
220 if unit.name == credits {
222 return None;
223 }
224 if unit.kind.is_library() {
226 return None;
227 }
228 let bytecode = match &unit.data {
229 ProgramData::Bytecode(c) => c.clone(),
230 ProgramData::SourcePath { .. } => {
231 let aleo_path = package.unit_bytecode_path(&unit.name.to_string());
233 fs::read_to_string(&aleo_path)
234 .unwrap_or_else(|e| panic!("Failed to read Aleo file at {}: {}", aleo_path.display(), e))
235 }
236 };
237 Some(run::Program { bytecode, name: unit.name.to_string() })
238 })
239 .collect();
240
241 let test_metadata: Vec<_> = test_functions
242 .iter()
243 .map(|tf| {
244 (tf.should_fail, format!("{}::{}", tf.file, tf.function), format!("{}.aleo/{}", tf.program, tf.function))
245 })
246 .collect();
247
248 let cases: Vec<Vec<run::Case>> = test_functions
249 .into_iter()
250 .map(|tf| {
251 vec![run::Case {
252 program_name: format!("{}.aleo", tf.program),
253 function: tf.function,
254 private_key: tf.private_key,
255 input: Vec::new(),
256 seed_mapping: Vec::new(),
257 }]
258 })
259 .collect();
260
261 let total = cases.len();
262 if total == 0 {
263 println!("No tests run.");
264 return Ok(TestOutput::default());
265 }
266
267 let plural = if total == 1 { "" } else { "s" };
268 println!();
269 println!("{} {total} test{plural}", gutter("Running").green().bold());
270
271 let mut tests = Vec::with_capacity(total);
272 let mut passed = 0usize;
273 let mut failures: Vec<(usize, String)> = Vec::new();
274
275 run::run_with_ledger(
277 &run::Config { seed: 0, start_height: None, programs, skip_proving: !command.prove },
278 &cases,
279 |index, outcomes| {
280 let outcome = &outcomes[0];
281 let (should_fail, display, qualified_name) = &test_metadata[index];
282
283 let message = match (&outcome.status, *should_fail) {
284 (run::ExecutionStatus::Accepted, false) => None,
285 (run::ExecutionStatus::Accepted, true) => Some("test succeeded when failure was expected".to_string()),
286 (_, true) => None,
287 (_, false) => Some(format!("{} -- {}", outcome.status, outcome.outcome.output)),
288 };
289
290 match message {
291 Some(err) => {
292 println!("{} {display}", gutter("FAIL").red().bold());
293 failures.push((index, err.clone()));
294 tests.push(TestResult { name: qualified_name.clone(), passed: false, error: Some(err) });
295 }
296 None => {
297 passed += 1;
298 println!("{} {display}", gutter("PASS").green().bold());
299 tests.push(TestResult { name: qualified_name.clone(), passed: true, error: None });
300 }
301 }
302 },
303 )?;
304
305 let failed = total - passed;
306 println!("{}", "─".repeat(24).dimmed());
307 let summary = gutter("Summary");
308 let summary = if failed == 0 { summary.green().bold() } else { summary.red().bold() };
309 println!("{summary} {total} test{plural} run: {passed} passed, {failed} failed");
310 for (index, err) in &failures {
311 let (_, display, _) = &test_metadata[*index];
312 println!("{} {display}\n{:>14}{}", gutter("FAIL").red().bold(), "", err.dimmed());
313 }
314
315 Ok(TestOutput { passed, failed, tests })
316}
317
318fn gutter(word: &str) -> String {
320 format!("{word:>12}")
321}