air-test-utils 0.18.3

Test utils for the AIR interpreter
/*
 * AquaVM Workflow Engine
 *
 * Copyright (C) 2024 Fluence DAO
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
#![deny(
    dead_code,
    nonstandard_style,
    unused_imports,
    unused_mut,
    unused_variables,
    unused_unsafe,
    unreachable_patterns
)]

pub mod call_services;
pub mod executed_state;
pub mod key_utils;
pub mod test_runner;

pub mod native_test_runner;
pub mod wasm_test_runner;

pub use air::interpreter_data::*;

use air::ExecutionCidState;
pub use avm_interface::raw_outcome::*;
pub use avm_server::*;

use futures::future::LocalBoxFuture;

pub mod prelude {
    pub use super::*;
    pub use call_services::*;
    pub use executed_state::*;
    pub use test_runner::*;

    pub use air_interpreter_value::JValue;
    pub use serde_json::json;
}

pub type CallServiceClosure<'x> =
    Box<dyn Fn(CallRequestParams) -> LocalBoxFuture<'x, CallServiceResult> + 'static>;

use air_interpreter_value::JValue;

#[macro_export]
macro_rules! checked_call_vm {
    ($vm:expr, $test_run_parameters:expr, $script:expr, $prev_data:expr, $data:expr) => {{
        match $vm
            .call($script, $prev_data, $data, $test_run_parameters)
            .await
        {
            Ok(v) if v.ret_code != 0 => {
                panic!("VM returns a error: {} {}", v.ret_code, v.error_message)
            }
            Ok(v) => v,
            Err(err) => panic!("VM call failed: {}", err),
        }
    }};
}

#[macro_export]
macro_rules! call_vm {
    ($vm:expr, $test_run_parameters:expr, $script:expr, $prev_data:expr, $data:expr) => {
        match $vm
            .call($script, $prev_data, $data, $test_run_parameters)
            .await
        {
            Ok(v) => v,
            Err(err) => panic!("VM call failed: {}", err),
        }
    };
}

pub fn trace_from_result(result: &RawAVMOutcome) -> ExecutionTrace {
    let data = data_from_result(result);
    data.trace
}

pub fn data_from_result(result: &RawAVMOutcome) -> InterpreterData {
    let env = InterpreterDataEnvelope::try_from_slice(&result.data)
        .expect("default serializer shouldn't fail");
    InterpreterData::try_from_slice(&env.inner_data).expect("default serializer shouldn't fail")
}

pub fn env_from_result(result: &RawAVMOutcome) -> InterpreterDataEnvelope<'_> {
    InterpreterDataEnvelope::try_from_slice(&result.data)
        .expect("default serializer shouldn't fail")
}

pub fn raw_data_from_trace(
    trace: impl Into<ExecutionTrace>,
    cid_state: ExecutionCidState,
) -> Vec<u8> {
    let data = InterpreterDataEnvelope::from_execution_result(
        trace.into(),
        cid_state.into(),
        <_>::default(),
        0,
        semver::Version::new(1, 1, 1),
    );
    data.serialize().expect("default serializer shouldn't fail")
}

pub fn raw_data_from_trace_with_canon(
    trace: impl Into<ExecutionTrace>,
    cid_state: ExecutionCidState,
) -> Vec<u8> {
    let data = InterpreterDataEnvelope::from_execution_result(
        trace.into(),
        CidInfo {
            value_store: cid_state.value_tracker.into(),
            tetraplet_store: cid_state.tetraplet_tracker.into(),
            canon_element_store: cid_state.canon_element_tracker.into(),
            canon_result_store: cid_state.canon_result_tracker.into(),
            service_result_store: cid_state.service_result_agg_tracker.into(),
        },
        <_>::default(),
        0,
        semver::Version::new(1, 1, 1),
    );
    data.serialize().expect("default serializer shouldn't fail")
}

#[macro_export]
macro_rules! assert_next_pks {
    ($expected:expr, $actual:expr) => {
        let expected: std::collections::HashSet<_> =
            $expected.into_iter().map(|s| s.as_str()).collect();
        let actual: std::collections::HashSet<_> = $actual.iter().map(|s| *s).collect();

        assert_eq!(expected, actual)
    };
}

pub fn print_trace(result: &RawAVMOutcome, trace_name: &str) {
    let data = data_from_result(result);
    let trace = &data.trace;

    println!("trace {} (states_count: {}): [", trace_name, trace.len());
    for (id, state) in trace.iter().enumerate() {
        print!("  {id}: {state}");
        match state {
            ExecutedState::Call(call_result) => print_call_value(&data, call_result),
            ExecutedState::Canon(canon_result) => print_canon_values(&data, canon_result),
            ExecutedState::Par(_) | ExecutedState::Fold(_) | ExecutedState::Ap(_) => {}
        }
        println!();
    }
    println!("]");
}

fn print_call_value(data: &InterpreterData, call_result: &CallResult) {
    let service_result_cid = match call_result {
        CallResult::Executed(ValueRef::Unused(_)) | CallResult::RequestSentBy(_) => return,
        CallResult::Executed(ValueRef::Scalar(cid)) => cid,
        CallResult::Executed(ValueRef::Stream { cid, .. }) => cid,
        CallResult::Failed(cid) => cid,
    };

    let service_result = data
        .cid_info
        .service_result_store
        .get(service_result_cid)
        .unwrap_or_else(|| panic!("service result CID not found: {:?}", service_result_cid));
    let value = data
        .cid_info
        .value_store
        .get(&service_result.value_cid)
        .unwrap_or_else(|| panic!("value CID not found: {:?}", service_result.value_cid));
    print!(" => {:#?}", value);
}

fn print_canon_values(data: &InterpreterData, canon_result: &CanonResult) {
    let canon_result_cid = match canon_result {
        CanonResult::RequestSentBy(_) => return,
        CanonResult::Executed(cid) => cid,
    };
    let canon_agg = data
        .cid_info
        .canon_result_store
        .get(canon_result_cid)
        .unwrap_or_else(|| panic!("canon result CID not found: {:?}", canon_result_cid));
    let canon_vals: Vec<_> = canon_agg
        .values
        .iter()
        .map(|elt_cid| {
            let elt = data
                .cid_info
                .canon_element_store
                .get(elt_cid)
                .unwrap_or_else(|| panic!("canon element CID not found: {:?}", elt_cid));
            data.cid_info
                .value_store
                .get(&elt.value)
                .unwrap_or_else(|| panic!("value CID not found: {:?}", elt.value));
        })
        .collect();
    print!(" => {:?}", canon_vals)
}

#[macro_export]
macro_rules! rc {
    ($expr:expr) => {
        ::std::rc::Rc::new($expr)
    };
}

use air::ToErrorCode;
use air_interpreter_interface::INTERPRETER_SUCCESS;

pub fn is_interpreter_succeded(result: &RawAVMOutcome) -> bool {
    result.ret_code == INTERPRETER_SUCCESS
}

pub fn check_error(result: &RawAVMOutcome, error: impl ToErrorCode + ToString) -> bool {
    result.ret_code == error.to_error_code() && result.error_message == error.to_string()
}

#[macro_export]
macro_rules! assert_error_eq {
    ($result:expr, $error:expr $(,)?) => {{
        let result: &::air_test_utils::RawAVMOutcome = $result;
        let error = $error;
        ::std::assert_eq!(
            (result.ret_code, &result.error_message),
            (::air::ToErrorCode::to_error_code(&error), &::std::string::ToString::to_string(&error))
        );
    }};
    ($result:expr, $error:expr, $($arg:tt)+) => {{
        let result: &::air_test_utils::RawAVMOutcome = $result;
        let error = $error;
        ::std::assert_eq!(
            (result.ret_code, &result.error_message),
            (::air::ToErrorCode::to_error_code(&error), &::std::string::ToString::to_string(&error)),
            $($arg)+
        );
    }};
}