Skip to content
Logo

Testing on the Host

On any architecture other than riscv64 the binding embeds libcmt's mock IO driver. The mock replaces the machine: inputs come from files you provide, and every output is written to a file next to them. This gives you fast, emulator-free feedback during development and works in any test runner.

Feeding inputs

The CMT_INPUTS environment variable lists the inputs for the session, as comma-separated reason:file pairs, consumed in order:

CMT_INPUTS="0:advance.bin,1:inspect.bin" node app.js

The reason selects the request type:

ReasonRequestFile contents
0advanceEVM-ABI encoded EvmAdvance input (see below)
1inspectraw query payload
anything elsegio replyraw response data (the reason is the response code)

Set CMT_DEBUG=yes to make the mock log every operation.

When the input list is exhausted, the next finish call fails — that is your test's natural end-of-session signal. run recognizes it and resolves, so a test can await the loop and then assert on the outputs. Driving finish yourself, you see the failure directly as a RollupError, whose errno depends on how the last request ended: -ENODATA if it was accepted, -ENOSYS if it was rejected (the mock's two ways of saying the same thing). Rejecting a request that is not the last one just moves on to the next input.

import { constants } from "node:os";
import { ,  } from "@cartesi/rollup";
 
const  = (: unknown) =>
     === "mock" &&
     instanceof  &&
    . === "cmt_rollup_finish" &&
    (. === -constants.. ||
        . === -constants..);

driver tells you which half of libcmt the binding was built against — "mock" on your host, "ioctl" inside a Cartesi Machine. It is fixed at build time by the target architecture, so it is a reliable check, unlike looking at CMT_INPUTS.

Collecting outputs

Each output is written to a file named after the input that produced it:

advance.output-0.bin    # first voucher/notice while handling advance.bin
advance.report-0.bin    # first report
advance.finish.bin      # accept/reject result

Vouchers and notices are EVM-ABI encoded (Voucher(address,uint256,bytes) / Notice(bytes)); reports are raw bytes. @cartesi/codec decodes both, so a test can assert on typed objects instead of byte offsets.

Generating an advance input

An advance input file is the ABI encoding of the EvmAdvance(uint256,address,address,uint256,uint256,uint256,uint256,bytes) call that the on-chain input box would produce. The simplest way to build one is encodeInput from @cartesi/codec:

// @filename: encode.ts
import {  } from "@cartesi/codec";
 
export const  = (
    {
        : 31337n,
        : `0x${"02".(20)}`,
        : `0x${"03".(20)}`,
        : 1n,
        : 1700000000n,
        : 0n,
        : 0n,
        : "0x68656c6c6f", // "hello"
    },
    "bytes",
);

You can also craft one with foundry's cast calldata, or by hand — the package's own test suite contains a dependency-free encoder in a dozen lines.

A complete test

Putting it together with node:test — encode an input, point CMT_INPUTS at it, run the application, and assert on the output files:

// @filename: app.test.ts
import  from "node:assert/strict";
import  from "node:fs";
import  from "node:os";
import path from "node:path";
import  from "node:test";
import { ,  } from "@cartesi/codec";
 
("echoes the payload as a notice", async () => {
    // run in a temp dir: the mock writes output files next to the inputs
    const  = .(path.(.(), "my-app-"));
    .();
 
    const  = path.(, "input.bin");
    .(
        ,
        (
            {
                : 31337n,
                : `0x${"02".(20)}`,
                : `0x${"03".(20)}`,
                : 1n,
                : 1700000000n,
                : 0n,
                : 0n,
                : "0x68656c6c6f", // "hello"
            },
            "bytes",
        ),
    );
    .. = `0:${}`;
 
    // the app under test: import it AFTER setting CMT_INPUTS, since the
    // device opens on construction. run() resolves when inputs run out.
    const {  } = await import("@cartesi/rollup");
    const  = new ();
    await .({
        (, ) {
            .(.);
            return true;
        },
    }); // resolves once the inputs are exhausted — session over
 
    const  = (
        .(path.(, "input.output-0.bin")),
    );
    .(., "Notice");
    .(.(.).(), "hello");
});

Where the real thing differs

The mock approximates the machine but does not emulate it: state is not rolled back on reject, and there is no determinism enforcement. Before shipping, run your application inside a real Cartesi Machine.