/ interpreter / src / test_interpreter.rs
test_interpreter.rs
  1  // Copyright (C) 2019-2025 ADnet Contributors
  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  //! These tests compare interpreter runs against ledger runs.
 18  
 19  use adl_ast::{NetworkName, Stub, interpreter_value::Value};
 20  use adl_compiler::{Compiler, run};
 21  use adl_disassembler::disassemble_from_str;
 22  use adl_errors::{BufferEmitter, Handler, Result};
 23  use adl_span::{Symbol, create_session_if_not_set_then, source_map::FileName};
 24  
 25  use snarkvm::prelude::{PrivateKey, TestnetV0};
 26  
 27  use indexmap::IndexMap;
 28  use itertools::Itertools as _;
 29  use std::{
 30      fs,
 31      path::{Path, PathBuf},
 32      str::FromStr,
 33  };
 34  use walkdir::WalkDir;
 35  
 36  use crate::interpreter::{Interpreter, InterpreterAction};
 37  
 38  const PROGRAM_DELIMITER: &str = "// --- Next Program --- //";
 39  
 40  type CurrentNetwork = TestnetV0;
 41  
 42  fn whole_compile(source: &str, handler: &Handler, import_stubs: IndexMap<Symbol, Stub>) -> Result<(String, String)> {
 43      let mut compiler = Compiler::new(
 44          None,
 45          /* is_test (a Leo test) */ false,
 46          handler.clone(),
 47          "/fakedirectory-wont-use".into(),
 48          None,
 49          import_stubs,
 50          NetworkName::AlphaTestnetV0,
 51      );
 52  
 53      let filename = FileName::Custom("execution-test".into());
 54  
 55      let bytecode = compiler.compile(source, filename, &Vec::new())?;
 56  
 57      Ok((bytecode, compiler.program_name.unwrap()))
 58  }
 59  
 60  fn parse_cases(source: &str) -> (Vec<run::Case>, Vec<String>) {
 61      let mut cases: Vec<run::Case> = Vec::new();
 62  
 63      // Captures quote-delimited strings.
 64      let re_input = regex::Regex::new(r#""([^"]+)""#).unwrap();
 65  
 66      for line in source.lines() {
 67          if line.starts_with("[case]") {
 68              cases.push(Default::default());
 69          } else if let Some(rest) = line.strip_prefix("program = ") {
 70              cases.last_mut().unwrap().program_name = rest.trim_matches('"').into();
 71          } else if let Some(rest) = line.strip_prefix("function = ") {
 72              cases.last_mut().unwrap().function = rest.trim_matches('"').into();
 73          } else if let Some(rest) = line.strip_prefix("private_key = ") {
 74              cases.last_mut().unwrap().private_key = Some(rest.trim_matches('"').into());
 75          } else if let Some(rest) = line.strip_prefix("input = ") {
 76              // Get quote-delimited strings.
 77              cases.last_mut().unwrap().input = re_input.captures_iter(rest).map(|s| s[1].to_string()).collect();
 78          }
 79      }
 80  
 81      let sources = source.split(PROGRAM_DELIMITER).map(|s| s.trim().to_string()).collect();
 82      (cases, sources)
 83  }
 84  
 85  #[derive(Debug)]
 86  pub struct TestResult {
 87      ledger_result: Vec<Value>,
 88      interpreter_result: Vec<Value>,
 89  }
 90  
 91  fn run_test(path: &Path, handler: &Handler, _buf: &BufferEmitter) -> Result<TestResult, ()> {
 92      let source = fs::read_to_string(path).unwrap_or_else(|e| panic!("Failed to read file {}: {e}.", path.display()));
 93      let (cases, sources) = parse_cases(&source);
 94      let mut import_stubs = IndexMap::new();
 95      let mut ledger_config = run::Config { seed: 2, start_height: None, programs: Vec::new() };
 96  
 97      let mut requires_ledger = false;
 98      for source in &sources {
 99          let (bytecode, name) = handler.extend_if_error(whole_compile(source, handler, import_stubs.clone()))?;
100          requires_ledger = bytecode.contains("async");
101  
102          let stub = handler
103              .extend_if_error(disassemble_from_str::<CurrentNetwork>(&name, &bytecode).map_err(|err| err.into()))?;
104          import_stubs.insert(Symbol::intern(&name), stub);
105  
106          ledger_config.programs.push(run::Program { bytecode, name });
107      }
108  
109      // Extract only the outputs, ignoring status, execution, etc.
110      let outputs: Vec<Value> = if requires_ledger {
111          // Note. We wrap the cases in a slice to run them on a single ledger instance.
112          // This is just to be consistent with previous semantics.
113          handler
114          .extend_if_error(run::run_with_ledger(&ledger_config, std::slice::from_ref(&cases)))?
115          .into_iter()
116          .flatten()
117          .map(|exec| exec.outcome.output) // only extract the inner output
118          .collect()
119      } else {
120          handler
121          .extend_if_error(run::run_without_ledger(&ledger_config, &cases))?
122          .into_iter()
123          .map(|eval| eval.outcome.output) // only extract the inner output
124          .collect()
125      };
126  
127      let private_key = PrivateKey::<CurrentNetwork>::from_str(adl_ast::TEST_PRIVATE_KEY)
128          .expect("Should be able to parse private key.");
129  
130      let tempdir = tempfile::tempdir().expect("tempdir");
131      assert_eq!(sources.len(), 1, "For now we only support one program.");
132      let paths: Vec<PathBuf> = sources
133          .iter()
134          .map(|source| {
135              let path = tempdir.path().join("main.adl");
136              fs::write(&path, source).expect("write failed");
137              path
138          })
139          .collect();
140      let empty: [&PathBuf; 0] = [];
141      let mut interpreter = Interpreter::new(
142          &[(paths[0].clone(), Vec::new())],
143          empty,
144          private_key.to_string(),
145          0,
146          chrono::Utc::now().timestamp(),
147          NetworkName::AlphaTestnetV0,
148      )
149      .expect("creating interpreter");
150      let interpreter_result = handler.extend_if_error(
151          cases
152              .iter()
153              .map(|case| {
154                  interpreter
155                      .action(InterpreterAction::LeoInterpretOver(format!(
156                          "{}/{}({})",
157                          case.program_name,
158                          case.function,
159                          case.input.iter().format(", ")
160                      )))
161                      .map(|opt_value| opt_value.unwrap_or(Value::make_unit()))
162              })
163              .collect::<Result<Vec<_>>>(),
164      )?;
165      Ok(TestResult { ledger_result: outputs, interpreter_result })
166  }
167  
168  #[test]
169  fn test_interpreter() {
170      let tests_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "..", "tests", "tests", "interpreter"]
171          .iter()
172          .collect::<PathBuf>()
173          .canonicalize()
174          .unwrap();
175  
176      let filter_string = std::env::var("TEST_FILTER").unwrap_or_default();
177  
178      let paths: Vec<PathBuf> = WalkDir::new(&tests_dir)
179          .into_iter()
180          .flatten()
181          .filter_map(|entry| {
182              let path = entry.path();
183  
184              let path_str = path.to_str().unwrap_or_else(|| panic!("Path not unicode: {}.", path.display()));
185  
186              if !path_str.contains(&filter_string) || !path_str.ends_with(".adl") {
187                  return None;
188              }
189  
190              Some(path.into())
191          })
192          .collect();
193  
194      create_session_if_not_set_then(|_| {
195          for path in paths.iter() {
196              let mut test_result = {
197                  let buf = BufferEmitter::new();
198                  let handler = Handler::new(buf.clone());
199                  match run_test(path, &handler, &buf) {
200                      Ok(result) => result,
201                      Err(..) => {
202                          let errs = buf.extract_errs();
203                          panic!("{} {} ", errs.len(), errs);
204                      }
205                  }
206              };
207  
208              // Clear the `id`, for comparison against what snarkvm produced.
209              test_result.interpreter_result.iter_mut().for_each(|value| value.id = None);
210              if test_result.ledger_result != test_result.interpreter_result {
211                  println!("TEST {} Failed", path.display());
212                  println!("LEDGER: {:?}", test_result.ledger_result);
213                  println!("INTERPRETER: {:?}", test_result.interpreter_result);
214                  panic!("Test failure");
215              }
216          }
217      })
218  }