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 typeV
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>;
}