mod gas_report;
use crate::commands::ARTIFACTS_DIR;
use crate::utils::search::search_files;
use clap::builder::ValueHint;
use clap::ArgAction;
use colored::Colorize;
use kythera_lib::{
ApplyRet, ExecutionEvent, MethodType, TestResult, TestResultType, Tester, WasmActor,
};
use std::error::Error;
use std::{
path::PathBuf,
sync::mpsc::{sync_channel, Receiver, SyncSender},
thread,
};
use self::gas_report::GasReport;
#[derive(clap::Args, Debug)]
pub struct Args {
#[clap(
long,
default_value = ARTIFACTS_DIR,
value_hint = ValueHint::FilePath,
value_name = "DIR",
)]
path: PathBuf,
#[clap(long, short, verbatim_doc_comment, action = ArgAction::Count)]
pub verbosity: u8,
#[clap(long)]
gas_report: bool,
}
pub fn test(args: &Args) -> anyhow::Result<()> {
let test_targets = search_files(&args.path)?;
let mut gas_report = GasReport::default();
let mut tester = Tester::new();
for test_target in test_targets {
log::info!("\tRunning Tests for Actor : {}", test_target.actor.name());
let constructor = test_target.actor.abi().constructor().cloned();
match (
tester.deploy_target_actor(test_target.actor.clone()),
constructor,
) {
(Ok(Some(ret)), Some(constructor)) if args.gas_report => {
gas_report.analyze_method(
tester
.deployed_actor()
.expect("Deployed actor should be available"),
constructor,
ret.msg_receipt.gas_used,
);
}
(Err(err), _) => {
log::error!("\nError: {}", err);
if let Some(source) = err.source() {
log::error!("Caused by: {}", source)
}
continue;
}
_ => {}
}
let populated_tests = test_target
.tests
.iter()
.filter(|test| {
test.abi().methods().iter().any(|method| {
matches!(method.r#type(), MethodType::Test | MethodType::TestFail)
})
})
.collect::<Vec<&WasmActor>>();
log::info!("\t\tTesting {} test files\n", populated_tests.len());
for test in populated_tests {
let (sync_tx, sync_rx) = sync_channel(1);
let (stream_tx, stream_rx) = sync_channel(10);
let verbosity = args.verbosity;
thread::spawn(move || stream_results(stream_rx, sync_tx, verbosity));
match tester.test(test, Some(stream_tx)) {
Ok(results) => {
if args.gas_report {
let deployed = tester
.deployed_actor()
.expect("Deployed actor should be available");
gas_report.analyze_results(deployed, &results);
}
}
Err(err) => {
log::error!("\nError: {}", err);
if let Some(source) = err.source() {
log::error!("Caused by: {}", source)
}
}
};
sync_rx
.recv()
.expect("Should be able to sync the end of streaming results");
}
}
if args.gas_report {
log::info!("\nGas report");
for table in gas_report.finalize() {
log::info!("{table}");
}
}
Ok(())
}
fn stream_results(
stream: Receiver<(WasmActor, TestResult)>,
sync_tx: SyncSender<()>,
verbosity: u8,
) {
let mut tests_failed = vec![];
let mut tests_passed = vec![];
let mut result = "FAILED".bright_red();
for (_actor, test_result) in stream {
log::info!("{test_result}");
match test_result.ret() {
TestResultType::Passed(apply_ret) | TestResultType::Failed(apply_ret) => {
log::info!("(gas consumption: {})", apply_ret.msg_receipt.gas_used);
if verbosity == 3 {
print_verbose_traces(apply_ret);
}
if test_result.passed() {
tests_passed.push(test_result);
} else {
if verbosity == 2 {
print_verbose_traces(apply_ret);
}
tests_failed.push(test_result);
}
}
TestResultType::Erred(_) => {
tests_failed.push(test_result);
}
}
}
if !tests_passed.is_empty() {
result = "ok".green();
}
if !tests_failed.is_empty() {
result = "FAILED".bright_red();
log::error!("\nfailures:");
for f in tests_failed.iter() {
log::error!("test {}", f.method());
match (f.method().r#type(), f.ret()) {
(_, TestResultType::Erred(err)) => {
log::error!("\nError: {err}");
}
(MethodType::Test, TestResultType::Failed(apply_ret)) => {
let info = apply_ret
.failure_info
.as_ref()
.expect("Failure info should be available");
log::error!("failed: {info}");
}
(MethodType::TestFail, TestResultType::Failed(_)) => {
log::error!("failed: test exited with exit code 0");
}
(_, TestResultType::Passed(_)) => panic!("Test should have failed"),
_ => panic!("Failed tests should be of type test or test fail"),
}
}
}
log::info!(
"\ntest result: {result}. {} passed; {} failed\n",
tests_passed.len(),
tests_failed.len()
);
sync_tx
.send(())
.expect("Should be able to sync finish streaming results");
}
fn print_verbose_traces(apply_ret: &ApplyRet) {
for trace in apply_ret.exec_trace.iter() {
match trace {
ExecutionEvent::GasCharge(gas_charge) if gas_charge.name == "OnChainReturnValue" => {}
ExecutionEvent::GasCharge(gas_charge) => {
log::info!("├─ [<Gas Charge>] {}", gas_charge.name);
log::info!("│ └─ ← {}", gas_charge.compute_gas);
}
kythera_lib::ExecutionEvent::Call {
from, to, method, ..
} => {
log::info!("├─ [<Call>] from {from} to {to} method: {method}");
}
ExecutionEvent::CallReturn(exit_code, _) => {
log::info!("└─ ← {exit_code}");
}
ExecutionEvent::CallError(syscal_error) => {
log::info!("├─ [<Syscall Error>] {syscal_error}");
}
_ => {}
}
}
}