Skip to content

Commit

Permalink
Document integration tests in implementation guide
Browse files Browse the repository at this point in the history
  • Loading branch information
gnidan committed Jul 1, 2024
1 parent a93223a commit 7a7d9f9
Show file tree
Hide file tree
Showing 9 changed files with 534 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState, useEffect } from "react";

import prettier from "prettier/standalone";

import { describeSchema } from "@ethdebug/format";
import { observeTraceTests } from "@ethdebug/pointers/dist/src/test-cases"

import { Collapsible } from "@theme/JSONSchemaViewer/components";
import CodeBlock from "@theme/CodeBlock";
import CodeListing from "@site/src/components/CodeListing";

const solidityPlugin: any = require("prettier-plugin-solidity/standalone");

export interface TestCaseProps {
name: string;
variableName: string;
}

export default function TestCase({
name,
variableName
}: TestCaseProps): JSX.Element {
const {
pointer,
compileOptions,
expectedValues
} = observeTraceTests[name as keyof typeof observeTraceTests];

const [sourcePath, { content }] =
// use the first since all test cases use only one source file
Object.entries(compileOptions.sources)[0];

const [
formattedContent,
setFormattedContent
] = useState<string | undefined>();

useEffect(() => {
prettier.format(content, {
parser: "solidity-parse",
plugins: [solidityPlugin]
})
.then(setFormattedContent);
}, [setFormattedContent]);

return <>
<h3>Solidity code</h3>

{typeof formattedContent === "undefined"
? <>Loading Solidity code...</>
: <CodeBlock language="solidity">{formattedContent}</CodeBlock>}

<h3>Expected value sequence</h3>

<ol>
{expectedValues.map((expectedValue, index) => <li key={index}><code>{
JSON.stringify(expectedValue)
}</code></li>)}
</ol>

<h3>Pointer</h3>
<Collapsible summary="See Pointer JSON">
<CodeBlock language="json">{
JSON.stringify(pointer, undefined, 2)
}</CodeBlock>
</Collapsible>

<h3>Full test case code listing</h3>
<Collapsible summary="See TypeScript test case definition">
<CodeListing
packageName="@ethdebug/pointers"
sourcePath="src/test-cases.ts"
extract={sourceFile => sourceFile.getVariableStatement(variableName)}
/>
</Collapsible>


</>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "End-to-end testing",
"position": 6,
"link": {
"type": "generated-index",
"description": "Implementing integration tests"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
sidebar_position: 4
---

import CodeListing from "@site/src/components/CodeListing";

# Simulating a blockchain

:::warning

In case you missed the
[note on the Summary page](/docs/implementation-guides/pointers/testing/summary#ganache-warning),
the functionality described in this page uses the unmaintained
[Ganache](https://github.com/trufflesuite/ganache) software library for
simulating the EVM. See note for rationale and risk expectations.

:::

This reference implemention relies heavily on the
[`Machine`](/docs/implementation-guides/pointers/types/data-and-machines#Machine)
interface it defines for reading the state of a running EVM; this page describes
how this implementation's integration tests adapt an
[EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) JavaScript provider object
to this interface.

Since the primary purpose of `Machine` is to represent a series of code
execution steps, the adapter described here simplifies the concept of an
execution trace by restricting it to mean that which happens within the course
of an Ethereum transaction. The tests thus define a `machineForProvider`
function to adapt a provider object for a particular transaction hash.

As a result, this code only functions in the context of a provider to a
blockchain whose JSON-RPC exposes the original
[go-ethereum](https://github.com/ethereum/go-ethereum)'s
`"debug_traceTransaction"` method, which exposes the state of the EVM at each
step of code execution for a particular transaction. Other kinds of traces (such
as tracing the execution of an `"eth_call"` request) are left to remain
intentionally out-of-scope for the purposes of testing this implementation.
Other implementations of the `Machine` interface need not make this restriction.

## Implementing `machineForProvider()`

The `machineForProvider()` function takes two arguments and returns an object
adhering to the `Machine` interface. See the code listing for this function:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("machineForProvider")}
/>

This function is written to return an object whose `trace()` method matches that
which is defined by `Machine`: a method to asynchronously produce an iterable
list of `Machine.State`s. This function leverages two other helper functions as
part of the behavior of this method: `requestStructLogs()` and
`toMachineState()`.

### Requesting "struct logs"

The Geth-style `"debug_traceTransaction"` method returns a list of execution
steps and machine states inside the `"structLogs"` field of the response's
result object.

The asynchronous `requestStructLogs` function is implemented as follows:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("requestStructLogs")}
/>

Since Ganache does not have a publicly-documented or easily-accessible exported
collection of types, but since it **does** use string literal types to infer the
specific type of provider request being made, this code can use TypeScript's
type interference to ensure type safety in the adapter:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getTypeAlias("StructLogs")}
/>

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getTypeAlias("StructLog")}
/>

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getTypeAlias("Depromise")}
/>

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getTypeAlias("Dearray")}
/>

These types are not exported by this module because they are internal to
`machineForProvider()` concerns.

### Converting to `Machine.State`

The `toMachineState()` function is implemented by leveraging the use of the
[addressing schemes](/spec/pointer/concepts#a-region-is-specified-in-terms-of-an-addressing-scheme)
defined by the **ethdebug/format/pointer** schema. Notice the use of the various
helper functions, listed below.

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("toMachineState")}
/>

#### Helper function: `constantUint()`

Since the interface defined by `Machine.State` is more asynchronous than likely
necessary (certainly it is more asynchronous than necessary for these testing
purposes), many properties defined within `Machine.State` must be converted from
a readily-available constant value into a `Promise` that resolves to that value:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("constantUint")}
/>

#### Helper function: `makeStack()`

Although the specification defines the `"stack"` data location to use a regular
segment-based addressing scheme, this reference implementation distinguishes the
stack from the other segment-based locations because of the use of numeric,
unstable slot values.

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("makeStack")}
/>

#### Helper function: `makeWords()`

For other segment-based locations, the `makeWords()` function is used:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("makeWords")}
/>

#### Helper function: `makeBytes()`

The `makeBytes()` function is used for plain bytes-based data locations, such as
`"memory"`:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("makeBytes")}
/>

## Note on loading Ganache

To prevent Ganache's warnings from appearing in test console output, a custom
`loadGanache()` function is defined to suppress known warnings while importing
the module:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/ganache.ts"
extract={(sourceFile) => sourceFile.getFunction("loadGanache")}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
sidebar_position: 3
---

import CodeListing from "@site/src/components/CodeListing";

# Invoking the compiler

In being able to test a pointer dereference implementation, it is necessary to
pair each tested pointer with associated EVM code that makes the pointer
meaningful. To avoid solutions such as pre-compiling Solidity or handwriting EVM
bytecode, the **@ethdebug/pointers** reference implementation's integration
tests are written so that each test case is described in terms of Solidity code
that the testing infrastructure compiles when executing the test.

The strategy taken by these tests is to use Solidity's `constructor` mechanism
for allowing tests to specify variable assignment and mutation logic without
needing to manage deployed contract instances. All these integration test cases
thus observe pointers only via the trace of a contract creation transaction.

## Integration logic

This testing infrastructure includes the `compileCreateBytecode()` function,
which accepts input resembling Solidity's compiler input data as argument (i.e.,
the collection of source contents by path and additional contract target
information) and asynchronously returns `Data` with the create (deployment)
bytecode for the target contract.

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/solc.ts"
extract={(sourceFile) => sourceFile.getFunction("compileCreateBytecode")}
/>

## The `CompileOptions` interface

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/solc.ts"
extract={(sourceFile) => sourceFile.getInterface("CompileOptions")}
/>

## "Syntactic sugar"-like helper function

To avoid test cases' needing to describe their associated code samples in terms
of source content by path, test cases that require only a single source file can
use the `singleSourceCompilation()` helper function that provides a more
succinct method for generating `CompileOptions` objects:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/solc.ts"
extract={(sourceFile) => sourceFile.getFunction("singleSourceCompilation")}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
sidebar_position: 5
---

import CodeListing from "@site/src/components/CodeListing";

# Deploying contracts

Deploying a contract with some EVM bytecode is straightforward with
[EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) JavaScript provider objects,
although it does require making a few RPC requests:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/deploy.ts"
extract={(sourceFile) => sourceFile.getFunction("deployContract")}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
sidebar_position: 2
---

import CodeListing from "@site/src/components/CodeListing";

# Finding example pointers

These integration tests seek to minimize the use of bespoke data whose
representations exist solely within the testing-associated code modules.

Instead of containing custom pointer objects defined inline, the integration
tests for this reference implementation use the official pointer examples
that are distributed as part of the **ethdebug/format/pointer** schema itself.

Since JSON Schema does not offer a means by which examples can be named (it
only defines a way to represent an ordered list of unlabeled example values),
these tests rely on searching for particular examples by their use of uniquely
indicative string values (e.g., the "string storage" example pointer is the
only example to contain the string `"string-storage-contract-variable-slot"`).

The logic for doing this search is captured by the `findExamplePointer()`
function:

<CodeListing
packageName="@ethdebug/pointers"
sourcePath="test/examples.ts"
extract={
sourceFile => sourceFile.getVariableStatement("findExamplePointer")
} />

(This function is written as an immediately-invoked inline function so as to
avoid unnecessary redundant calls to `describeSchema()`.)
Loading

0 comments on commit 7a7d9f9

Please sign in to comment.