Rúnar

The Test Runner

Runar ships with a purpose-built test runner that understands contract compilation, script evaluation, and transaction validation. It is built on top of vitest and extends it with contract-aware capabilities — source map integration, script-level tracing, and property-based fuzzing support.

Running Tests from the CLI

The simplest way to run your test suite:

runar test

This discovers all *.test.ts files in the tests/ directory, compiles any referenced contracts, and runs the tests. Under the hood, it invokes vitest with pre-configured transforms that handle .runar.ts imports.

Common Flags

# Run a specific test file
runar test tests/PriceBet.test.ts

The following flags are vitest flags that may be passed through (they are not registered runar CLI flags):

# Run tests in watch mode (re-runs on file change)
runar test --watch

# Run tests matching a name pattern
runar test --filter "settles to Alice"

# Show verbose output with script execution traces
runar test --verbose

# Generate a coverage report
runar test --coverage

# Run tests in parallel (default) or sequentially
runar test --sequence

Exit Codes

CodeMeaning
0All tests passed
1One or more tests failed
2Configuration or compilation error

Test Configuration Options

Test behavior is configured through vitest.config.ts (or vite.config.ts). The Runar monorepo’s root vitest.config.ts provides aliases so tests can import runar-testing and runar-compiler by name:

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // Directory containing test files
    include: ['tests/**/*.test.ts'],

    // Timeout per test in milliseconds
    testTimeout: 30000,
  },
});

Since runar test delegates to vitest, all standard vitest configuration options apply. See the vitest documentation for the full set of options.

Filtering and Selecting Tests

By File

Pass one or more file paths to run specific test files:

runar test tests/PriceBet.test.ts tests/Counter.test.ts

By Test Name

Use --filter to match test names with a substring or regex:

# Substring match
runar test --filter "settles"

# Regex match
runar test --filter "/^PriceBet.*Alice/"

By Tag

Tests can be tagged using vitest’s built-in describe blocks, and filtered by tag:

describe('PriceBet [integration]', () => {
  // ...
});
runar test --filter "[integration]"

ScriptVM: Direct Bitcoin Script Execution

For low-level testing, runar-testing exports a ScriptVM that executes raw Bitcoin Script bytecode. This is useful when you need to test compiled output directly or verify script behavior without the TestContract abstraction:

import { ScriptVM, hexToBytes } from 'runar-testing';

const vm = new ScriptVM();

// Execute unlocking script followed by locking script
// ScriptVM.execute() takes Uint8Array arguments, not hex strings
const result = vm.execute(
  hexToBytes('4830450221...'), // unlocking script (Uint8Array)
  hexToBytes('76a914...88ac')  // locking script (Uint8Array)
);

expect(result.success).toBe(true);
expect(result.stack).toEqual([new Uint8Array([1])]); // OP_TRUE left on stack (Uint8Array[])

By default, the ScriptVM mocks signature verification (checkSig always returns true). Provide a checkSigCallback in options to use real signature verification.

RunarInterpreter: Reference Oracle

The RunarInterpreter is a reference implementation that executes the ANF intermediate representation directly, without compiling to Bitcoin Script. It serves as a ground-truth oracle for verifying compiler correctness:

import { RunarInterpreter } from 'runar-testing';

const interpreter = new RunarInterpreter();
const result = interpreter.execute(anfProgram, 'methodName', args);

You rarely use the interpreter directly in application tests. It is primarily used by the Runar project’s own conformance test suite to validate that compiled script behavior matches the interpreted behavior.

Property-Based Fuzzing

Runar integrates with fast-check for property-based testing. The runar-testing package provides contract-specific generators:

import { describe, it } from 'vitest';
import { fc } from '@fast-check/vitest';
import { arbContract, arbStatelessContract } from 'runar-testing';

describe('PriceBet fuzz', () => {
  it.prop([arbContract('./contracts/PriceBet.runar.ts')])(
    'never panics on random inputs',
    (contract) => {
      // arbContract generates random valid initial state and method args
      const result = contract.callRandom();
      // Property: execution should never throw an unhandled exception.
      // It can fail (result.success === false) but must not crash.
      expect(result).toBeDefined();
    }
  );
});

Available Generators

arbContract(sourcePath) — Generates a TestContract with random initial state and provides a callRandom() method that invokes a random public method with random arguments of the correct types.

arbStatelessContract(sourcePath) — Same as arbContract but specialized for stateless contracts. It skips state initialization and only generates method arguments.

Both generators respect the contract’s type annotations to produce well-typed random values (e.g., bigint for numeric fields, valid-length hex strings for PubKey).

Differential Fuzzing

Differential fuzzing compares the output of two implementations to find discrepancies. Runar uses this technique to validate that compiled Bitcoin Script produces the same results as the reference interpreter:

import { differentialFuzz } from 'runar-testing';

// Runs the same random programs through compiler+ScriptVM and RunarInterpreter,
// comparing results. Throws on first disagreement.
differentialFuzz('./contracts/PriceBet.runar.ts', { runs: 5000 });

This is primarily a compiler development tool, but contract authors can use it to build confidence that their contract compiles correctly.

Cross-Compiler Conformance

Runar supports four frontend languages (TypeScript, Solidity, Move, Python). The conformance test suite verifies that the same logical contract produces identical compiled output regardless of the source language. It works by comparing SHA-256 hashes of the compiled artifact against golden files:

The conformance suite is run as part of the project’s internal test infrastructure. The golden files live in tests/golden/ and contain the expected SHA-256 hash for each contract/compiler combination. A mismatch indicates a compiler regression or an intentional change that requires updating the golden files.

Understanding Test Output and Reports

A typical test run produces output like this:

runar test v0.1.0

Compiling 3 contracts...
  P2PKH.runar.ts       42ms   (186 bytes)
  PriceBet.runar.ts    118ms  (1,204 bytes)
  Counter.runar.ts     87ms   (892 bytes)

Running 8 tests in 3 files...

 PASS  tests/P2PKH.test.ts (2 tests) 48ms
 PASS  tests/PriceBet.test.ts (3 tests) 92ms
 FAIL  tests/Counter.test.ts (3 tests) 67ms
   x Counter > rejects decrement below zero
     AssertionError: expected false to be true
     Script trace:
       OP_1 OP_SUB OP_DUP OP_0 OP_LESSTHAN OP_VERIFY
                                              ^^^^^^^^ FAILED
     Source: Counter.runar.ts:18 assert(this.count >= 0n)

 Test Files  2 passed | 1 failed (3)
      Tests  7 passed | 1 failed (8)
   Duration  512ms

When --verbose or trace: true is enabled, failed tests include:

  • The full Bitcoin Script opcode trace
  • Stack contents at each step
  • A source map back to the original line of Runar code

Continuous Integration Setup

Runar tests work in any CI environment that supports Node.js. Here is a GitHub Actions example:

name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install
      - run: pnpm runar test --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

For projects using Go or Rust contracts, add the corresponding toolchain setup steps before runar test. The test runner automatically detects the source language and invokes the appropriate frontend compiler.