use std::path::Path;
use std::sync::Mutex;
use std::vec::IntoIter;
use anyhow::{bail, Context, Result};
use cairo_felt::Felt252;
use cairo_lang_compiler::db::RootDatabase;
use cairo_lang_compiler::diagnostics::DiagnosticsReporter;
use cairo_lang_compiler::project::setup_project;
use cairo_lang_filesystem::cfg::{Cfg, CfgSet};
use cairo_lang_filesystem::ids::CrateId;
use cairo_lang_runner::casm_run::format_next_item;
use cairo_lang_runner::profiling::{ProfilingInfo, ProfilingInfoProcessor};
use cairo_lang_runner::{RunResultValue, SierraCasmRunner};
use cairo_lang_sierra::extensions::gas::CostTokenType;
use cairo_lang_sierra::ids::FunctionId;
use cairo_lang_sierra::program::{Program, StatementIdx};
use cairo_lang_sierra_to_casm::metadata::MetadataComputationConfig;
use cairo_lang_starknet::contract::ContractInfo;
use cairo_lang_starknet::starknet_plugin_suite;
use cairo_lang_test_plugin::test_config::{PanicExpectation, TestExpectation};
use cairo_lang_test_plugin::{
compile_test_prepared_db, test_plugin_suite, TestCompilation, TestConfig,
};
use cairo_lang_utils::casts::IntoOrPanic;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
use colored::Colorize;
use itertools::Itertools;
use num_traits::ToPrimitive;
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
#[cfg(test)]
mod test;
pub struct TestRunner {
compiler: TestCompiler,
config: TestRunConfig,
}
impl TestRunner {
pub fn new(
path: &Path,
starknet: bool,
allow_warnings: bool,
config: TestRunConfig,
) -> Result<Self> {
let compiler = TestCompiler::try_new(path, starknet, allow_warnings)?;
Ok(Self { compiler, config })
}
pub fn run(&self) -> Result<Option<TestsSummary>> {
let runner = CompiledTestRunner::new(self.compiler.build()?, self.config.clone());
runner.run()
}
}
pub struct CompiledTestRunner {
pub compiled: TestCompilation,
pub config: TestRunConfig,
}
impl CompiledTestRunner {
pub fn new(compiled: TestCompilation, config: TestRunConfig) -> Self {
Self { compiled, config }
}
pub fn run(self) -> Result<Option<TestsSummary>> {
let (compiled, filtered_out) = filter_test_cases(
self.compiled,
self.config.include_ignored,
self.config.ignored,
self.config.filter,
);
let TestsSummary { passed, failed, ignored, failed_run_results } = run_tests(
compiled.named_tests,
compiled.sierra_program,
compiled.function_set_costs,
compiled.contracts_info,
self.config.run_profiler,
compiled.statements_functions,
)?;
if failed.is_empty() {
println!(
"test result: {}. {} passed; {} failed; {} ignored; {filtered_out} filtered out;",
"ok".bright_green(),
passed.len(),
failed.len(),
ignored.len()
);
Ok(None)
} else {
println!("failures:");
for (failure, run_result) in failed.iter().zip_eq(failed_run_results) {
print!(" {failure} - ");
match run_result {
RunResultValue::Success(_) => {
println!("expected panic but finished successfully.");
}
RunResultValue::Panic(values) => {
println!("{}", format_for_panic(values.into_iter()));
}
}
}
println!();
bail!(
"test result: {}. {} passed; {} failed; {} ignored",
"FAILED".bright_red(),
passed.len(),
failed.len(),
ignored.len()
);
}
}
}
fn format_for_panic(mut felts: IntoIter<Felt252>) -> String {
let mut items = Vec::new();
while let Some(item) = format_next_item(&mut felts) {
items.push(item.quote_if_string());
}
let panic_values_string =
if let [item] = &items[..] { item.clone() } else { format!("({})", items.join(", ")) };
format!("Panicked with {panic_values_string}.")
}
#[derive(Clone, Debug)]
pub struct TestRunConfig {
pub filter: String,
pub include_ignored: bool,
pub ignored: bool,
pub run_profiler: bool,
}
pub struct TestCompiler {
pub db: RootDatabase,
pub main_crate_ids: Vec<CrateId>,
pub test_crate_ids: Vec<CrateId>,
pub starknet: bool,
}
impl TestCompiler {
pub fn try_new(path: &Path, starknet: bool, allow_warnings: bool) -> Result<Self> {
let db = &mut {
let mut b = RootDatabase::builder();
b.detect_corelib();
b.with_cfg(CfgSet::from_iter([Cfg::name("test")]));
b.with_plugin_suite(test_plugin_suite());
if starknet {
b.with_plugin_suite(starknet_plugin_suite());
}
b.build()?
};
let main_crate_ids = setup_project(db, Path::new(&path))?;
let mut reporter = DiagnosticsReporter::stderr().with_crates(&main_crate_ids);
if allow_warnings {
reporter = reporter.allow_warnings();
}
if reporter.check(db) {
bail!("failed to compile: {}", path.display());
}
Ok(Self {
db: db.snapshot(),
test_crate_ids: main_crate_ids.clone(),
main_crate_ids,
starknet,
})
}
pub fn build(&self) -> Result<TestCompilation> {
compile_test_prepared_db(
&self.db,
self.starknet,
self.main_crate_ids.clone(),
self.test_crate_ids.clone(),
)
}
}
pub fn filter_test_cases(
compiled: TestCompilation,
include_ignored: bool,
ignored: bool,
filter: String,
) -> (TestCompilation, usize) {
let total_tests_count = compiled.named_tests.len();
let named_tests = compiled.named_tests
.into_iter()
.map(|(func, mut test)| {
if include_ignored {
test.ignored = false;
}
(func, test)
})
.filter(|(name, _)| name.contains(&filter))
.filter(|(_, test)| !ignored || test.ignored)
.collect_vec();
let filtered_out = total_tests_count - named_tests.len();
let tests = TestCompilation { named_tests, ..compiled };
(tests, filtered_out)
}
enum TestStatus {
Success,
Fail(RunResultValue),
}
struct TestResult {
status: TestStatus,
gas_usage: Option<i64>,
profiling_info: Option<ProfilingInfo>,
}
pub struct TestsSummary {
passed: Vec<String>,
failed: Vec<String>,
ignored: Vec<String>,
failed_run_results: Vec<RunResultValue>,
}
pub fn run_tests(
named_tests: Vec<(String, TestConfig)>,
sierra_program: Program,
function_set_costs: OrderedHashMap<FunctionId, OrderedHashMap<CostTokenType, i32>>,
contracts_info: OrderedHashMap<Felt252, ContractInfo>,
run_profiler: bool,
statements_functions: UnorderedHashMap<StatementIdx, String>,
) -> Result<TestsSummary> {
let runner = SierraCasmRunner::new(
sierra_program.clone(),
Some(MetadataComputationConfig {
function_set_costs,
linear_gas_solver: true,
linear_ap_change_solver: true,
}),
contracts_info,
run_profiler,
)
.with_context(|| "Failed setting up runner.")?;
println!("running {} tests", named_tests.len());
let wrapped_summary = Mutex::new(Ok(TestsSummary {
passed: vec![],
failed: vec![],
ignored: vec![],
failed_run_results: vec![],
}));
named_tests
.into_par_iter()
.map(|(name, test)| -> anyhow::Result<(String, Option<TestResult>)> {
if test.ignored {
return Ok((name, None));
}
let func = runner.find_function(name.as_str())?;
let result = runner
.run_function_with_starknet_context(
func,
&[],
test.available_gas,
Default::default(),
)
.with_context(|| format!("Failed to run the function `{}`.", name.as_str()))?;
Ok((
name,
Some(TestResult {
status: match &result.value {
RunResultValue::Success(_) => match test.expectation {
TestExpectation::Success => TestStatus::Success,
TestExpectation::Panics(_) => TestStatus::Fail(result.value),
},
RunResultValue::Panic(value) => match test.expectation {
TestExpectation::Success => TestStatus::Fail(result.value),
TestExpectation::Panics(panic_expectation) => match panic_expectation {
PanicExpectation::Exact(expected) if value != &expected => {
TestStatus::Fail(result.value)
}
_ => TestStatus::Success,
},
},
},
gas_usage: test
.available_gas
.zip(result.gas_counter)
.map(|(before, after)| {
before.into_or_panic::<i64>() - after.to_bigint().to_i64().unwrap()
})
.or_else(|| {
runner.initial_required_gas(func).map(|gas| gas.into_or_panic::<i64>())
}),
profiling_info: result.profiling_info,
}),
))
})
.for_each(|r| {
let mut wrapped_summary = wrapped_summary.lock().unwrap();
if wrapped_summary.is_err() {
return;
}
let (name, status) = match r {
Ok((name, status)) => (name, status),
Err(err) => {
*wrapped_summary = Err(err);
return;
}
};
let summary = wrapped_summary.as_mut().unwrap();
let (res_type, status_str, gas_usage, profiling_info) = match status {
Some(TestResult { status: TestStatus::Success, gas_usage, profiling_info }) => {
(&mut summary.passed, "ok".bright_green(), gas_usage, profiling_info)
}
Some(TestResult {
status: TestStatus::Fail(run_result),
gas_usage,
profiling_info,
}) => {
summary.failed_run_results.push(run_result);
(&mut summary.failed, "fail".bright_red(), gas_usage, profiling_info)
}
None => (&mut summary.ignored, "ignored".bright_yellow(), None, None),
};
if let Some(gas_usage) = gas_usage {
println!("test {name} ... {status_str} (gas usage est.: {gas_usage})");
} else {
println!("test {name} ... {status_str}");
}
if let Some(profiling_info) = profiling_info {
let profiling_processor = ProfilingInfoProcessor::new(
sierra_program.clone(),
statements_functions.clone(),
);
let processed_profiling_info = profiling_processor.process(&profiling_info);
println!("Profiling info:\n{processed_profiling_info}");
}
res_type.push(name);
});
wrapped_summary.into_inner().unwrap()
}