|
| 1 | +# xtp-test |
| 2 | + |
| 3 | +A Zig test framework for [xtp](https://getxtp.com) / |
| 4 | +[Extism](https://extism.org) plugins. |
| 5 | + |
| 6 | +## Example |
| 7 | + |
| 8 | +```zig |
| 9 | +const std = @import("std"); |
| 10 | +const Test = @import("xtp-test").Test; |
| 11 | +
|
| 12 | +const CountVowel = struct { |
| 13 | + total: u32, |
| 14 | + count: u32, |
| 15 | + vowels: []const u8, |
| 16 | +}; |
| 17 | +
|
| 18 | +// you _must_ export a single `test` function (in Zig, "test" is a keyword, so use this raw literal syntax) |
| 19 | +export fn @"test"() i32 { |
| 20 | + // initialize your test to run functions in a target plugin |
| 21 | + const xtp_test = Test.init(std.heap.wasm_allocator); |
| 22 | + xtp_test.assert("this is a test", true); |
| 23 | +
|
| 24 | + // run the "count_vowels" function in the target plugin and assert the output is as expected |
| 25 | + const output = xtp_test.call("count_vowels", "this is a test") catch unreachable; |
| 26 | + const cv = fromJson(output); |
| 27 | + xtp_test.assertEq("count_vowels returns expected count", cv.count, 4); |
| 28 | +
|
| 29 | + // create a group of tests inside a new scope, use defer to close the group at the end of the scope |
| 30 | + // NOTE: creating a group will reset the target plugin, before the group starts and after it's closed |
| 31 | + { |
| 32 | + const maintain_state_group = xtp_test.newGroup("plugin should maintain state"); |
| 33 | + errdefer maintain_state_group.close(); |
| 34 | + var accumTotal: u32 = 0; |
| 35 | + for (0..10) |_| { |
| 36 | + const loop_output = xtp_test.call("count_vowels", "this is a test") catch unreachable; |
| 37 | + const loop_cv = fromJson(loop_output); |
| 38 | + accumTotal += cv.count; |
| 39 | + const msg = std.fmt.allocPrint(std.heap.wasm_allocator, "count_vowels returns expected incremented total: {}", .{accumTotal}) catch unreachable; |
| 40 | + xtp_test.assertEq(msg, loop_cv.total, accumTotal); |
| 41 | + } |
| 42 | + } |
| 43 | +
|
| 44 | + // create a group without a scope, and close it manually at the end of your tests |
| 45 | + const simple_group = xtp_test.newGroup("simple timing tests"); |
| 46 | + // get the number of seconds elapsed in the "count_vowels" function using some input |
| 47 | + const sec = xtp_test.timeSec("count_vowels", "this is a test"); |
| 48 | + xtp_test.assert("it should be fast", sec < 0.5); |
| 49 | +
|
| 50 | + // get the number of nanoseconds elapsed in the "count_vowels" function using some input |
| 51 | + const ns = xtp_test.timeNs("count_vowels", "this is a test"); |
| 52 | + xtp_test.assert("it should be really fast", ns < 1e5); |
| 53 | + simple_group.close(); |
| 54 | +
|
| 55 | + return 0; |
| 56 | +} |
| 57 | +
|
| 58 | +fn fromJson(json: []const u8) CountVowel { |
| 59 | + const cv = std.json.parseFromSlice(CountVowel, std.heap.wasm_allocator, json, .{}) catch unreachable; |
| 60 | + return cv.value; |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +## API Docs |
| 65 | + |
| 66 | +See the [`main.zig`](/src/main.zig) file for the public API of this library. |
| 67 | + |
| 68 | +## Usage |
| 69 | + |
| 70 | +**1. Create a Zig project using the XTP Test library** |
| 71 | + |
| 72 | +```sh |
| 73 | +mkdir zig-xtp-test |
| 74 | +cd zig-xtp-test |
| 75 | +zig init |
| 76 | +zig fetch --save https://github.com/dylibso/xtp-test-zig/archive/v0.0.1.tar.gz |
| 77 | +# see the `build.zig` in this repo for examples on how to configure it |
| 78 | +``` |
| 79 | + |
| 80 | +**2. Write your test in Zig** |
| 81 | + |
| 82 | +```zig |
| 83 | +const std = @import("std"); |
| 84 | +const Test = @import("xtp-test").Test; |
| 85 | +
|
| 86 | +const CountVowel = struct { |
| 87 | + total: u32, |
| 88 | + count: u32, |
| 89 | + vowels: []const u8, |
| 90 | +}; |
| 91 | +
|
| 92 | +// you _must_ export a single `test` function (in Zig, "test" is a keyword, so use this raw literal syntax) |
| 93 | +export fn @"test"() i32 { |
| 94 | + // initialize your test to run functions in a target plugin |
| 95 | + const xtp_test = Test.init(std.heap.wasm_allocator); |
| 96 | + xtp_test.assert("this is a test", true); |
| 97 | +
|
| 98 | + // run the "count_vowels" function in the target plugin and assert the output is as expected |
| 99 | + const output = xtp_test.call("count_vowels", "this is a test") catch unreachable; |
| 100 | + const cv = fromJson(output); |
| 101 | + xtp_test.assertEq("count_vowels returns expected count", cv.count, 4); |
| 102 | +
|
| 103 | + ... |
| 104 | +``` |
| 105 | + |
| 106 | +**3. Compile your test to .wasm:** |
| 107 | + |
| 108 | +Ensure your `build.zig` is set up properly to compile to wasm32 `freestanding` |
| 109 | +or `wasi`. See the |
| 110 | +[Extism `zig-pdk` examples](https://github.com/extism/zig-pdk) or the |
| 111 | +`build.zig` in this repository for more details. |
| 112 | + |
| 113 | +```sh |
| 114 | +zig build |
| 115 | +# which should output a .wasm into zig-out/bin/ |
| 116 | +``` |
| 117 | + |
| 118 | +**4. Run the test against your plugin:** Once you have your test code as a |
| 119 | +`.wasm` module, you can run the test against your plugin using the `xtp` CLI: |
| 120 | + |
| 121 | +### Install `xtp` |
| 122 | + |
| 123 | +```sh |
| 124 | +curl https://static.dylibso.com/cli/install.sh | sudo sh |
| 125 | +``` |
| 126 | + |
| 127 | +### Run the test suite |
| 128 | + |
| 129 | +```sh |
| 130 | +xtp plugin test ./plugin-*.wasm --with test.wasm --host host.wasm |
| 131 | +# ^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ |
| 132 | +# your plugin(s) test to run optional mock host functions |
| 133 | +``` |
| 134 | + |
| 135 | +**Note:** The optional mock host functions must be implemented as Extism |
| 136 | +plugins, whose exported functions match the host function signature imported by |
| 137 | +the plugins being tested. |
| 138 | + |
| 139 | +## Need Help? |
| 140 | + |
| 141 | +Please reach out via the |
| 142 | +[`#xtp` channel on Discord](https://discord.com/channels/1011124058408112148/1220464672784908358). |
0 commit comments