1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
use anyhow::Result;
use extism_pdk::{Memory, ToMemory};

mod harness {
    #[link(wasm_import_module = "xtp:test/harness")]
    extern "C" {
        pub fn call(name: u64, input: u64) -> u64;
        pub fn time(name: u64, input: u64) -> u64;
        pub fn assert(name: u64, value: u64, message: u64);
        pub fn reset();
        pub fn group(name: u64);
    }
}

/// Call a function from the Extism plugin being tested, passing input and returning its output Memory.
pub fn call_memory(func_name: impl AsRef<str>, input: impl ToMemory) -> Result<Memory> {
    let func_name = func_name.as_ref();
    let func_mem = Memory::from_bytes(func_name)?;
    let input_mem = input.to_memory()?;
    let output_ptr = unsafe { harness::call(func_mem.offset(), input_mem.offset()) };
    func_mem.free();
    input_mem.free();

    let output = match Memory::find(output_ptr) {
        None => anyhow::bail!("Error in call to {func_name}: invalid output offset"),
        Some(x) => x,
    };
    Ok(output)
}

/// Call a function from the Extism plugin being tested, passing input and returning its output.
pub fn call<T: extism_pdk::FromBytesOwned>(
    func_name: impl AsRef<str>,
    input: impl ToMemory,
) -> Result<T> {
    let output_mem = call_memory(func_name, input)?;
    let output = output_mem.to();
    output_mem.free();
    output
}

/// Call a function from the Extism plugin being tested, passing input and returning the time in nanoseconds spent in the fuction.
pub fn time_ns(func_name: impl AsRef<str>, input: impl ToMemory) -> Result<u64> {
    let func_name = func_name.as_ref();
    let func_mem = Memory::from_bytes(func_name)?;
    let input_mem = input.to_memory()?;
    let ns = unsafe { harness::time(func_mem.offset(), input_mem.offset()) };
    func_mem.free();
    input_mem.free();

    Ok(ns)
}

/// Call a function from the Extism plugin being tested, passing input and returning the time in seconds spent in the fuction.
pub fn time_sec(func_name: impl AsRef<str>, input: impl ToMemory) -> Result<f64> {
    time_ns(func_name, input).map(|x| x as f64 / 1e9)
}

/// Assert that the `outcome` is true, naming the assertion with `name`, which will be used as a label in the CLI runner. The `reason` argument
/// will be used to print a message when the assertion fails, this should contain some additional information about values being compared.
pub fn assert(name: impl AsRef<str>, outcome: bool, reason: impl AsRef<str>) {
    let name_mem = Memory::from_bytes(name.as_ref()).expect("assert name Extism memory");
    let reason_mem = Memory::from_bytes(reason.as_ref()).expect("assert reason Extism memory");
    unsafe {
        harness::assert(name_mem.offset(), outcome as u64, reason_mem.offset());
    }
    reason_mem.free();
    name_mem.free();
}

/// Assert that `x` and `y` are equal, naming the assertion with `msg`, which will be used as a label in the CLI runner.
pub fn assert_eq<U: std::fmt::Debug, T: std::fmt::Debug + PartialEq<U>>(
    msg: impl AsRef<str>,
    x: T,
    y: U,
) {
    assert(msg, x == y, format!("Expected {:?} == {:?}", x, y));
}

/// Assert that `x` and `y` are not equal, naming the assertion with `msg`, which will be used as a label in the CLI runner.
pub fn assert_ne<U: std::fmt::Debug, T: std::fmt::Debug + PartialEq<U>>(
    msg: impl AsRef<str>,
    x: T,
    y: U,
) {
    assert(msg, x != y, format!("Expected {:?} != {:?}", x, y));
}

// Create a new test group. NOTE: these cannot be nested and starting a new group will end the last one.
fn start_group(name: impl AsRef<str>) {
    let name_mem = Memory::from_bytes(name.as_ref()).expect("assert message Extism memory");
    unsafe {
        harness::group(name_mem.offset());
    }
    name_mem.free();
}

/// Reset the loaded plugin, clearing all state.
pub fn reset() {
    unsafe {
        harness::reset();
    }
}

/// Run a test group, resetting the plugin before and after the group is run.
/// ```rust
/// use extism_pdk::*;
///
/// #[plugin_fn]
/// pub fn test() -> FnResult<()> {
///   xtp_test::group("group name", || {
///       xtp_test::assert("test name", true);
///   })?;
///   Ok(())
/// }
/// ```
pub fn group(name: impl AsRef<str>, f: impl FnOnce() -> Result<()>) -> Result<()> {
    reset();
    start_group(name);
    let res = f();
    reset();
    res
}

#[macro_export]
macro_rules! assert {
    ($name:expr, $b:expr) => {
        $crate::assert(
            $name,
            $b,
            format!("Assertion failed ({}, line {})", file!(), line!()),
        );
    };
}

#[macro_export]
macro_rules! assert_eq {
    ($name:expr, $a:expr, $b:expr) => {
        $crate::assert(
            $name,
            $a == $b,
            format!(
                "Expected {:?} == {:?} ({}, line {})",
                $a,
                $b,
                file!(),
                line!()
            ),
        );
    };
}

#[macro_export]
macro_rules! assert_ne {
    ($name:expr, $a:expr, $b:expr) => {
        $crate::assert(
            $name,
            $a != $b,
            format!(
                "Expected {:?} != {:?} ({}, line {})",
                $a,
                $b,
                file!(),
                line!()
            ),
        );
    };
}