Skip to main content

Observing the machine

These integration tests leverage the observeTrace<V>() helper function to consolidate the logic to setup and execute the testing of a particular example pointer. This function is designed to simulate an EVM and repeatedly observe the result of dererencing this pointer across each step in the machine trace.

This function accepts a test case description in the form of an options argument of type ObserveTraceOptions. In its simplest form, this object must contain the following information:

  • The pointer to be dereferenced and viewed repeatedly
  • Solidity code for a contract whose constructor manages a variable to which the pointer corresponds
  • An observe({ regions, read }: Cursor.View): Promise<V> function that converts a cursor view into a native JavaScript value of type V

With this information, the observeTrace<V>() function initializes an in-memory EVM, compiles and deploys the Solidity contract, then steps through the code execution of that contract's deployment. Over the course of this stepping, this function first dereferences the given pointer and then repeatedly calls observe() with each new machine state. It aggregates all the changing values of type V it observes and finally returns the full V[] list of these values.

This enables the integration tests to evaluate how a pointer gets dereferenced in native JavaScript terms, rather than just in the terms of a particular resolved collection of regions. For instance, this allows tests to specify that observing a Solidity string storage pointer should yield a list of JavaScript string values.

Beyond the "simplest form" described above, ObserveTraceOptions defines a number of optional properties for customizing observation behavior, including to allow observing pointers to complex types (e.g. arrays) and to allow skipping observation at times where it may be unsafe. See below for the full documented code listing for this type.

Function implementation

The full implementation for observeTrace follows:

/**
* This function performs the steps necessary to setup and watch the code
* execution of the given contract's deployment.
*
* This function tracks the changes to the given pointer's dereferenced cursor
* by invoking the given `observe()` function to obtain a single primitive
* result of type `V`.
*
* Upon reaching the end of the trace for this code execution, this function
* then returns an ordered list of all the observed values, removing sequential
* duplicates (using the defined `equals` function if it exists or just `===`).
*/
export async function observeTrace<V>({
pointer,
compileOptions,
observe,
equals = (a, b) => a === b,
shouldObserve = () => Promise.resolve(true)
}: ObserveTraceOptions<V>): Promise<V[]> {
const observedValues: V[] = [];

// initialize local development blockchain
const provider = (await loadGanache()).provider({
logging: {
quiet: true
}
});

// perform compilation
const bytecode = await compileCreateBytecode(compileOptions);

// deploy contract
const { transactionHash } = await deployContract(bytecode, provider);

// prepare to inspect the EVM for that deployment transaction
const machine = machineForProvider(provider, transactionHash);

let cursor; // delay initialization until first state of trace
let lastObservedValue;
for await (const state of machine.trace()) {
if (!await shouldObserve(state)) {
continue;
}

if (!cursor) {
cursor = await dereference(pointer, { state });
}

const { regions, read } = await cursor.view(state);
const observedValue = await observe({ regions, read }, state);

if (
typeof lastObservedValue === "undefined" ||
!equals(observedValue, lastObservedValue)
) {
observedValues.push(observedValue);
lastObservedValue = observedValue;
}
}

return observedValues;
}

interface ObserveTraceOptions<V>

This interface is generic to some type V:

export interface ObserveTraceOptions<V> {
/**
* Pointer that is used repeatedly over the course of a trace to view the
* machine at each step.
*/
pointer: Pointer;

/**
* The necessary metadata and the Solidity source code for a contract whose
* `constructor()` manages the lifecycle of the variable that the specified
* `pointer` corresponds to
*/
compileOptions: CompileOptions;

/**
* A function that understands the structure of the specified `pointer` and
* converts a particular `Cursor.View` for that pointer into a
* JavaScript-native value of type `V`
*/
observe({ regions, read }: Cursor.View, state: Machine.State): Promise<V>;

/**
* Optional predicate that compares two `V` values for domain-specific
* equality.
*
* If not specified, this defaults to `(a, b) => a === b`.
*/
equals?(a: V, b: V): boolean;

/**
* Optional asynchronous predicate that specifies whether or not a particular
* step in the machine trace is a safe time to view the cursor for the
* specified `pointer`.
*
* If not specified, this defaults to `() => Promise.resolve(true)` (i.e.,
* every step gets observed).
*/
shouldObserve?(state: Machine.State): Promise<boolean>;
}