Skip to content

dylibso/xtp-test-js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

xtp-test

A JavaScript test framework for xtp / Extism plugins.

Example

// test.js
import { Test } from "@dylibso/xtp-test";

export function test() {
  // call a function from some Extism plugin (you'll link these up in the CLI command to run the test),
  // passing in some data and getting back `MemoryView`, which we convert to JSON using the `MemoryView.json`
  // method
  const res = Test.call("count_vowels", "some input").json();
  const count = res["count"];
  // assert the count of the vowels is correct, giving the test case a name (which will be shown in the CLI output)
  Test.assertEqual("count_vowels of 'some input'", count, 4);

  // create a group of tests, which will be run together. This resets the plugin before and after the group is complete.
  Test.group("count_vowels maintains state", () => {
    let accumTotal = 0;
    const expectedFinalTotal = 12;
    for (let i = 0; i < 3; i++) {
      const output = Test.call("count_vowels", "this is a test").json();
      accumTotal += output.count;
      Test.assertEqual(
        `total count increased to: ${accumTotal}`,
        accumTotal,
        4 * (i + 1),
      );
    }

    Test.assertEqual(
      "expected total reached by end of test",
      accumTotal,
      expectedFinalTotal,
    );
  });

  // this function is also an Extism plugin, so return an int32 value (non-zero returns will cause the whole test suite to fail.)
  return 0;
}

API Docs

Test is the primary entrypoint to this library. It exposes plugin calling functions and timing & assetion functions to validate expectations of plugin behavior.

export class Test {
  // call a function from the Extism plugin being tested, passing in `Input` and returning the output as `MemoryView`, which 
  // can be used to convert the type to a JavaScript native value.
  static call(funcName: string, input: Input): MemoryView { ... }

  // read the mock test input provided by the test runner, returns `MemoryView`.
  // this input is defined in an xtp.toml file, or by the --mock-input-data or --mock-input-file flags.
  static mockInput(): MemoryView { ... }
  
  // Run a test group, resetting the plugin before and after the group is run.
  static group(name: string, callback: () => void) { .. }

  // Reset the loaded plugin, clearing all state.
  static reset() { ... }

  // call a function from the Extism plugin being tested, passing in `Input` and get the number of nanoseconds spent in the function.
  static timeNanoseconds(funcName: string, input: Input): number { ... }

  // call a function from the Extism plugin being tested, passing in `Input` and get the number of seconds spent in the function.
  static timeSeconds(funcName: string, input: Input): number { ... }

  // 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.
  static assert(name: string, outcome: boolean, reason: string) { ... }

  // assert that `x` and `y` are equal, naming the assertion with `msg`, which will be used as a label in the CLI runner.
  static assertEqual(msg: string, x: unknown, y: unknown) { ... }
  
  // assert that `x` and `y` are not equal, naming the assertion with `msg`, which will be used as a label in the CLI runner.
  static assertNotEqual(msg: string, x: unknown, y: unknown) { ... }

  // assert that `x` is greater than `y`, naming the assertion with `msg`, which will be used as a label in the CLI runner.
  static assertGreaterThan(msg: string, x: any, y: any) { ... }

  // assert that `x` is greater than or equal to `y`, naming the assertion with `msg`, which will be used as a label in the CLI runner.
  static assertGreaterThanOrEqualTo(msg: string, x: any, y: any) { ... }
  
  // assert that `x` is less than `y`, naming the assertion with `msg`, which will be used as a label in the CLI runner.
  static assertLessThan(msg: string, x: any, y: any) { ... }
  
  // assert that `x` is less than or equal to `y`, naming the assertion with `msg`, which will be used as a label in the CLI runner.
  static assertLessThanOrEqualTo(msg: string, x: any, y: any) { ... }
}

Input is a type to represent various kinds of function call input data.

type Input = string | ArrayBuffer | object | undefined;

MemoryView wraps an Extism memory handle, allowing you to convert between multiple types without dealing directly with low-level memory access.

// Provides access to data in Extism memory
export class MemoryView extends DataView {
  ...

  // Returns true if the underlying memory handle is empty or undefined.
  isEmpty(): boolean {...}

  // Get the JSON representation of a value stored in Extism memory
  json(): any { ... }

  // Get the string representation of a value stored in Extism memory
  text(): string { ... }

  // Read bytes from Extism memory into an ArrayBuffer
  arrayBuffer(): ArrayBuffer { ... }
}

Usage

Follow the steps to compile this to WebAssembly using the Extism js-pdk toolchain, in particular, the instructions to use a bundler.

Quick steps:

1. Install extism-js compiler:

curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh
sh install.sh

2. Create your test script:

You need an interface file to link the xtp-list library, so create test.d.ts and paste this:

// test.d.ts
declare module "main" {
  export function test(): I32;
}

declare module "xtp:test" {
  interface harness {
    assert(name: PTR, value: I64, reason: PTR): void;
    call(func: PTR, input: PTR): PTR;
    time(func: PTR, input: PTR): I64;
    group(name: PTR): void;
    reset(): void;
    mock_input(): PTR;
  }
}

Your test will call function exports, but here we demonstrate calling our count_vowels function from an example module:

// test.js
import { Test } from "@dylibso/xtp-test";

export function test() {
  const res = Test.call("count_vowels", "some input").json();
  const count = res["count"];
  Test.assertEqual("count_vowels of 'some input'", count, 4);
  return 0;
}

3. Compile your test and interface to .wasm:

Once you bundle test.js using esbuild or something similar, you can compile your test to .wasm using extism-js:

extism-js dist/test.js -i test.d.ts -o test.wasm

4. Run the test against your plugin: Once you have your test code as a .wasm module, you can run the test against your plugin using the xtp CLI:

Install xtp

curl https://static.dylibso.com/cli/install.sh | sudo sh

Run the test suite

xtp plugin test ./plugin-*.wasm --with test.wasm --mock-host host.wasm
#               ^^^^^^^^^^^^^^^        ^^^^^^^^^             ^^^^^^^^^
#               your plugin(s)         test to run           optional mock host functions

Note: The optional mock host functions must be implemented as Extism plugins, whose exported functions match the host function signature imported by the plugins being tested.

Need Help?

Please reach out via the #xtp channel on Discord.