/ adl / cli / commands / test.rs
test.rs
  1  // Copyright (C) 2019-2025 Alpha-Delta Network Inc.
  2  // This file is part of the ADL library.
  3  
  4  // The ADL 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 ADL 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 ADL library. If not, see <https://www.gnu.org/licenses/>.
 16  
 17  use super::*;
 18  
 19  use adl_ast::{NetworkName, TEST_PRIVATE_KEY};
 20  use adl_compiler::run;
 21  use adl_package::{Package, ProgramData};
 22  use adl_span::Symbol;
 23  
 24  use alphavm::prelude::TestnetV0;
 25  
 26  use colored::Colorize as _;
 27  use std::fs;
 28  
 29  /// Test an ADL program.
 30  #[derive(Parser, Debug)]
 31  pub struct AdlTest {
 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  
 45  impl Command for AdlTest {
 46      type Input = <AdlBuild as Command>::Output;
 47      type Output = ();
 48  
 49      fn log_span(&self) -> Span {
 50          tracing::span!(tracing::Level::INFO, "Adl")
 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          (AdlBuild { 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  
 64  fn handle_test(command: AdlTest, package: Package) -> Result<()> {
 65      // Get the private key.
 66      let private_key = PrivateKey::<TestnetV0>::from_str(TEST_PRIVATE_KEY)?;
 67  
 68      let adl_paths = collect_adl_paths(&package);
 69      let alphastd_paths = collect_alphastd_paths(&package);
 70  
 71      let (native_test_functions, interpreter_result) = adl_interpreter::find_and_run_tests(
 72          &adl_paths,
 73          &alphastd_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(".alpha").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::Program> = package
 90          .programs
 91          .iter()
 92          .filter_map(|program| {
 93              // Skip credits.alpha 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 alphastd_path = if program.name == program_name_symbol {
102                          build_directory.join("main.alpha")
103                      } else {
104                          package.imports_directory().join(format!("{}.alpha", program.name))
105                      };
106                      fs::read_to_string(&alphastd_path)
107                          .unwrap_or_else(|e| panic!("Failed to read Alpha file at {}: {}", alphastd_path.display(), e))
108                  }
109              };
110              Some(run::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::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::Case {
120                  program_name: format!("{}.alpha", 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 = run::run_with_ledger(&run::Config { seed: 0, start_height: None, programs }, &cases)?
129          .into_iter()
130          .flatten()
131          .collect::<Vec<_>>();
132  
133      let native_results: Vec<_> = outcomes
134          .into_iter()
135          .zip(should_fails)
136          .map(|(outcome, should_fail)| {
137              let run::ExecutionOutcome { outcome: inner, status, .. } = outcome;
138  
139              let message = match (&status, should_fail) {
140                  (run::ExecutionStatus::Accepted, false) => None,
141                  (run::ExecutionStatus::Accepted, true) => Some("Test succeeded when failure was expected.".to_string()),
142                  (_, true) => None,
143                  (_, false) => Some(format!("{} -- {}", status, inner.output)),
144              };
145  
146              (inner.program_name, inner.function, message)
147          })
148          .collect::<Vec<_>>();
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  }