Compare commits

..

23 Commits

Author SHA1 Message Date
Cyrill Leutwiler 12fdd9b4d1 test
Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
2025-10-08 13:04:03 +02:00
pgherveou dffb80ac0a fixes 2025-10-08 11:18:31 +02:00
pgherveou 43a1114337 custom rpc port 2025-10-08 11:10:46 +02:00
pgherveou 3a07ea042b fix 2025-10-08 10:45:49 +02:00
pgherveou 9e2aa972db fix 2025-10-08 10:33:59 +02:00
pgherveou 86f2173e8b nit 2025-10-08 10:14:22 +02:00
pgherveou 6e658aec49 Merge branch 'pg/fmt' into pg/ml-runner 2025-10-08 10:04:38 +02:00
pgherveou 1aba74ec3e fix 2025-10-08 10:03:00 +02:00
pgherveou 180bd64bc5 Merge branch 'pg/fmt' into pg/ml-runner 2025-10-08 10:01:36 +02:00
pgherveou 967cbac349 fix 2025-10-08 10:00:32 +02:00
pgherveou a8d84c8360 fix 2025-10-08 09:59:53 +02:00
pgherveou c83a755416 Merge branch 'main' into pg/fmt 2025-10-08 09:59:42 +02:00
pgherveou 0711216539 add fmt check 2025-10-08 09:57:28 +02:00
pgherveou b40c17c0af fixes 2025-10-08 09:52:13 +02:00
pgherveou 8ae994f9de fixes 2025-10-08 09:43:36 +02:00
pgherveou 3f3cbfa934 Merge branch 'pg/fmt' into pg/ml-runner 2025-10-08 09:28:45 +02:00
pgherveou c676114fe1 apply fmt 2025-10-08 09:27:11 +02:00
pgherveou 92885351ed use polkadot-sdk rustfmt 2025-10-08 09:26:24 +02:00
pgherveou e16f8ebf59 Merge branch 'pg/fmt' into pg/ml-runner 2025-10-08 09:19:21 +02:00
pgherveou d482808eb2 add rustfmt.toml 2025-10-08 07:18:17 +00:00
pgherveou 1f84ce6f61 fix lint 2025-10-08 06:28:57 +00:00
pgherveou 765569a8b6 fix 2025-10-08 08:22:26 +02:00
pgherveou 6e64f678ee ml-runner init 2025-10-07 16:10:43 +00:00
60 changed files with 2976 additions and 2348 deletions
+19
View File
@@ -18,9 +18,28 @@ env:
POLKADOT_VERSION: polkadot-stable2506-2
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install nightly toolchain
run: rustup toolchain install nightly
- name: Install rustfmt for nightly
run: rustup component add --toolchain nightly rustfmt
- name: Cargo fmt
run: cargo +nightly fmt --all -- --check
cache-polkadot:
name: Build and cache Polkadot binaries on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: [fmt]
strategy:
matrix:
os: [ubuntu-24.04, macos-14]
+1 -1
View File
@@ -9,7 +9,7 @@ node_modules
*.log
profile.json.gz
resolc-compiler-tests
workdir
!/schema.json
!/dev-genesis.json
-3
View File
@@ -1,6 +1,3 @@
[submodule "polkadot-sdk"]
path = polkadot-sdk
url = https://github.com/paritytech/polkadot-sdk.git
[submodule "resolc-compiler-tests"]
path = resolc-compiler-tests
url = https://github.com/paritytech/resolc-compiler-tests
Generated
+41 -217
View File
@@ -782,15 +782,6 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anstream"
version = "0.6.18"
@@ -2337,7 +2328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
dependencies = [
"data-encoding",
"syn 2.0.101",
"syn 1.0.109",
]
[[package]]
@@ -2936,22 +2927,6 @@ dependencies = [
"sp-crypto-hashing",
]
[[package]]
name = "frame-decode"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c470df86cf28818dd3cd2fc4667b80dbefe2236c722c3dc1d09e7c6c82d6dfcd"
dependencies = [
"frame-metadata",
"parity-scale-codec",
"scale-decode",
"scale-encode",
"scale-info",
"scale-type-resolver",
"sp-crypto-hashing",
"thiserror 2.0.12",
]
[[package]]
name = "frame-metadata"
version = "23.0.0"
@@ -4551,6 +4526,27 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "ml-test-runner"
version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"clap",
"revive-dt-common",
"revive-dt-compiler",
"revive-dt-config",
"revive-dt-core",
"revive-dt-format",
"revive-dt-node",
"revive-dt-node-interaction",
"revive-dt-report",
"temp-dir",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "moka"
version = "0.12.10"
@@ -5589,7 +5585,6 @@ dependencies = [
"clap",
"moka",
"once_cell",
"regex",
"schemars 1.0.4",
"semver 1.0.26",
"serde",
@@ -5628,7 +5623,6 @@ dependencies = [
"semver 1.0.26",
"serde",
"serde_json",
"serde_with",
"strum",
"temp-dir",
]
@@ -5638,7 +5632,6 @@ name = "revive-dt-core"
version = "0.1.0"
dependencies = [
"alloy",
"ansi_term",
"anyhow",
"bson",
"cacache",
@@ -5669,7 +5662,7 @@ dependencies = [
"alloy",
"anyhow",
"futures",
"itertools 0.14.0",
"regex",
"revive-common",
"revive-dt-common",
"schemars 1.0.4",
@@ -5698,7 +5691,6 @@ dependencies = [
"serde_yaml_ng",
"sp-core",
"sp-runtime",
"subxt 0.44.0",
"temp-dir",
"tokio",
"tower 0.5.2",
@@ -5971,7 +5963,7 @@ dependencies = [
"security-framework 3.3.0",
"security-framework-sys",
"webpki-root-certs 0.26.11",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -7368,48 +7360,11 @@ dependencies = [
"serde",
"serde_json",
"sp-crypto-hashing",
"subxt-core 0.43.0",
"subxt-lightclient 0.43.0",
"subxt-macro 0.43.0",
"subxt-metadata 0.43.0",
"subxt-rpcs 0.43.0",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
"url",
"wasm-bindgen-futures",
"web-time",
]
[[package]]
name = "subxt"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddbf938ac1d86a361a84709a71cdbae5d87f370770b563651d1ec052eed9d0b4"
dependencies = [
"async-trait",
"derive-where",
"either",
"frame-metadata",
"futures",
"hex",
"jsonrpsee",
"parity-scale-codec",
"primitive-types 0.13.1",
"scale-bits",
"scale-decode",
"scale-encode",
"scale-info",
"scale-value",
"serde",
"serde_json",
"sp-crypto-hashing",
"subxt-core 0.44.0",
"subxt-lightclient 0.44.0",
"subxt-macro 0.44.0",
"subxt-metadata 0.44.0",
"subxt-rpcs 0.44.0",
"subxt-core",
"subxt-lightclient",
"subxt-macro",
"subxt-metadata",
"subxt-rpcs",
"thiserror 2.0.12",
"tokio",
"tokio-util",
@@ -7431,24 +7386,7 @@ dependencies = [
"quote",
"scale-info",
"scale-typegen",
"subxt-metadata 0.43.0",
"syn 2.0.101",
"thiserror 2.0.12",
]
[[package]]
name = "subxt-codegen"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c250ad8cd102d40ae47977b03295a2ff791375f30ddc7474d399fb56efb793b"
dependencies = [
"heck",
"parity-scale-codec",
"proc-macro2",
"quote",
"scale-info",
"scale-typegen",
"subxt-metadata 0.44.0",
"subxt-metadata",
"syn 2.0.101",
"thiserror 2.0.12",
]
@@ -7462,7 +7400,7 @@ dependencies = [
"base58",
"blake2",
"derive-where",
"frame-decode 0.8.3",
"frame-decode",
"frame-metadata",
"hashbrown 0.14.5",
"hex",
@@ -7478,37 +7416,7 @@ dependencies = [
"serde",
"serde_json",
"sp-crypto-hashing",
"subxt-metadata 0.43.0",
"thiserror 2.0.12",
"tracing",
]
[[package]]
name = "subxt-core"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5705c5b420294524e41349bf23c6b11aa474ce731de7317f4153390e1927f702"
dependencies = [
"base58",
"blake2",
"derive-where",
"frame-decode 0.9.0",
"frame-metadata",
"hashbrown 0.14.5",
"hex",
"impl-serde",
"keccak-hash",
"parity-scale-codec",
"primitive-types 0.13.1",
"scale-bits",
"scale-decode",
"scale-encode",
"scale-info",
"scale-value",
"serde",
"serde_json",
"sp-crypto-hashing",
"subxt-metadata 0.44.0",
"subxt-metadata",
"thiserror 2.0.12",
"tracing",
]
@@ -7530,23 +7438,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "subxt-lightclient"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e02732a6c9ae46bc282c1a741b3d3e494021b3e87e7e92cfb3620116d92911"
dependencies = [
"futures",
"futures-util",
"serde",
"serde_json",
"smoldot-light",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "subxt-macro"
version = "0.43.0"
@@ -7558,26 +7449,9 @@ dependencies = [
"proc-macro-error2",
"quote",
"scale-typegen",
"subxt-codegen 0.43.0",
"subxt-metadata 0.43.0",
"subxt-utils-fetchmetadata 0.43.0",
"syn 2.0.101",
]
[[package]]
name = "subxt-macro"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501bf358698f5ab02a6199a1fcd3f1b482e2f5b6eb5d185411e6a74a175ec8e8"
dependencies = [
"darling 0.20.11",
"parity-scale-codec",
"proc-macro-error2",
"quote",
"scale-typegen",
"subxt-codegen 0.44.0",
"subxt-metadata 0.44.0",
"subxt-utils-fetchmetadata 0.44.0",
"subxt-codegen",
"subxt-metadata",
"subxt-utils-fetchmetadata",
"syn 2.0.101",
]
@@ -7587,22 +7461,7 @@ version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c134068711c0c46906abc0e6e4911204420331530738e18ca903a5469364d9f"
dependencies = [
"frame-decode 0.8.3",
"frame-metadata",
"hashbrown 0.14.5",
"parity-scale-codec",
"scale-info",
"sp-crypto-hashing",
"thiserror 2.0.12",
]
[[package]]
name = "subxt-metadata"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01fb7c0bfafad78dda7084c6a2444444744af3bbf7b2502399198b9b4c20eddf"
dependencies = [
"frame-decode 0.9.0",
"frame-decode",
"frame-metadata",
"hashbrown 0.14.5",
"parity-scale-codec",
@@ -7627,32 +7486,8 @@ dependencies = [
"primitive-types 0.13.1",
"serde",
"serde_json",
"subxt-core 0.43.0",
"subxt-lightclient 0.43.0",
"thiserror 2.0.12",
"tokio-util",
"tracing",
"url",
]
[[package]]
name = "subxt-rpcs"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab68a9c20ecedb0cb7d62d64f884e6add91bb70485783bf40aa8eac5c389c6e0"
dependencies = [
"derive-where",
"frame-metadata",
"futures",
"hex",
"impl-serde",
"jsonrpsee",
"parity-scale-codec",
"primitive-types 0.13.1",
"serde",
"serde_json",
"subxt-core 0.44.0",
"subxt-lightclient 0.44.0",
"subxt-core",
"subxt-lightclient",
"thiserror 2.0.12",
"tokio-util",
"tracing",
@@ -7682,7 +7517,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"sp-crypto-hashing",
"subxt-core 0.43.0",
"subxt-core",
"thiserror 2.0.12",
"zeroize",
]
@@ -7698,17 +7533,6 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "subxt-utils-fetchmetadata"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e450f6812a653c5a3e63a079aa3b60a3f4c362722753c3222286eaa1800f9002"
dependencies = [
"hex",
"parity-scale-codec",
"thiserror 2.0.12",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -9502,7 +9326,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"sp-core",
"subxt 0.43.0",
"subxt",
"subxt-signer",
"thiserror 1.0.69",
"tokio",
@@ -9562,7 +9386,7 @@ dependencies = [
"async-trait",
"futures",
"lazy_static",
"subxt 0.43.0",
"subxt",
"subxt-signer",
"tokio",
"zombienet-configuration",
+1 -4
View File
@@ -22,7 +22,6 @@ revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" }
revive-dt-report = { version = "0.1.0", path = "crates/report" }
revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
ansi_term = "0.12.1"
anyhow = "1.0"
bson = { version = "2.15.0" }
cacache = { version = "13.1.0" }
@@ -50,7 +49,6 @@ sha2 = { version = "0.10.9" }
sp-core = "36.1.0"
sp-runtime = "41.1.0"
strum = { version = "0.27.2", features = ["derive"] }
subxt = { version = "0.44.0" }
temp-dir = { version = "0.1.16" }
tempfile = "3.3"
thiserror = "2"
@@ -69,14 +67,13 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
"env-filter",
] }
indexmap = { version = "2.10.0", default-features = false }
itertools = { version = "0.14.0" }
# revive compiler
revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev = "891f6554354ce466abd496366dbf8b4f82141241" }
zombienet-sdk = { git = "https://github.com/paritytech/zombienet-sdk.git", rev ="891f6554354ce466abd496366dbf8b4f82141241" }
[workspace.dependencies.alloy]
version = "1.0.37"
+174 -51
View File
@@ -52,69 +52,192 @@ All of the above need to be installed and available in the path in order for the
This tool is being updated quite frequently. Therefore, it's recommended that you don't install the tool and then run it, but rather that you run it from the root of the directory using `cargo run --release`. The help command of the tool gives you all of the information you need to know about each of the options and flags that the tool offers.
```bash
$ cargo run --release -- execute-tests --help
Error: Executes tests in the MatterLabs format differentially on multiple targets concurrently
Usage: retester execute-tests [OPTIONS]
Options:
-w, --working-directory <WORKING_DIRECTORY>
The working directory that the program will use for all of the temporary artifacts needed at runtime.
If not specified, then a temporary directory will be created and used by the program for all temporary artifacts.
[default: ]
-p, --platform <PLATFORMS>
The set of platforms that the differential tests should run on
[default: geth-evm-solc,revive-dev-node-polkavm-resolc]
Possible values:
- geth-evm-solc: The Go-ethereum reference full node EVM implementation with the solc compiler
- kitchensink-polkavm-resolc: The kitchensink node with the PolkaVM backend with the resolc compiler
- kitchensink-revm-solc: The kitchensink node with the REVM backend with the solc compiler
- revive-dev-node-polkavm-resolc: The revive dev node with the PolkaVM backend with the resolc compiler
- revive-dev-node-revm-solc: The revive dev node with the REVM backend with the solc compiler
-c, --corpus <CORPUS>
A list of test corpus JSON files to be tested
-h, --help
Print help (see a summary with '-h')
Solc Configuration:
--solc.version <VERSION>
Specifies the default version of the Solc compiler that should be used if there is no override specified by one of the test cases
[default: 0.8.29]
Resolc Configuration:
--resolc.path <resolc.path>
Specifies the path of the resolc compiler to be used by the tool.
If this is not specified, then the tool assumes that it should use the resolc binary that's provided in the user's $PATH.
[default: resolc]
Geth Configuration:
--geth.path <geth.path>
Specifies the path of the geth node to be used by the tool.
If this is not specified, then the tool assumes that it should use the geth binary that's provided in the user's $PATH.
[default: geth]
--geth.start-timeout-ms <geth.start-timeout-ms>
The amount of time to wait upon startup before considering that the node timed out
[default: 5000]
Kitchensink Configuration:
--kitchensink.path <kitchensink.path>
Specifies the path of the kitchensink node to be used by the tool.
If this is not specified, then the tool assumes that it should use the kitchensink binary that's provided in the user's $PATH.
[default: substrate-node]
--kitchensink.start-timeout-ms <kitchensink.start-timeout-ms>
The amount of time to wait upon startup before considering that the node timed out
[default: 5000]
--kitchensink.dont-use-dev-node
This configures the tool to use Kitchensink instead of using the revive-dev-node
Revive Dev Node Configuration:
--revive-dev-node.path <revive-dev-node.path>
Specifies the path of the revive dev node to be used by the tool.
If this is not specified, then the tool assumes that it should use the revive dev node binary that's provided in the user's $PATH.
[default: revive-dev-node]
--revive-dev-node.start-timeout-ms <revive-dev-node.start-timeout-ms>
The amount of time to wait upon startup before considering that the node timed out
[default: 5000]
Eth RPC Configuration:
--eth-rpc.path <eth-rpc.path>
Specifies the path of the ETH RPC to be used by the tool.
If this is not specified, then the tool assumes that it should use the ETH RPC binary that's provided in the user's $PATH.
[default: eth-rpc]
--eth-rpc.start-timeout-ms <eth-rpc.start-timeout-ms>
The amount of time to wait upon startup before considering that the node timed out
[default: 5000]
Genesis Configuration:
--genesis.path <genesis.path>
Specifies the path of the genesis file to use for the nodes that are started.
This is expected to be the path of a JSON geth genesis file.
Wallet Configuration:
--wallet.default-private-key <DEFAULT_KEY>
The private key of the default signer
[default: 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d]
--wallet.additional-keys <ADDITIONAL_KEYS>
This argument controls which private keys the nodes should have access to and be added to its wallet signers. With a value of N, private keys (0, N] will be added to the signer set of the node
[default: 100000]
Concurrency Configuration:
--concurrency.number-of-nodes <NUMBER_OF_NODES>
Determines the amount of nodes that will be spawned for each chain
[default: 5]
--concurrency.number-of-threads <NUMBER_OF_THREADS>
Determines the amount of tokio worker threads that will will be used
[default: 16]
--concurrency.number-of-concurrent-tasks <NUMBER_CONCURRENT_TASKS>
Determines the amount of concurrent tasks that will be spawned to run tests.
Defaults to 10 x the number of nodes.
--concurrency.ignore-concurrency-limit
Determines if the concurrency limit should be ignored or not
Compilation Configuration:
--compilation.invalidate-cache
Controls if the compilation cache should be invalidated or not
Report Configuration:
--report.include-compiler-input
Controls if the compiler input is included in the final report
--report.include-compiler-output
Controls if the compiler output is included in the final report
```
To run tests with this tool you need a corpus JSON file that defines the tests included in the corpus. The simplest corpus file looks like the following:
```json
{
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"path": "resolc-compiler-tests/fixtures/solidity"
}
```
> [!NOTE]
> Note that the tests can be found in the [`resolc-compiler-tests`](https://github.com/paritytech/resolc-compiler-tests) repository.
The above corpus file instructs the tool to look for all of the test cases contained within all of the metadata files of the specified directory.
The simplest command to run this tool is the following:
```bash
RUST_LOG="info" cargo run --release -- test \
--test ./resolc-compiler-tests/fixtures/solidity \
RUST_LOG="info" cargo run --release -- execute-tests \
--platform geth-evm-solc \
--corpus corp.json \
--working-directory workdir \
--concurrency.number-of-nodes 5 \
--concurrency.ignore-concurrency-limit \
> logs.log \
2> output.log
```
The above command will run the tool executing every one of the tests discovered in the path provided to the tool. All of the logs from the execution will be persisted in the `logs.log` file and all of the output of the tool will be persisted to the `output.log` file. If all that you're looking for is to run the tool and check which tests succeeded and failed, then the `output.log` file is what you need to be looking at. However, if you're contributing the to the tool then the `logs.log` file will be very valuable.
The above command will run the tool executing every one of the tests discovered in the path specified in the corpus file. All of the logs from the execution will be persisted in the `logs.log` file and all of the output of the tool will be persisted to the `output.log` file. If all that you're looking for is to run the tool and check which tests succeeded and failed, then the `output.log` file is what you need to be looking at. However, if you're contributing the to the tool then the `logs.log` file will be very valuable.
<details>
<summary>User Managed Nodes</summary>
If you only want to run a subset of tests, then you can specify that in your corpus file. The following is an example:
This section describes how the user can make use of nodes that they manage rather than allowing the tool to spawn and manage the nodes on the user's behalf.
> ⚠️ This is an advanced feature of the tool and could lead test successes or failures to not be reproducible. Please use this feature with caution and only if you understand the implications of running your own node instead of having the framework manage your nodes. ⚠️
If you're an advanced user and you'd like to manage your own nodes instead of having the tool initialize, spawn, and manage them, then you can choose to run your own nodes and then provide them to the tool to make use of just like the following:
```bash
#!/usr/bin/env bash
set -euo pipefail
PLATFORM="revive-dev-node-revm-solc"
retester export-genesis "$PLATFORM" > chainspec.json
# Start revive-dev-node in a detached tmux session
tmux new-session -d -s revive-dev-node \
'RUST_LOG="error,evm=debug,sc_rpc_server=info,runtime::revive=debug" revive-dev-node \
--dev \
--chain chainspec.json \
--force-authoring \
--rpc-methods Unsafe \
--rpc-cors all \
--rpc-max-connections 4294967295 \
--pool-limit 4294967295 \
--pool-kbytes 4294967295'
sleep 5
# Start eth-rpc in a detached tmux session
tmux new-session -d -s eth-rpc \
'RUST_LOG="info,eth-rpc=debug" eth-rpc \
--dev \
--node-rpc-url ws://127.0.0.1:9944 \
--rpc-max-connections 4294967295'
sleep 5
# Run the tests (logs to files as before)
RUST_LOG="info" retester test \
--platform "$PLATFORM" \
--corpus ./revive-differential-tests/fixtures/solidity \
--working-directory ./workdir \
--concurrency.number-of-nodes 1 \
--concurrency.number-of-concurrent-tasks 5 \
--revive-dev-node.existing-rpc-url "http://localhost:8545" \
> logs.log
```json
{
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"paths": [
"path/to/a/single/metadata/file/I/want/to/run.json",
"path/to/a/directory/to/find/all/metadata/files/within"
]
}
```
</details>
Binary file not shown.
-1
View File
@@ -14,7 +14,6 @@ anyhow = { workspace = true }
clap = { workspace = true }
moka = { workspace = true, features = ["sync"] }
once_cell = { workspace = true }
regex = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
+5 -3
View File
@@ -1,9 +1,11 @@
//! This module implements a cached file system allowing for results to be stored in-memory rather
//! rather being queried from the file system again.
use std::fs;
use std::io::{Error, Result};
use std::path::{Path, PathBuf};
use std::{
fs,
io::{Error, Result},
path::{Path, PathBuf},
};
use moka::sync::Cache;
use once_cell::sync::Lazy;
+1 -2
View File
@@ -1,5 +1,4 @@
use std::ops::ControlFlow;
use std::time::Duration;
use std::{ops::ControlFlow, time::Duration};
use anyhow::{Context as _, Result, anyhow};
@@ -135,6 +135,6 @@ macro_rules! define_wrapper_type {
};
}
/// Technically not needed but this allows for the macro to be found in the `macros` module of the
/// crate in addition to being found in the root of the crate.
/// Technically not needed but this allows for the macro to be found in the `macros` module of
/// the crate in addition to being found in the root of the crate.
pub use {define_wrapper_type, impl_for_wrapper};
-2
View File
@@ -1,13 +1,11 @@
mod identifiers;
mod mode;
mod parsed_test_specifier;
mod private_key_allocator;
mod round_robin_pool;
mod version_or_requirement;
pub use identifiers::*;
pub use mode::*;
pub use parsed_test_specifier::*;
pub use private_key_allocator::*;
pub use round_robin_pool::*;
pub use version_or_requirement::*;
+1 -268
View File
@@ -1,14 +1,7 @@
use crate::iterators::EitherIter;
use crate::types::VersionOrRequirement;
use anyhow::{Context as _, bail};
use regex::Regex;
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
use std::{fmt::Display, str::FromStr, sync::LazyLock};
/// This represents a mode that a given test should be run with, if possible.
///
@@ -38,19 +31,6 @@ impl Display for Mode {
}
}
impl FromStr for Mode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed_mode = ParsedMode::from_str(s)?;
let mut iter = parsed_mode.to_modes();
let (Some(mode), None) = (iter.next(), iter.next()) else {
bail!("Failed to parse the mode")
};
Ok(mode)
}
}
impl Mode {
/// Return all of the available mode combinations.
pub fn all() -> impl Iterator<Item = &'static Mode> {
@@ -189,250 +169,3 @@ impl ModeOptimizerSetting {
!matches!(self, ModeOptimizerSetting::M0)
}
}
/// This represents a mode that has been parsed from test metadata.
///
/// Mode strings can take the following form (in pseudo-regex):
///
/// ```text
/// [YEILV][+-]? (M[0123sz])? <semver>?
/// ```
///
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
#[serde(try_from = "String", into = "String")]
pub struct ParsedMode {
pub pipeline: Option<ModePipeline>,
pub optimize_flag: Option<bool>,
pub optimize_setting: Option<ModeOptimizerSetting>,
pub version: Option<semver::VersionReq>,
}
impl FromStr for ParsedMode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?x)
^
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
\s*
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
\s*
(?P<version>[>=<^]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
$
").unwrap()
});
let Some(caps) = REGEX.captures(s) else {
anyhow::bail!("Cannot parse mode '{s}' from string");
};
let pipeline = match caps.name("pipeline") {
Some(m) => Some(
ModePipeline::from_str(m.as_str())
.context("Failed to parse mode pipeline from string")?,
),
None => None,
};
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
let optimize_setting = match caps.name("optimize_setting") {
Some(m) => Some(
ModeOptimizerSetting::from_str(m.as_str())
.context("Failed to parse optimizer setting from string")?,
),
None => None,
};
let version = match caps.name("version") {
Some(m) => Some(
semver::VersionReq::parse(m.as_str())
.map_err(|e| {
anyhow::anyhow!(
"Cannot parse the version requirement '{}': {e}",
m.as_str()
)
})
.context("Failed to parse semver requirement from mode string")?,
),
None => None,
};
Ok(ParsedMode {
pipeline,
optimize_flag,
optimize_setting,
version,
})
}
}
impl Display for ParsedMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut has_written = false;
if let Some(pipeline) = self.pipeline {
pipeline.fmt(f)?;
if let Some(optimize_flag) = self.optimize_flag {
f.write_str(if optimize_flag { "+" } else { "-" })?;
}
has_written = true;
}
if let Some(optimize_setting) = self.optimize_setting {
if has_written {
f.write_str(" ")?;
}
optimize_setting.fmt(f)?;
has_written = true;
}
if let Some(version) = &self.version {
if has_written {
f.write_str(" ")?;
}
version.fmt(f)?;
}
Ok(())
}
}
impl From<ParsedMode> for String {
fn from(parsed_mode: ParsedMode) -> Self {
parsed_mode.to_string()
}
}
impl TryFrom<String> for ParsedMode {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
ParsedMode::from_str(&value)
}
}
impl ParsedMode {
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|| EitherIter::A(ModePipeline::test_cases()),
|p| EitherIter::B(std::iter::once(*p)),
);
let optimize_flag_setting = self.optimize_flag.map(|flag| {
if flag {
ModeOptimizerSetting::M3
} else {
ModeOptimizerSetting::M0
}
});
let optimize_flag_iter = match optimize_flag_setting {
Some(setting) => EitherIter::A(std::iter::once(setting)),
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
};
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|| EitherIter::A(optimize_flag_iter),
|s| EitherIter::B(std::iter::once(*s)),
);
pipeline_iter.flat_map(move |pipeline| {
optimize_settings_iter
.clone()
.map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: self.version.clone(),
})
})
}
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
/// This avoids any duplicate entries.
pub fn many_to_modes<'a>(
parsed: impl Iterator<Item = &'a ParsedMode>,
) -> impl Iterator<Item = Mode> {
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
modes.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parsed_mode_from_str() {
let strings = vec![
("Mz", "Mz"),
("Y", "Y"),
("Y+", "Y+"),
("Y-", "Y-"),
("E", "E"),
("E+", "E+"),
("E-", "E-"),
("Y M0", "Y M0"),
("Y M1", "Y M1"),
("Y M2", "Y M2"),
("Y M3", "Y M3"),
("Y Ms", "Y Ms"),
("Y Mz", "Y Mz"),
("E M0", "E M0"),
("E M1", "E M1"),
("E M2", "E M2"),
("E M3", "E M3"),
("E Ms", "E Ms"),
("E Mz", "E Mz"),
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
("Y 0.8.0", "Y ^0.8.0"),
("E+ 0.8.0", "E+ ^0.8.0"),
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
("E Mz <0.7.0", "E Mz <0.7.0"),
// We can parse +- _and_ M1/M2 but the latter takes priority.
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
("E- M2 0.7.0", "E- M2 ^0.7.0"),
// We don't see this in the wild but it is parsed.
("<=0.8", "<=0.8"),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
assert_eq!(
expected,
parsed.to_string(),
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
);
}
}
#[test]
fn test_parsed_mode_to_test_modes() {
let strings = vec![
("Mz", vec!["Y Mz", "E Mz"]),
("Y", vec!["Y M0", "Y M3"]),
("E", vec!["E M0", "E M3"]),
("Y+", vec!["Y M3"]),
("Y-", vec!["Y M0"]),
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
(
"<=0.8",
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
assert_eq!(
expected_set, actual_set,
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
);
}
}
}
@@ -1,133 +0,0 @@
use std::{fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Context as _, bail};
use crate::types::Mode;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ParsedTestSpecifier {
/// All of the test cases in the file should be ran across all of the specified modes
FileOrDirectory {
/// The path of the metadata file containing the test cases.
metadata_or_directory_file_path: PathBuf,
},
/// Only a specific case within the metadata file should be ran across all of the modes in the
/// file.
Case {
/// The path of the metadata file containing the test cases.
metadata_file_path: PathBuf,
/// The index of the specific case to run.
case_idx: usize,
},
/// A specific case and a specific mode should be ran. This is the most specific out of all of
/// the specifier types.
CaseWithMode {
/// The path of the metadata file containing the test cases.
metadata_file_path: PathBuf,
/// The index of the specific case to run.
case_idx: usize,
/// The parsed mode that the test should be run in.
mode: Mode,
},
}
impl Display for ParsedTestSpecifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParsedTestSpecifier::FileOrDirectory {
metadata_or_directory_file_path,
} => {
write!(f, "{}", metadata_or_directory_file_path.display())
}
ParsedTestSpecifier::Case {
metadata_file_path,
case_idx,
} => {
write!(f, "{}::{}", metadata_file_path.display(), case_idx)
}
ParsedTestSpecifier::CaseWithMode {
metadata_file_path,
case_idx,
mode,
} => {
write!(
f,
"{}::{}::{}",
metadata_file_path.display(),
case_idx,
mode
)
}
}
}
}
impl FromStr for ParsedTestSpecifier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split_iter = s.split("::");
let Some(path_string) = split_iter.next() else {
bail!("Could not find the path in the test specifier")
};
let path = PathBuf::from(path_string)
.canonicalize()
.context("Failed to canonicalize the path of the test")?;
let Some(case_idx_string) = split_iter.next() else {
return Ok(Self::FileOrDirectory {
metadata_or_directory_file_path: path,
});
};
let case_idx = usize::from_str(case_idx_string)
.context("Failed to parse the case idx of the test specifier from string")?;
// At this point the provided path must be a file.
if !path.is_file() {
bail!(
"Test specifier with a path and case idx must point to a file and not a directory"
)
}
let Some(mode_string) = split_iter.next() else {
return Ok(Self::Case {
metadata_file_path: path,
case_idx,
});
};
let mode = Mode::from_str(mode_string)
.context("Failed to parse the mode string in the parsed test specifier")?;
Ok(Self::CaseWithMode {
metadata_file_path: path,
case_idx,
mode,
})
}
}
impl From<ParsedTestSpecifier> for String {
fn from(value: ParsedTestSpecifier) -> Self {
value.to_string()
}
}
impl TryFrom<String> for ParsedTestSpecifier {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<&str> for ParsedTestSpecifier {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
@@ -1,5 +1,4 @@
use alloy::primitives::U256;
use alloy::signers::local::PrivateKeySigner;
use alloy::{primitives::U256, signers::local::PrivateKeySigner};
use anyhow::{Context, Result, bail};
/// This is a sequential private key allocator. When instantiated, it allocated private keys in
+1 -2
View File
@@ -10,8 +10,7 @@ use std::{
pin::Pin,
};
use alloy::json_abi::JsonAbi;
use alloy::primitives::Address;
use alloy::{json_abi::JsonAbi, primitives::Address};
use anyhow::{Context as _, Result};
use semver::Version;
use serde::{Deserialize, Serialize};
+37 -38
View File
@@ -253,45 +253,44 @@ impl SolidityCompiler for Resolc {
.evm
.and_then(|evm| evm.bytecode.clone())
.context("Unexpected - Contract compiled with resolc has no bytecode")?;
let abi = {
let metadata = contract_information
.metadata
.as_ref()
.context("No metadata found for the contract")?;
let solc_metadata_str = match metadata {
serde_json::Value::String(solc_metadata_str) => {
solc_metadata_str.as_str()
}
serde_json::Value::Object(metadata_object) => {
let solc_metadata_value = metadata_object
.get("solc_metadata")
.context("Contract doesn't have a 'solc_metadata' field")?;
solc_metadata_value
.as_str()
.context("The 'solc_metadata' field is not a string")?
}
serde_json::Value::Null
| serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::Array(_) => {
anyhow::bail!("Unsupported type of metadata {metadata:?}")
}
let abi =
{
let metadata = contract_information
.metadata
.as_ref()
.context("No metadata found for the contract")?;
let solc_metadata_str = match metadata {
serde_json::Value::String(solc_metadata_str) => {
solc_metadata_str.as_str()
}
serde_json::Value::Object(metadata_object) => {
let solc_metadata_value = metadata_object
.get("solc_metadata")
.context("Contract doesn't have a 'solc_metadata' field")?;
solc_metadata_value
.as_str()
.context("The 'solc_metadata' field is not a string")?
}
serde_json::Value::Null
| serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::Array(_) => {
anyhow::bail!("Unsupported type of metadata {metadata:?}")
}
};
let solc_metadata =
serde_json::from_str::<serde_json::Value>(solc_metadata_str).context(
"Failed to deserialize the solc_metadata as a serde_json generic value",
)?;
let output_value = solc_metadata
.get("output")
.context("solc_metadata doesn't have an output field")?;
let abi_value = output_value
.get("abi")
.context("solc_metadata output doesn't contain an abi field")?;
serde_json::from_value::<JsonAbi>(abi_value.clone())
.context("ABI found in solc_metadata output is not valid ABI")?
};
let solc_metadata = serde_json::from_str::<serde_json::Value>(
solc_metadata_str,
)
.context(
"Failed to deserialize the solc_metadata as a serde_json generic value",
)?;
let output_value = solc_metadata
.get("output")
.context("solc_metadata doesn't have an output field")?;
let abi_value = output_value
.get("abi")
.context("solc_metadata output doesn't contain an abi field")?;
serde_json::from_value::<JsonAbi>(abi_value.clone())
.context("ABI found in solc_metadata output is not valid ABI")?
};
map.insert(contract_name, (bytecode.object, abi));
}
}
+4 -4
View File
@@ -21,8 +21,7 @@ use foundry_compilers_artifacts::{
output_selection::{
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
},
solc::CompilerOutput as SolcOutput,
solc::*,
solc::{CompilerOutput as SolcOutput, *},
};
use semver::Version;
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
@@ -284,8 +283,9 @@ impl SolidityCompiler for Solc {
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support
// mode E (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler
// is new enough.
pipeline == ModePipeline::ViaEVMAssembly
|| (pipeline == ModePipeline::ViaYulIR && self.compiler_supports_yul())
}
@@ -7,10 +7,7 @@ pragma solidity >=0.6.9;
import "./callable.sol";
contract Main {
function main(
uint[1] calldata p1,
Callable callable
) public pure returns (uint) {
function main(uint[1] calldata p1, Callable callable) public returns(uint) {
return callable.f(p1);
}
}
-1
View File
@@ -18,7 +18,6 @@ semver = { workspace = true }
temp-dir = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true }
[lints]
+91 -393
View File
@@ -12,18 +12,19 @@ use std::{
use alloy::{
genesis::Genesis,
hex::ToHexExt,
network::EthereumWallet,
primitives::{B256, FixedBytes, U256},
primitives::{FixedBytes, U256},
signers::local::PrivateKeySigner,
};
use clap::{Parser, ValueEnum, ValueHint};
use revive_dt_common::types::{ParsedTestSpecifier, PlatformIdentifier};
use revive_dt_common::types::PlatformIdentifier;
use semver::Version;
use serde::{Deserialize, Serialize, Serializer};
use serde::{Serialize, Serializer};
use strum::{AsRefStr, Display, EnumString, IntoStaticStr};
use temp_dir::TempDir;
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
#[command(name = "retester")]
pub enum Context {
/// Executes tests in the MatterLabs format differentially on multiple targets concurrently.
@@ -34,9 +35,6 @@ pub enum Context {
/// Exports the JSON schema of the MatterLabs test format used by the tool.
ExportJsonSchema,
/// Exports the genesis file of the desired platform.
ExportGenesis(Box<ExportGenesisContext>),
}
impl Context {
@@ -47,15 +45,6 @@ impl Context {
pub fn report_configuration(&self) -> &ReportConfiguration {
self.as_ref()
}
pub fn update_for_profile(&mut self) {
match self {
Context::Test(ctx) => ctx.update_for_profile(),
Context::Benchmark(ctx) => ctx.update_for_profile(),
Context::ExportJsonSchema => {}
Context::ExportGenesis(..) => {}
}
}
}
impl AsRef<WorkingDirectoryConfiguration> for Context {
@@ -63,7 +52,7 @@ impl AsRef<WorkingDirectoryConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -73,7 +62,7 @@ impl AsRef<CorpusConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -83,7 +72,7 @@ impl AsRef<SolcConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -93,7 +82,7 @@ impl AsRef<ResolcConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -103,7 +92,6 @@ impl AsRef<GethConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportGenesis(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -114,7 +102,6 @@ impl AsRef<KurtosisConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportGenesis(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -125,7 +112,6 @@ impl AsRef<PolkadotParachainConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportGenesis(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -136,7 +122,6 @@ impl AsRef<KitchensinkConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportGenesis(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -147,7 +132,6 @@ impl AsRef<ReviveDevNodeConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportGenesis(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -158,7 +142,7 @@ impl AsRef<EthRpcConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -167,7 +151,7 @@ impl AsRef<GenesisConfiguration> for Context {
fn as_ref(&self) -> &GenesisConfiguration {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(..) | Self::ExportGenesis(..) => {
Self::Benchmark(..) => {
static GENESIS: LazyLock<GenesisConfiguration> = LazyLock::new(Default::default);
&GENESIS
}
@@ -181,7 +165,6 @@ impl AsRef<WalletConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportGenesis(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema => unreachable!(),
}
}
@@ -192,7 +175,7 @@ impl AsRef<ConcurrencyConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -202,7 +185,7 @@ impl AsRef<CompilationConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
@@ -212,27 +195,25 @@ impl AsRef<ReportConfiguration> for Context {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(context) => context.as_ref().as_ref(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
Self::ExportJsonSchema => unreachable!(),
}
}
}
impl AsRef<IgnoreSuccessConfiguration> for Context {
fn as_ref(&self) -> &IgnoreSuccessConfiguration {
match self {
Self::Test(context) => context.as_ref().as_ref(),
Self::Benchmark(..) => unreachable!(),
Self::ExportJsonSchema | Self::ExportGenesis(..) => unreachable!(),
}
}
}
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct TestExecutionContext {
/// The commandline profile to use. Different profiles change the defaults of the various cli
/// arguments.
#[arg(long = "profile", default_value_t = Profile::Default)]
pub profile: Profile,
/// The working directory that the program will use for all of the temporary artifacts needed
/// at runtime.
///
/// If not specified, then a temporary directory will be created and used by the program for
/// all temporary artifacts.
#[clap(
short,
long,
default_value = "",
value_hint = ValueHint::DirPath,
)]
pub working_directory: WorkingDirectoryConfiguration,
/// The set of platforms that the differential tests should run on.
#[arg(
@@ -242,23 +223,6 @@ pub struct TestExecutionContext {
)]
pub platforms: Vec<PlatformIdentifier>,
/// The output format to use for the tool's output.
#[arg(short, long, default_value_t = OutputFormat::CargoTestLike)]
pub output_format: OutputFormat,
/// The working directory that the program will use for all of the temporary artifacts needed at
/// runtime.
///
/// If not specified, then a temporary directory will be created and used by the program for all
/// temporary artifacts.
#[clap(
short,
long,
default_value = "",
value_hint = ValueHint::DirPath,
)]
pub working_directory: WorkingDirectoryConfiguration,
/// Configuration parameters for the corpus files to use.
#[clap(flatten, next_help_heading = "Corpus Configuration")]
pub corpus_configuration: CorpusConfiguration,
@@ -314,60 +278,15 @@ pub struct TestExecutionContext {
/// Configuration parameters for the report.
#[clap(flatten, next_help_heading = "Report Configuration")]
pub report_configuration: ReportConfiguration,
/// Configuration parameters for ignoring certain test cases based on the report
#[clap(flatten, next_help_heading = "Ignore Success Configuration")]
pub ignore_success_configuration: IgnoreSuccessConfiguration,
}
impl TestExecutionContext {
pub fn update_for_profile(&mut self) {
match self.profile {
Profile::Default => {}
Profile::Debug => {
let default_concurrency_config =
ConcurrencyConfiguration::parse_from(["concurrency-configuration"]);
let working_directory_config = WorkingDirectoryConfiguration::default();
if self.concurrency_configuration.number_of_nodes
== default_concurrency_config.number_of_nodes
{
self.concurrency_configuration.number_of_nodes = 1;
}
if self.concurrency_configuration.number_of_threads
== default_concurrency_config.number_of_threads
{
self.concurrency_configuration.number_of_threads = 5;
}
if self.concurrency_configuration.number_concurrent_tasks
== default_concurrency_config.number_concurrent_tasks
{
self.concurrency_configuration.number_concurrent_tasks = 1;
}
if working_directory_config == self.working_directory {
let home_directory =
PathBuf::from(std::env::var("HOME").expect("Home dir not found"));
let working_directory = home_directory.join(".retester-workdir");
self.working_directory = WorkingDirectoryConfiguration::Path(working_directory)
}
}
}
}
}
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct BenchmarkingContext {
/// The commandline profile to use. Different profiles change the defaults of the various cli
/// arguments.
#[arg(long = "profile", default_value_t = Profile::Default)]
pub profile: Profile,
/// The working directory that the program will use for all of the temporary artifacts needed at
/// runtime.
/// The working directory that the program will use for all of the temporary artifacts needed
/// at runtime.
///
/// If not specified, then a temporary directory will be created and used by the program for all
/// temporary artifacts.
/// If not specified, then a temporary directory will be created and used by the program for
/// all temporary artifacts.
#[clap(
short,
long,
@@ -442,72 +361,6 @@ pub struct BenchmarkingContext {
pub report_configuration: ReportConfiguration,
}
impl BenchmarkingContext {
pub fn update_for_profile(&mut self) {
match self.profile {
Profile::Default => {}
Profile::Debug => {
let default_concurrency_config =
ConcurrencyConfiguration::parse_from(["concurrency-configuration"]);
let working_directory_config = WorkingDirectoryConfiguration::default();
if self.concurrency_configuration.number_of_nodes
== default_concurrency_config.number_of_nodes
{
self.concurrency_configuration.number_of_nodes = 1;
}
if self.concurrency_configuration.number_of_threads
== default_concurrency_config.number_of_threads
{
self.concurrency_configuration.number_of_threads = 5;
}
if self.concurrency_configuration.number_concurrent_tasks
== default_concurrency_config.number_concurrent_tasks
{
self.concurrency_configuration.number_concurrent_tasks = 1;
}
if working_directory_config == self.working_directory {
let home_directory =
PathBuf::from(std::env::var("HOME").expect("Home dir not found"));
let working_directory = home_directory.join(".retester-workdir");
self.working_directory = WorkingDirectoryConfiguration::Path(working_directory)
}
}
}
}
}
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
pub struct ExportGenesisContext {
/// The platform of choice to export the genesis for.
pub platform: PlatformIdentifier,
/// Configuration parameters for the geth node.
#[clap(flatten, next_help_heading = "Geth Configuration")]
pub geth_configuration: GethConfiguration,
/// Configuration parameters for the lighthouse node.
#[clap(flatten, next_help_heading = "Lighthouse Configuration")]
pub lighthouse_configuration: KurtosisConfiguration,
/// Configuration parameters for the Kitchensink.
#[clap(flatten, next_help_heading = "Kitchensink Configuration")]
pub kitchensink_configuration: KitchensinkConfiguration,
/// Configuration parameters for the Polkadot Parachain.
#[clap(flatten, next_help_heading = "Polkadot Parachain Configuration")]
pub polkadot_parachain_configuration: PolkadotParachainConfiguration,
/// Configuration parameters for the Revive Dev Node.
#[clap(flatten, next_help_heading = "Revive Dev Node Configuration")]
pub revive_dev_node_configuration: ReviveDevNodeConfiguration,
/// Configuration parameters for the wallet.
#[clap(flatten, next_help_heading = "Wallet Configuration")]
pub wallet_configuration: WalletConfiguration,
}
impl Default for TestExecutionContext {
fn default() -> Self {
Self::parse_from(["execution-context"])
@@ -604,15 +457,9 @@ impl AsRef<ReportConfiguration> for TestExecutionContext {
}
}
impl AsRef<IgnoreSuccessConfiguration> for TestExecutionContext {
fn as_ref(&self) -> &IgnoreSuccessConfiguration {
&self.ignore_success_configuration
}
}
impl Default for BenchmarkingContext {
fn default() -> Self {
Self::parse_from(["benchmarking-context"])
Self::parse_from(["execution-context"])
}
}
@@ -700,71 +547,16 @@ impl AsRef<ReportConfiguration> for BenchmarkingContext {
}
}
impl Default for ExportGenesisContext {
fn default() -> Self {
Self::parse_from(["export-genesis-context"])
}
}
impl AsRef<GethConfiguration> for ExportGenesisContext {
fn as_ref(&self) -> &GethConfiguration {
&self.geth_configuration
}
}
impl AsRef<KurtosisConfiguration> for ExportGenesisContext {
fn as_ref(&self) -> &KurtosisConfiguration {
&self.lighthouse_configuration
}
}
impl AsRef<KitchensinkConfiguration> for ExportGenesisContext {
fn as_ref(&self) -> &KitchensinkConfiguration {
&self.kitchensink_configuration
}
}
impl AsRef<PolkadotParachainConfiguration> for ExportGenesisContext {
fn as_ref(&self) -> &PolkadotParachainConfiguration {
&self.polkadot_parachain_configuration
}
}
impl AsRef<ReviveDevNodeConfiguration> for ExportGenesisContext {
fn as_ref(&self) -> &ReviveDevNodeConfiguration {
&self.revive_dev_node_configuration
}
}
impl AsRef<WalletConfiguration> for ExportGenesisContext {
fn as_ref(&self) -> &WalletConfiguration {
&self.wallet_configuration
}
}
/// A set of configuration parameters for the corpus files to use for the execution.
#[serde_with::serde_as]
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct CorpusConfiguration {
/// A list of test specifiers for the tests that the tool should run.
///
/// Test specifiers follow the following format:
///
/// - `{directory_path|metadata_file_path}`: A path to a metadata file where all of the cases
/// live and should be run. Alternatively, it points to a directory instructing the framework
/// to discover of the metadata files that live there an execute them.
/// - `{metadata_file_path}::{case_idx}`: The path to a metadata file and then a case idx
/// separated by two colons. This specifies that only this specific test case within the
/// metadata file should be executed.
/// - `{metadata_file_path}::{case_idx}::{mode}`: This is very similar to the above specifier
/// with the exception that in this case the mode is specified and will be used in the test.
#[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
#[arg(short = 't', long = "test")]
pub test_specifiers: Vec<ParsedTestSpecifier>,
/// A list of test corpus JSON files to be tested.
#[arg(short = 'c', long = "corpus")]
pub paths: Vec<PathBuf>,
}
/// A set of configuration parameters for Solc.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct SolcConfiguration {
/// Specifies the default version of the Solc compiler that should be used if there is no
/// override specified by one of the test cases.
@@ -773,7 +565,7 @@ pub struct SolcConfiguration {
}
/// A set of configuration parameters for Resolc.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct ResolcConfiguration {
/// Specifies the path of the resolc compiler to be used by the tool.
///
@@ -784,12 +576,12 @@ pub struct ResolcConfiguration {
}
/// A set of configuration parameters for Polkadot Parachain.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct PolkadotParachainConfiguration {
/// Specifies the path of the polkadot-parachain node to be used by the tool.
///
/// If this is not specified, then the tool assumes that it should use the polkadot-parachain binary
/// that's provided in the user's $PATH.
/// If this is not specified, then the tool assumes that it should use the polkadot-parachain
/// binary that's provided in the user's $PATH.
#[clap(
id = "polkadot-parachain.path",
long = "polkadot-parachain.path",
@@ -808,7 +600,7 @@ pub struct PolkadotParachainConfiguration {
}
/// A set of configuration parameters for Geth.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct GethConfiguration {
/// Specifies the path of the geth node to be used by the tool.
///
@@ -828,12 +620,12 @@ pub struct GethConfiguration {
}
/// A set of configuration parameters for kurtosis.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct KurtosisConfiguration {
/// Specifies the path of the kurtosis node to be used by the tool.
///
/// If this is not specified, then the tool assumes that it should use the kurtosis binary that's
/// provided in the user's $PATH.
/// If this is not specified, then the tool assumes that it should use the kurtosis binary
/// that's provided in the user's $PATH.
#[clap(
id = "kurtosis.path",
long = "kurtosis.path",
@@ -843,7 +635,7 @@ pub struct KurtosisConfiguration {
}
/// A set of configuration parameters for Kitchensink.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct KitchensinkConfiguration {
/// Specifies the path of the kitchensink node to be used by the tool.
///
@@ -867,12 +659,12 @@ pub struct KitchensinkConfiguration {
}
/// A set of configuration parameters for the revive dev node.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct ReviveDevNodeConfiguration {
/// Specifies the path of the revive dev node to be used by the tool.
///
/// If this is not specified, then the tool assumes that it should use the revive dev node binary
/// that's provided in the user's $PATH.
/// If this is not specified, then the tool assumes that it should use the revive dev node
/// binary that's provided in the user's $PATH.
#[clap(
id = "revive-dev-node.path",
long = "revive-dev-node.path",
@@ -888,36 +680,10 @@ pub struct ReviveDevNodeConfiguration {
value_parser = parse_duration
)]
pub start_timeout_ms: Duration,
/// The consensus to use for the spawned revive-dev-node.
#[clap(
id = "revive-dev-node.consensus",
long = "revive-dev-node.consensus",
default_value = "instant-seal"
)]
pub consensus: String,
/// Specifies the connection string of an existing node that's not managed by the framework.
///
/// If this argument is specified then the framework will not spawn certain nodes itself but
/// rather it will opt to using the existing node's through their provided connection strings.
///
/// This means that if `ConcurrencyConfiguration.number_of_nodes` is 10 and we only specify the
/// connection strings of 2 nodes here, then nodes 0 and 1 will use the provided connection
/// strings and nodes 2 through 10 (exclusive) will all be spawned and managed by the framework.
///
/// Thus, if you want all of the transactions and tests to happen against the node that you
/// spawned and manage then you need to specify a `ConcurrencyConfiguration.number_of_nodes` of
/// 1.
#[clap(
id = "revive-dev-node.existing-rpc-url",
long = "revive-dev-node.existing-rpc-url"
)]
pub existing_rpc_url: Vec<String>,
}
/// A set of configuration parameters for the ETH RPC.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct EthRpcConfiguration {
/// Specifies the path of the ETH RPC to be used by the tool.
///
@@ -937,7 +703,7 @@ pub struct EthRpcConfiguration {
}
/// A set of configuration parameters for the genesis.
#[derive(Clone, Debug, Default, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Parser, Serialize)]
pub struct GenesisConfiguration {
/// Specifies the path of the genesis file to use for the nodes that are started.
///
@@ -975,14 +741,15 @@ impl GenesisConfiguration {
}
/// A set of configuration parameters for the wallet.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct WalletConfiguration {
/// The private key of the default signer.
#[clap(
long = "wallet.default-private-key",
default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"
)]
default_key: B256,
#[serde(serialize_with = "serialize_private_key")]
default_key: PrivateKeySigner,
/// This argument controls which private keys the nodes should have access to and be added to
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
@@ -1000,8 +767,7 @@ impl WalletConfiguration {
pub fn wallet(&self) -> Arc<EthereumWallet> {
self.wallet
.get_or_init(|| {
let mut wallet =
EthereumWallet::new(PrivateKeySigner::from_bytes(&self.default_key).unwrap());
let mut wallet = EthereumWallet::new(self.default_key.clone());
for signer in (1..=self.additional_keys)
.map(|id| U256::from(id))
.map(|id| id.to_be_bytes::<32>())
@@ -1019,8 +785,15 @@ impl WalletConfiguration {
}
}
fn serialize_private_key<S>(value: &PrivateKeySigner, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
value.to_bytes().encode_hex().serialize(serializer)
}
/// A set of configuration for concurrency.
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct ConcurrencyConfiguration {
/// Determines the amount of nodes that will be spawned for each chain.
#[clap(long = "concurrency.number-of-nodes", default_value_t = 5)]
@@ -1030,38 +803,42 @@ pub struct ConcurrencyConfiguration {
#[arg(
long = "concurrency.number-of-threads",
default_value_t = std::thread::available_parallelism()
.map(|n| n.get() * 4 / 6)
.map(|n| n.get())
.unwrap_or(1)
)]
pub number_of_threads: usize,
/// Determines the amount of concurrent tasks that will be spawned to run tests. This means that
/// at any given time there is `concurrency.number-of-concurrent-tasks` tests concurrently
/// executing.
/// Determines the amount of concurrent tasks that will be spawned to run tests.
///
/// Note that a task limit of `0` means no limit on the number of concurrent tasks.
#[arg(long = "concurrency.number-of-concurrent-tasks", default_value_t = 500)]
number_concurrent_tasks: usize,
/// Defaults to 10 x the number of nodes.
#[arg(long = "concurrency.number-of-concurrent-tasks")]
number_concurrent_tasks: Option<usize>,
/// Determines if the concurrency limit should be ignored or not.
#[arg(long = "concurrency.ignore-concurrency-limit")]
ignore_concurrency_limit: bool,
}
impl ConcurrencyConfiguration {
pub fn concurrency_limit(&self) -> Option<usize> {
if self.number_concurrent_tasks == 0 {
None
} else {
Some(self.number_concurrent_tasks)
match self.ignore_concurrency_limit {
true => None,
false => Some(
self.number_concurrent_tasks
.unwrap_or(20 * self.number_of_nodes),
),
}
}
}
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct CompilationConfiguration {
/// Controls if the compilation cache should be invalidated or not.
#[arg(long = "compilation.invalidate-cache")]
pub invalidate_compilation_cache: bool,
}
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
#[derive(Clone, Debug, Parser, Serialize)]
pub struct ReportConfiguration {
/// Controls if the compiler input is included in the final report.
#[clap(long = "report.include-compiler-input")]
@@ -1072,15 +849,8 @@ pub struct ReportConfiguration {
pub include_compiler_output: bool,
}
#[derive(Clone, Debug, Parser, Serialize, Deserialize)]
pub struct IgnoreSuccessConfiguration {
/// The path of the report generated by the tool to use to ignore the cases that succeeded.
#[clap(long = "ignore-success.report-path")]
pub path: Option<PathBuf>,
}
/// Represents the working directory that the program uses.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub enum WorkingDirectoryConfiguration {
/// A temporary directory is used as the working directory. This will be removed when dropped.
TemporaryDirectory(Arc<TempDir>),
@@ -1088,24 +858,6 @@ pub enum WorkingDirectoryConfiguration {
Path(PathBuf),
}
impl Serialize for WorkingDirectoryConfiguration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.as_path().serialize(serializer)
}
}
impl<'a> Deserialize<'a> for WorkingDirectoryConfiguration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
PathBuf::deserialize(deserializer).map(Self::Path)
}
}
impl WorkingDirectoryConfiguration {
pub fn as_path(&self) -> &Path {
self.as_ref()
@@ -1155,6 +907,15 @@ impl Display for WorkingDirectoryConfiguration {
}
}
impl Serialize for WorkingDirectoryConfiguration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.as_path().serialize(serializer)
}
}
fn parse_duration(s: &str) -> anyhow::Result<Duration> {
u64::from_str(s)
.map(Duration::from_millis)
@@ -1189,66 +950,3 @@ pub enum TestingPlatform {
/// A polkadot/Substrate based network
Zombienet,
}
/// The output format to use for the test execution output.
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
ValueEnum,
EnumString,
Display,
AsRefStr,
IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
pub enum OutputFormat {
/// The legacy format that was used in the past for the output.
Legacy,
/// An output format that looks heavily resembles the output from `cargo test`.
CargoTestLike,
}
/// Command line profiles used to override the default values provided for the commands.
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
ValueEnum,
EnumString,
Display,
AsRefStr,
IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
pub enum Profile {
/// The default profile used by the framework. This profile is optimized to make the test
/// and workload execution happen as fast as possible.
#[default]
Default,
/// A debug profile optimized for use cases when certain tests are being debugged. This profile
/// sets up the framework with the following:
///
/// * `concurrency.number-of-nodes` set to 1 node.
/// * `concurrency.number-of-concurrent-tasks` set to 1 such that tests execute sequentially.
/// * `concurrency.number-of-threads` set to 5.
/// * `working-directory` set to ~/.retester-workdir
Debug,
}
-1
View File
@@ -21,7 +21,6 @@ revive-dt-node = { workspace = true }
revive-dt-node-interaction = { workspace = true }
revive-dt-report = { workspace = true }
ansi_term = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
bson = { workspace = true }
+118 -59
View File
@@ -22,7 +22,6 @@ use alloy::{
},
};
use anyhow::{Context as _, Result, bail};
use futures::TryFutureExt;
use indexmap::IndexMap;
use revive_dt_common::{
futures::{PollingWaitBehavior, poll},
@@ -31,12 +30,12 @@ use revive_dt_common::{
use revive_dt_format::{
metadata::{ContractInstance, ContractPathAndIdent},
steps::{
AllocateAccountStep, Calldata, EtherValue, FunctionCallStep, Method, RepeatStep, Step,
StepIdx, StepPath,
AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, FunctionCallStep, Method,
RepeatStep, Step, StepAddress, StepIdx, StepPath, StorageEmptyAssertionStep,
},
traits::{ResolutionContext, ResolverApi},
};
use tokio::sync::{Mutex, OnceCell, mpsc::UnboundedSender};
use tokio::sync::{Mutex, mpsc::UnboundedSender};
use tracing::{Instrument, Span, debug, error, field::display, info, info_span, instrument};
use crate::{
@@ -60,8 +59,8 @@ pub struct Driver<'a, I> {
/// The definition of the test that the driver is instructed to execute.
test_definition: &'a TestDefinition<'a>,
/// The private key allocator used by this driver and other drivers when account allocations are
/// needed.
/// The private key allocator used by this driver and other drivers when account allocations
/// are needed.
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
/// The execution state associated with the platform.
@@ -124,7 +123,13 @@ where
&self.platform_information.reporter,
)
.await
.inspect_err(|err| error!(?err, "Pre-linking compilation failed"))
.inspect_err(|err| {
error!(
?err,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Pre-linking compilation failed"
)
})
.context("Failed to produce the pre-linking compiled contracts")?;
let mut deployed_libraries = None::<HashMap<_, _>>;
@@ -132,7 +137,13 @@ where
.test_definition
.metadata
.contract_sources()
.inspect_err(|err| error!(?err, "Failed to retrieve contract sources from metadata"))
.inspect_err(|err| {
error!(
?err,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Failed to retrieve contract sources from metadata"
)
})
.context("Failed to get the contract instances from the metadata file")?;
for library_instance in self
.test_definition
@@ -180,19 +191,20 @@ where
TransactionRequest::default().from(deployer_address),
code,
);
let receipt = self
.execute_transaction(tx)
.and_then(|(_, receipt_fut)| receipt_fut)
.await
.inspect_err(|err| {
error!(
?err,
%library_instance,
"Failed to deploy the library"
)
})?;
let receipt = self.execute_transaction(tx).await.inspect_err(|err| {
error!(
?err,
%library_instance,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Failed to deploy the library"
)
})?;
debug!(?library_instance, "Deployed library");
debug!(
?library_instance,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Deployed library"
);
let library_address = receipt
.contract_address
@@ -215,7 +227,13 @@ where
&self.platform_information.reporter,
)
.await
.inspect_err(|err| error!(?err, "Post-linking compilation failed"))
.inspect_err(|err| {
error!(
?err,
platform_identifier = %self.platform_information.platform.platform_identifier(),
"Post-linking compilation failed"
)
})
.context("Failed to compile the post-link contracts")?;
self.execution_state = ExecutionState::new(
@@ -251,6 +269,7 @@ where
skip_all,
fields(
driver_id = self.driver_id,
platform_identifier = %self.platform_information.platform.platform_identifier(),
%step_path,
),
err(Debug),
@@ -286,11 +305,15 @@ where
.handle_function_call_contract_deployment(step)
.await
.context("Failed to deploy contracts for the function call step")?;
let transaction_hash = self
let execution_receipt = self
.handle_function_call_execution(step, deployment_receipts)
.await
.context("Failed to handle the function call execution")?;
self.handle_function_call_variable_assignment(step, transaction_hash)
let tracing_result = self
.handle_function_call_call_frame_tracing(execution_receipt.transaction_hash)
.await
.context("Failed to handle the function call call frame tracing")?;
self.handle_function_call_variable_assignment(step, &tracing_result)
.await
.context("Failed to handle function call variable assignment")?;
Ok(1)
@@ -344,19 +367,18 @@ where
&mut self,
step: &FunctionCallStep,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
) -> Result<TxHash> {
) -> Result<TransactionReceipt> {
match step.method {
// This step was already executed when `handle_step` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.remove(&step.instance)
.context("Failed to find deployment receipt for constructor call")
.map(|receipt| receipt.transaction_hash),
.context("Failed to find deployment receipt for constructor call"),
Method::Fallback | Method::FunctionName(_) => {
let tx = step
.as_transaction(self.resolver.as_ref(), self.default_resolution_context())
.await?;
Ok(self.execute_transaction(tx).await?.0)
self.execute_transaction(tx).await
}
}
}
@@ -395,19 +417,15 @@ where
async fn handle_function_call_variable_assignment(
&mut self,
step: &FunctionCallStep,
tx_hash: TxHash,
tracing_result: &CallFrame,
) -> Result<()> {
let Some(ref assignments) = step.variable_assignments else {
return Ok(());
};
// Handling the return data variable assignments.
let callframe = OnceCell::new();
for (variable_name, output_word) in assignments.return_data.iter().zip(
callframe
.get_or_try_init(|| self.handle_function_call_call_frame_tracing(tx_hash))
.await
.context("Failed to get the callframe trace for transaction")?
tracing_result
.output
.as_ref()
.unwrap_or_default()
@@ -428,6 +446,26 @@ where
Ok(())
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id))]
pub async fn execute_balance_assertion(
&mut self,
_: &StepPath,
_: &BalanceAssertionStep,
) -> anyhow::Result<usize> {
// Kept empty intentionally for the benchmark driver.
Ok(1)
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
async fn execute_storage_empty_assertion_step(
&mut self,
_: &StepPath,
_: &StorageEmptyAssertionStep,
) -> Result<usize> {
// Kept empty intentionally for the benchmark driver.
Ok(1)
}
#[instrument(level = "info", skip_all, fields(driver_id = self.driver_id), err(Debug))]
async fn execute_repeat_step(
&mut self,
@@ -509,6 +547,7 @@ where
skip_all,
fields(
driver_id = self.driver_id,
platform_identifier = %self.platform_information.platform.platform_identifier(),
%contract_instance,
%deployer
),
@@ -551,6 +590,7 @@ where
skip_all,
fields(
driver_id = self.driver_id,
platform_identifier = %self.platform_information.platform.platform_identifier(),
%contract_instance,
%deployer
),
@@ -620,11 +660,7 @@ where
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match self
.execute_transaction(tx)
.and_then(|(_, receipt_fut)| receipt_fut)
.await
{
let receipt = match self.execute_transaction(tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(?error, "Contract deployment transaction failed.");
@@ -651,6 +687,33 @@ where
Ok((address, abi, receipt))
}
#[instrument(level = "info", fields(driver_id = self.driver_id), skip_all)]
async fn step_address_auto_deployment(
&mut self,
step_address: &StepAddress,
) -> Result<Address> {
match step_address {
StepAddress::Address(address) => Ok(*address),
StepAddress::ResolvableAddress(resolvable) => {
let Some(instance) = resolvable
.strip_suffix(".address")
.map(ContractInstance::new)
else {
bail!("Not an address variable");
};
self.get_or_deploy_contract_instance(
&instance,
FunctionCallStep::default_caller_address(),
None,
None,
)
.await
.map(|v| v.0)
}
}
}
// endregion:Contract Deployment
// region:Resolution & Resolver
@@ -671,7 +734,7 @@ where
async fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> anyhow::Result<(TxHash, impl Future<Output = Result<TransactionReceipt>>)> {
) -> anyhow::Result<TransactionReceipt> {
let node = self.platform_information.node;
let transaction_hash = node
.submit_transaction(transaction)
@@ -684,28 +747,24 @@ where
.send(WatcherEvent::SubmittedTransaction { transaction_hash })
.context("Failed to send the transaction hash to the watcher")?;
Ok((transaction_hash, async move {
info!("Starting to poll for transaction receipt");
poll(
Duration::from_secs(30 * 60),
PollingWaitBehavior::Constant(Duration::from_secs(1)),
|| {
async move {
match node.get_receipt(transaction_hash).await {
Ok(receipt) => {
info!("Polling succeeded, receipt found");
Ok(ControlFlow::Break(receipt))
}
Err(_) => Ok(ControlFlow::Continue(())),
info!("Starting to poll for transaction receipt");
poll(
Duration::from_secs(30 * 60),
PollingWaitBehavior::Constant(Duration::from_secs(1)),
|| {
async move {
match node.get_receipt(transaction_hash).await {
Ok(receipt) => {
info!("Polling succeeded, receipt found");
Ok(ControlFlow::Break(receipt))
}
Err(_) => Ok(ControlFlow::Continue(())),
}
.instrument(info_span!("Polling for receipt"))
},
)
.instrument(info_span!("Polling for receipt", %transaction_hash))
.await
.inspect(|_| info!("Found the transaction receipt"))
}))
}
.instrument(info_span!("Polling for receipt"))
},
)
.await
}
// endregion:Transaction Execution
}
@@ -6,19 +6,16 @@ use anyhow::Context as _;
use futures::{FutureExt, StreamExt};
use revive_dt_common::types::PrivateKeyAllocator;
use revive_dt_core::Platform;
use revive_dt_format::{
corpus::Corpus,
steps::{Step, StepIdx, StepPath},
};
use revive_dt_format::steps::{Step, StepIdx, StepPath};
use tokio::sync::Mutex;
use tracing::{Instrument, error, info, info_span, instrument, warn};
use tracing::{error, info, info_span, instrument, warn};
use revive_dt_config::{BenchmarkingContext, Context};
use revive_dt_report::Reporter;
use crate::{
differential_benchmarks::{Driver, Watcher, WatcherEvent},
helpers::{CachedCompiler, NodePool, create_test_definitions_stream},
helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream},
};
/// Handles the differential testing executing it according to the information defined in the
@@ -42,17 +39,9 @@ pub async fn handle_differential_benchmarks(
let full_context = Context::Benchmark(Box::new(context.clone()));
// Discover all of the metadata files that are defined in the context.
let corpus = context
.corpus_configuration
.test_specifiers
.clone()
.into_iter()
.try_fold(Corpus::default(), Corpus::with_test_specifier)
.context("Failed to parse the test corpus")?;
info!(
len = corpus.metadata_file_count(),
"Discovered metadata files"
);
let metadata_files = collect_metadata_files(&context)
.context("Failed to collect metadata files for differential testing")?;
info!(len = metadata_files.len(), "Discovered metadata files");
// Discover the list of platforms that the tests should run on based on the context.
let platforms = context
@@ -95,9 +84,8 @@ pub async fn handle_differential_benchmarks(
// Preparing test definitions for the execution.
let test_definitions = create_test_definitions_stream(
&full_context,
&corpus,
metadata_files.iter(),
&platforms_and_nodes,
None,
reporter.clone(),
)
.await
@@ -171,15 +159,12 @@ pub async fn handle_differential_benchmarks(
futures::future::try_join(
watcher.run(),
driver
.execute_all()
.instrument(info_span!("Executing Benchmarks", %platform_identifier))
.inspect(|_| {
info!("All transactions submitted - driver completed execution");
watcher_tx
.send(WatcherEvent::AllTransactionsSubmitted)
.unwrap()
}),
driver.execute_all().inspect(|_| {
info!("All transactions submitted - driver completed execution");
watcher_tx
.send(WatcherEvent::AllTransactionsSubmitted)
.unwrap()
}),
)
.await
.context("Failed to run the driver and executor")
@@ -10,14 +10,15 @@ use revive_dt_format::metadata::{ContractIdent, ContractInstance};
#[derive(Clone)]
/// The state associated with the test execution of one of the workloads.
pub struct ExecutionState {
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
/// against them and therefore they're ready to be deployed on-demand.
/// The compiled contracts, these contracts have been compiled and have had the libraries
/// linked against them and therefore they're ready to be deployed on-demand.
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A map of all of the deployed contracts and information about them.
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata file.
/// This map stores the variables used for each one of the cases contained in the metadata
/// file.
pub variables: HashMap<String, U256>,
}
@@ -104,12 +104,6 @@ impl Watcher {
async move {
let mut mined_blocks_information = Vec::new();
// region:TEMPORARY
eprintln!("Watcher information for {}", self.platform_identifier);
eprintln!(
"block_number,block_timestamp,mined_gas,block_gas_limit,tx_count,ref_time,max_ref_time,proof_size,max_proof_size"
);
// endregion:TEMPORARY
while let Some(block) = blocks_information_stream.next().await {
// If the block number is equal to or less than the last block before the
// repetition then we ignore it and continue on to the next block.
@@ -124,9 +118,8 @@ impl Watcher {
}
info!(
block_number = block.block_number,
block_tx_count = block.transaction_hashes.len(),
remaining_transactions = watch_for_transaction_hashes.read().await.len(),
block_tx_count = block.transaction_hashes.len(),
"Observed a block"
);
@@ -138,24 +131,6 @@ impl Watcher {
watch_for_transaction_hashes.remove(tx_hash);
}
// region:TEMPORARY
// TODO: The following core is TEMPORARY and will be removed once we have proper
// reporting in place and then it can be removed. This serves as as way of doing
// some very simple reporting for the time being.
eprintln!(
"\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"",
block.block_number,
block.block_timestamp,
block.mined_gas,
block.block_gas_limit,
block.transaction_hashes.len(),
block.ref_time,
block.max_ref_time,
block.proof_size,
block.max_proof_size,
);
// endregion:TEMPORARY
mined_blocks_information.push(block);
}
@@ -164,10 +139,41 @@ impl Watcher {
}
};
let (_, _) =
let (_, mined_blocks_information) =
futures::future::join(watcher_event_watching_task, block_information_watching_task)
.await;
// region:TEMPORARY
{
// TODO: The following core is TEMPORARY and will be removed once we have proper
// reporting in place and then it can be removed. This serves as as way of doing some
// very simple reporting for the time being.
use std::io::Write;
let mut stderr = std::io::stderr().lock();
writeln!(
stderr,
"Watcher information for {}",
self.platform_identifier
)?;
writeln!(
stderr,
"block_number,block_timestamp,mined_gas,block_gas_limit,tx_count"
)?;
for block in mined_blocks_information {
writeln!(
stderr,
"{},{},{},{},{}",
block.block_number,
block.block_timestamp,
block.mined_gas,
block.block_gas_limit,
block.transaction_hashes.len()
)?
}
}
// endregion:TEMPORARY
Ok(())
}
}
+22 -5
View File
@@ -131,8 +131,8 @@ pub struct PlatformDriver<'a, I> {
/// The definition of the test that the driver is instructed to execute.
test_definition: &'a TestDefinition<'a>,
/// The private key allocator used by this driver and other drivers when account allocations are
/// needed.
/// The private key allocator used by this driver and other drivers when account allocations
/// are needed.
private_key_allocator: Arc<Mutex<PrivateKeyAllocator>>,
/// The execution state associated with the platform.
@@ -415,9 +415,13 @@ where
let caller = {
let context = self.default_resolution_context();
let resolver = self.platform_information.node.resolver().await?;
step.caller
let resolved = step
.caller
.resolve_address(resolver.as_ref(), context)
.await?
.await?;
self.platform_information
.node
.resolve_signer_or_default(resolved)
};
if let (_, _, Some(receipt)) = self
.get_or_deploy_contract_instance(&instance, caller, calldata, value)
@@ -445,7 +449,7 @@ where
.context("Failed to find deployment receipt for constructor call"),
Method::Fallback | Method::FunctionName(_) => {
let resolver = self.platform_information.node.resolver().await?;
let tx = match step
let mut tx = match step
.as_transaction(resolver.as_ref(), self.default_resolution_context())
.await
{
@@ -455,6 +459,15 @@ where
}
};
// Resolve the signer to ensure we use an address that has keys
if let Some(from) = tx.from {
tx.from = Some(
self.platform_information
.node
.resolve_signer_or_default(from),
);
}
self.platform_information.node.execute_transaction(tx).await
}
}
@@ -954,6 +967,10 @@ where
}
let tx = {
let deployer = self
.platform_information
.node
.resolve_signer_or_default(deployer);
let tx = TransactionRequest::default().from(deployer);
let tx = match value {
Some(ref value) => tx.value(value.into_inner()),
+63 -154
View File
@@ -7,21 +7,19 @@ use std::{
time::{Duration, Instant},
};
use ansi_term::{ANSIStrings, Color};
use crate::Platform;
use anyhow::Context as _;
use futures::{FutureExt, StreamExt};
use revive_dt_common::{cached_fs::read_to_string, types::PrivateKeyAllocator};
use revive_dt_core::Platform;
use revive_dt_format::corpus::Corpus;
use revive_dt_common::types::PrivateKeyAllocator;
use tokio::sync::{Mutex, RwLock, Semaphore};
use tracing::{Instrument, error, info, info_span, instrument};
use revive_dt_config::{Context, OutputFormat, TestExecutionContext};
use revive_dt_config::{Context, TestExecutionContext};
use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus};
use crate::{
differential_tests::Driver,
helpers::{CachedCompiler, NodePool, create_test_definitions_stream},
helpers::{CachedCompiler, NodePool, collect_metadata_files, create_test_definitions_stream},
};
/// Handles the differential testing executing it according to the information defined in the
@@ -34,17 +32,9 @@ pub async fn handle_differential_tests(
let reporter_clone = reporter.clone();
// Discover all of the metadata files that are defined in the context.
let corpus = context
.corpus_configuration
.test_specifiers
.clone()
.into_iter()
.try_fold(Corpus::default(), Corpus::with_test_specifier)
.context("Failed to parse the test corpus")?;
info!(
len = corpus.metadata_file_count(),
"Discovered metadata files"
);
let metadata_files = collect_metadata_files(&context)
.context("Failed to collect metadata files for differential testing")?;
info!(len = metadata_files.len(), "Discovered metadata files");
// Discover the list of platforms that the tests should run on based on the context.
let platforms = context
@@ -81,20 +71,11 @@ pub async fn handle_differential_tests(
info!("Spawned the platform nodes");
// Preparing test definitions.
let only_execute_failed_tests = match context.ignore_success_configuration.path.as_ref() {
Some(path) => {
let report = read_to_string(path)
.context("Failed to read the report file to ignore the succeeding test cases")?;
Some(serde_json::from_str(&report).context("Failed to deserialize report")?)
}
None => None,
};
let full_context = Context::Test(Box::new(context.clone()));
let test_definitions = create_test_definitions_stream(
&full_context,
&corpus,
metadata_files.iter(),
&platforms_and_nodes,
only_execute_failed_tests.as_ref(),
reporter.clone(),
)
.await
@@ -195,7 +176,7 @@ pub async fn handle_differential_tests(
.report_completion_event()
.expect("Can't fail")
});
let cli_reporting_task = start_cli_reporting_task(context.output_format, reporter);
let cli_reporting_task = start_cli_reporting_task(reporter);
tokio::task::spawn(async move {
loop {
@@ -205,7 +186,6 @@ pub async fn handle_differential_tests(
?remaining_tasks,
"Remaining Tests"
);
drop(remaining_tasks);
tokio::time::sleep(Duration::from_secs(10)).await
}
});
@@ -216,15 +196,21 @@ pub async fn handle_differential_tests(
}
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
async fn start_cli_reporting_task(output_format: OutputFormat, reporter: Reporter) {
async fn start_cli_reporting_task(reporter: Reporter) {
let mut aggregator_events_rx = reporter.subscribe().await.expect("Can't fail");
drop(reporter);
let start = Instant::now();
let mut global_success_count = 0;
let mut global_failure_count = 0;
let mut global_ignore_count = 0;
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const GREY: &str = "\x1B[90m";
const COLOR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m";
let mut number_of_successes = 0;
let mut number_of_failures = 0;
let mut buf = BufWriter::new(stderr());
while let Ok(event) = aggregator_events_rx.recv().await {
@@ -237,132 +223,55 @@ async fn start_cli_reporting_task(output_format: OutputFormat, reporter: Reporte
continue;
};
match output_format {
OutputFormat::Legacy => {
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
for (case_idx, case_status) in case_status.into_iter() {
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
global_success_count += 1;
writeln!(
buf,
"{}",
ANSIStrings(&[
Color::Green.bold().paint("Case Succeeded"),
Color::Green
.paint(format!(" - Steps Executed: {steps_executed}")),
])
)
}
TestCaseStatus::Failed { reason } => {
global_failure_count += 1;
writeln!(
buf,
"{}",
ANSIStrings(&[
Color::Red.bold().paint("Case Failed"),
Color::Red.paint(format!(" - Reason: {}", reason.trim())),
])
)
}
TestCaseStatus::Ignored { reason, .. } => {
global_ignore_count += 1;
writeln!(
buf,
"{}",
ANSIStrings(&[
Color::Yellow.bold().paint("Case Ignored"),
Color::Yellow.paint(format!(" - Reason: {}", reason.trim())),
])
)
}
};
let _ = writeln!(buf, "{} - {}", mode, metadata_file_path.display());
for (case_idx, case_status) in case_status.into_iter() {
let _ = write!(buf, "\tCase Index {case_idx:>3}: ");
let _ = match case_status {
TestCaseStatus::Succeeded { steps_executed } => {
number_of_successes += 1;
writeln!(
buf,
"{}{}Case Succeeded{} - Steps Executed: {}{}",
GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET
)
}
let _ = writeln!(buf);
}
OutputFormat::CargoTestLike => {
writeln!(
buf,
"\t{} {} - {}\n",
Color::Green.paint("Running"),
metadata_file_path.display(),
mode
)
.unwrap();
let mut success_count = 0;
let mut failure_count = 0;
let mut ignored_count = 0;
writeln!(buf, "running {} tests", case_status.len()).unwrap();
for (case_idx, case_result) in case_status.iter() {
let status = match case_result {
TestCaseStatus::Succeeded { .. } => {
success_count += 1;
global_success_count += 1;
Color::Green.paint("ok")
}
TestCaseStatus::Failed { reason } => {
failure_count += 1;
global_failure_count += 1;
Color::Red.paint(format!("FAILED, {reason}"))
}
TestCaseStatus::Ignored { reason, .. } => {
ignored_count += 1;
global_ignore_count += 1;
Color::Yellow.paint(format!("ignored, {reason:?}"))
}
};
writeln!(buf, "test case_idx_{} ... {}", case_idx, status).unwrap();
TestCaseStatus::Failed { reason } => {
number_of_failures += 1;
writeln!(
buf,
"{}{}Case Failed{} - Reason: {}{}",
RED,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
)
}
writeln!(buf).unwrap();
let status = if failure_count > 0 {
Color::Red.paint("FAILED")
} else {
Color::Green.paint("ok")
};
writeln!(
TestCaseStatus::Ignored { reason, .. } => writeln!(
buf,
"test result: {}. {} passed; {} failed; {} ignored",
status, success_count, failure_count, ignored_count,
)
.unwrap();
writeln!(buf).unwrap();
buf = tokio::task::spawn_blocking(move || {
buf.flush().unwrap();
buf
})
.await
.unwrap();
}
"{}{}Case Ignored{} - Reason: {}{}",
GREY,
BOLD,
BOLD_RESET,
reason.trim(),
COLOR_RESET,
),
};
}
let _ = writeln!(buf);
}
// Summary at the end.
match output_format {
OutputFormat::Legacy => {
writeln!(
buf,
"{} cases: {} cases succeeded, {} cases failed in {} seconds",
global_success_count + global_failure_count + global_ignore_count,
Color::Green.paint(global_success_count.to_string()),
Color::Red.paint(global_failure_count.to_string()),
start.elapsed().as_secs()
)
.unwrap();
}
OutputFormat::CargoTestLike => {
writeln!(
buf,
"run finished. {} passed; {} failed; {} ignored; finished in {}s",
global_success_count,
global_failure_count,
global_ignore_count,
start.elapsed().as_secs()
)
.unwrap();
}
}
let _ = writeln!(
buf,
"{} cases: {}{}{} cases succeeded, {}{}{} cases failed in {} seconds",
number_of_successes + number_of_failures,
GREEN,
number_of_successes,
COLOR_RESET,
RED,
number_of_failures,
COLOR_RESET,
start.elapsed().as_secs()
);
}
@@ -10,14 +10,15 @@ use revive_dt_format::metadata::{ContractIdent, ContractInstance};
#[derive(Clone)]
/// The state associated with the test execution of one of the tests.
pub struct ExecutionState {
/// The compiled contracts, these contracts have been compiled and have had the libraries linked
/// against them and therefore they're ready to be deployed on-demand.
/// The compiled contracts, these contracts have been compiled and have had the libraries
/// linked against them and therefore they're ready to be deployed on-demand.
pub compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A map of all of the deployed contracts and information about them.
pub deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
/// This map stores the variables used for each one of the cases contained in the metadata file.
/// This map stores the variables used for each one of the cases contained in the metadata
/// file.
pub variables: HashMap<String, U256>,
}
+21 -1
View File
@@ -8,10 +8,10 @@ use std::{
sync::{Arc, LazyLock},
};
use crate::Platform;
use futures::FutureExt;
use revive_dt_common::{iterators::FilesWithExtensionIterator, types::CompilerIdentifier};
use revive_dt_compiler::{Compiler, CompilerOutput, Mode, SolidityCompiler};
use revive_dt_core::Platform;
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
@@ -325,6 +325,26 @@ impl ArtifactsCache {
let value = bson::from_slice::<CacheValue>(&value).ok()?;
Some(value)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_or_insert_with(
&self,
key: &CacheKey<'_>,
callback: impl AsyncFnOnce() -> Result<CacheValue>,
) -> Result<CacheValue> {
match self.get(key).await {
Some(value) => {
debug!("Cache hit");
Ok(value)
}
None => {
debug!("Cache miss");
let value = callback().await?;
self.insert(key, &value).await?;
Ok(value)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
+33
View File
@@ -0,0 +1,33 @@
use revive_dt_config::CorpusConfiguration;
use revive_dt_format::{corpus::Corpus, metadata::MetadataFile};
use tracing::{info, info_span, instrument};
/// Given an object that implements [`AsRef<CorpusConfiguration>`], this function finds all of the
/// corpus files and produces a map containing all of the [`MetadataFile`]s discovered.
#[instrument(level = "debug", name = "Collecting Corpora", skip_all)]
pub fn collect_metadata_files(
context: impl AsRef<CorpusConfiguration>,
) -> anyhow::Result<Vec<MetadataFile>> {
let mut metadata_files = Vec::new();
let corpus_configuration = AsRef::<CorpusConfiguration>::as_ref(&context);
for path in &corpus_configuration.paths {
let span = info_span!("Processing corpus file", path = %path.display());
let _guard = span.enter();
let corpus = Corpus::try_from_path(path)?;
info!(
name = corpus.name(),
number_of_contained_paths = corpus.path_count(),
"Deserialized corpus file"
);
metadata_files.extend(corpus.enumerate_tests());
}
// There's a possibility that there are certain paths that all lead to the same metadata files
// and therefore it's important that we sort them and then deduplicate them.
metadata_files.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
metadata_files.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
Ok(metadata_files)
}
+2
View File
@@ -1,7 +1,9 @@
mod cached_compiler;
mod metadata;
mod pool;
mod test;
pub use cached_compiler::*;
pub use metadata::*;
pub use pool::*;
pub use test::*;
+1 -1
View File
@@ -2,9 +2,9 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::Platform;
use anyhow::Context as _;
use revive_dt_config::*;
use revive_dt_core::Platform;
use revive_dt_node_interaction::EthereumNode;
/// The node pool starts one or more [Node] which then can be accessed
+44 -65
View File
@@ -1,57 +1,70 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{borrow::Cow, path::Path};
use std::{borrow::Cow, collections::BTreeMap, path::Path, sync::Arc};
use futures::{Stream, StreamExt, stream};
use indexmap::{IndexMap, indexmap};
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_common::{iterators::EitherIter, types::PlatformIdentifier};
use revive_dt_config::Context;
use revive_dt_format::corpus::Corpus;
use revive_dt_format::mode::ParsedMode;
use serde_json::{Value, json};
use revive_dt_compiler::Mode;
use revive_dt_compiler::SolidityCompiler;
use revive_dt_compiler::{Mode, SolidityCompiler};
use revive_dt_format::{
case::{Case, CaseIdx},
metadata::MetadataFile,
};
use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::{ExecutionSpecificReporter, Report, Reporter, TestCaseStatus};
use revive_dt_report::{TestSpecificReporter, TestSpecifier};
use revive_dt_report::{ExecutionSpecificReporter, Reporter, TestSpecificReporter, TestSpecifier};
use tracing::{debug, error, info};
use crate::Platform;
use crate::helpers::NodePool;
use crate::{Platform, helpers::NodePool};
pub async fn create_test_definitions_stream<'a>(
// This is only required for creating the compiler objects and is not used anywhere else in the
// function.
context: &Context,
corpus: &'a Corpus,
metadata_files: impl IntoIterator<Item = &'a MetadataFile>,
platforms_and_nodes: &'a BTreeMap<PlatformIdentifier, (&dyn Platform, NodePool)>,
only_execute_failed_tests: Option<&Report>,
reporter: Reporter,
) -> impl Stream<Item = TestDefinition<'a>> {
stream::iter(
corpus
.cases_iterator()
.map(move |(metadata_file, case_idx, case, mode)| {
metadata_files
.into_iter()
// Flatten over the cases.
.flat_map(|metadata_file| {
metadata_file
.cases
.iter()
.enumerate()
.map(move |(case_idx, case)| (metadata_file, case_idx, case))
})
// Flatten over the modes, prefer the case modes over the metadata file modes.
.flat_map(move |(metadata_file, case_idx, case)| {
let reporter = reporter.clone();
(
metadata_file,
case_idx,
case,
mode.clone(),
reporter.test_specific_reporter(Arc::new(TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
})),
)
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter()).map(Cow::<'static, _>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
modes.into_iter().map(move |mode| {
(
metadata_file,
case_idx,
case,
mode.clone(),
reporter.test_specific_reporter(Arc::new(TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
})),
)
})
})
// Inform the reporter of each one of the test cases that were discovered which we expect to
// run.
// Inform the reporter of each one of the test cases that were discovered which we
// expect to run.
.inspect(|(_, _, _, _, reporter)| {
reporter
.report_test_case_discovery_event()
@@ -121,7 +134,7 @@ pub async fn create_test_definitions_stream<'a>(
)
// Filter out the test cases which are incompatible or that can't run in the current setup.
.filter_map(move |test| async move {
match test.check_compatibility(only_execute_failed_tests) {
match test.check_compatibility() {
Ok(()) => Some(test),
Err((reason, additional_information)) => {
debug!(
@@ -181,16 +194,12 @@ pub struct TestDefinition<'a> {
impl<'a> TestDefinition<'a> {
/// Checks if this test can be ran with the current configuration.
pub fn check_compatibility(
&self,
only_execute_failed_tests: Option<&Report>,
) -> TestCheckFunctionResult {
pub fn check_compatibility(&self) -> TestCheckFunctionResult {
self.check_metadata_file_ignored()?;
self.check_case_file_ignored()?;
self.check_target_compatibility()?;
self.check_evm_version_compatibility()?;
self.check_compiler_compatibility()?;
self.check_ignore_succeeded(only_execute_failed_tests)?;
Ok(())
}
@@ -298,36 +307,6 @@ impl<'a> TestDefinition<'a> {
))
}
}
/// Checks if the test case should be executed or not based on the passed report and whether the
/// user has instructed the tool to ignore the already succeeding test cases.
fn check_ignore_succeeded(
&self,
only_execute_failed_tests: Option<&Report>,
) -> TestCheckFunctionResult {
let Some(report) = only_execute_failed_tests else {
return Ok(());
};
let test_case_status = report
.test_case_information
.get(&(self.metadata_file_path.to_path_buf().into()))
.and_then(|obj| obj.get(&self.mode))
.and_then(|obj| obj.get(&self.case_idx))
.and_then(|obj| obj.status.as_ref());
match test_case_status {
Some(TestCaseStatus::Failed { .. }) => Ok(()),
Some(TestCaseStatus::Ignored { .. }) => Err((
"Ignored since it was ignored in a previous run",
indexmap! {},
)),
Some(TestCaseStatus::Succeeded { .. }) => {
Err(("Ignored since it succeeded in a prior run", indexmap! {}))
}
None => Ok(()),
}
}
}
pub struct TestPlatformInformation<'a> {
+19 -107
View File
@@ -3,6 +3,9 @@
//! This crate defines the testing configuration and
//! provides a helper utility to execute tests.
pub mod differential_tests;
pub mod helpers;
use std::{
pin::Pin,
thread::{self, JoinHandle},
@@ -14,13 +17,17 @@ use revive_dt_common::types::*;
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
use revive_dt_config::*;
use revive_dt_node::{
Node, node_implementations::geth::GethNode,
node_implementations::lighthouse_geth::LighthouseGethNode,
node_implementations::substrate::SubstrateNode, node_implementations::zombienet::ZombienetNode,
Node,
node_implementations::{
geth::GethNode, lighthouse_geth::LighthouseGethNode, substrate::SubstrateNode,
zombienet::ZombieNode,
},
};
use revive_dt_node_interaction::EthereumNode;
use tracing::info;
pub use helpers::CachedCompiler;
/// A trait that describes the interface for the platforms that are supported by the tool.
#[allow(clippy::type_complexity)]
pub trait Platform {
@@ -59,9 +66,6 @@ pub trait Platform {
context: Context,
version: Option<VersionOrRequirement>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>>;
/// Exports the genesis/chainspec for the node.
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -107,15 +111,6 @@ impl Platform for GethEvmSolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let genesis = AsRef::<GenesisConfiguration>::as_ref(&context).genesis()?;
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let node_genesis = GethNode::node_genesis(genesis.clone(), &wallet);
serde_json::to_value(node_genesis)
.context("Failed to convert node genesis to a serde_value")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -161,15 +156,6 @@ impl Platform for LighthouseGethEvmSolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let genesis = AsRef::<GenesisConfiguration>::as_ref(&context).genesis()?;
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let node_genesis = LighthouseGethNode::node_genesis(genesis.clone(), &wallet);
serde_json::to_value(node_genesis)
.context("Failed to convert node genesis to a serde_value")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -205,9 +191,7 @@ impl Platform for KitchensinkPolkavmResolcPlatform {
let node = SubstrateNode::new(
kitchensink_path,
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
context,
&[],
);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
@@ -224,16 +208,6 @@ impl Platform for KitchensinkPolkavmResolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let export_chainspec_command = SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND;
SubstrateNode::node_genesis(kitchensink_path, export_chainspec_command, &wallet)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -269,9 +243,7 @@ impl Platform for KitchensinkRevmSolcPlatform {
let node = SubstrateNode::new(
kitchensink_path,
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
None,
context,
&[],
);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
@@ -288,16 +260,6 @@ impl Platform for KitchensinkRevmSolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let export_chainspec_command = SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND;
SubstrateNode::node_genesis(kitchensink_path, export_chainspec_command, &wallet)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -325,21 +287,15 @@ impl Platform for ReviveDevNodePolkavmResolcPlatform {
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
let eth_rpc_connection_strings = revive_dev_node_configuration.existing_rpc_url.clone();
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
.path
.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = SubstrateNode::new(
revive_dev_node_path,
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
Some(revive_dev_node_consensus),
context,
&eth_rpc_connection_strings,
);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
@@ -356,16 +312,6 @@ impl Platform for ReviveDevNodePolkavmResolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let export_chainspec_command = SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND;
SubstrateNode::node_genesis(revive_dev_node_path, export_chainspec_command, &wallet)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -393,21 +339,15 @@ impl Platform for ReviveDevNodeRevmSolcPlatform {
context: Context,
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
let eth_rpc_connection_strings = revive_dev_node_configuration.existing_rpc_url.clone();
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
.path
.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = SubstrateNode::new(
revive_dev_node_path,
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
Some(revive_dev_node_consensus),
context,
&eth_rpc_connection_strings,
);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
@@ -424,16 +364,6 @@ impl Platform for ReviveDevNodeRevmSolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let revive_dev_node_path = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let export_chainspec_command = SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND;
SubstrateNode::node_genesis(revive_dev_node_path, export_chainspec_command, &wallet)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -466,7 +396,7 @@ impl Platform for ZombienetPolkavmResolcPlatform {
.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = ZombienetNode::new(polkadot_parachain_path, context);
let node = ZombieNode::new(polkadot_parachain_path, context);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
}))
@@ -478,19 +408,10 @@ impl Platform for ZombienetPolkavmResolcPlatform {
version: Option<VersionOrRequirement>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
Box::pin(async move {
let compiler = Resolc::new(context, version).await;
let compiler = Solc::new(context, version).await;
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
ZombienetNode::node_genesis(polkadot_parachain_path, &wallet)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -523,7 +444,7 @@ impl Platform for ZombienetRevmSolcPlatform {
.clone();
let genesis = genesis_configuration.genesis()?.clone();
Ok(thread::spawn(move || {
let node = ZombienetNode::new(polkadot_parachain_path, context);
let node = ZombieNode::new(polkadot_parachain_path, context);
let node = spawn_node(node, genesis)?;
Ok(Box::new(node) as Box<_>)
}))
@@ -539,15 +460,6 @@ impl Platform for ZombienetRevmSolcPlatform {
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
})
}
fn export_genesis(&self, context: Context) -> anyhow::Result<serde_json::Value> {
let polkadot_parachain_path = AsRef::<PolkadotParachainConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
ZombienetNode::node_genesis(polkadot_parachain_path, &wallet)
}
}
impl From<PlatformIdentifier> for Box<dyn Platform> {
+3 -18
View File
@@ -2,11 +2,10 @@ mod differential_benchmarks;
mod differential_tests;
mod helpers;
use anyhow::Context as _;
use clap::Parser;
use revive_dt_report::ReportAggregator;
use schemars::schema_for;
use tracing::{info, level_filters::LevelFilter};
use tracing::info;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use revive_dt_config::Context;
@@ -31,20 +30,14 @@ fn main() -> anyhow::Result<()> {
.with_writer(writer)
.with_thread_ids(false)
.with_thread_names(false)
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::OFF.into())
.from_env_lossy(),
)
.with_env_filter(EnvFilter::from_default_env())
.with_ansi(false)
.pretty()
.finish();
tracing::subscriber::set_global_default(subscriber)?;
info!("Differential testing tool is starting");
let mut context = Context::try_parse()?;
context.update_for_profile();
let context = Context::try_parse()?;
let (reporter, report_aggregator_task) = ReportAggregator::new(context.clone()).into_task();
match context {
@@ -79,14 +72,6 @@ fn main() -> anyhow::Result<()> {
Ok(())
}),
Context::ExportGenesis(ref export_genesis_context) => {
let platform = Into::<&dyn Platform>::into(export_genesis_context.platform);
let genesis = platform.export_genesis(context)?;
let genesis_json = serde_json::to_string_pretty(&genesis)
.context("Failed to serialize the genesis to JSON")?;
println!("{genesis_json}");
Ok(())
}
Context::ExportJsonSchema => {
let schema = schema_for!(Metadata);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
+1 -1
View File
@@ -16,12 +16,12 @@ revive-common = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
futures = { workspace = true }
regex = { workspace = true }
tracing = { workspace = true }
schemars = { workspace = true }
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
itertools = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
+4 -7
View File
@@ -1,12 +1,9 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use revive_dt_common::{
macros::define_wrapper_type,
types::{Mode, ParsedMode},
};
use revive_dt_common::{macros::define_wrapper_type, types::Mode};
use crate::steps::*;
use crate::{mode::ParsedMode, steps::*};
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
pub struct Case {
@@ -48,8 +45,8 @@ pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<Expected>,
/// An optional boolean which defines if the case as a whole should be ignored. If null then the
/// case will not be ignored.
/// An optional boolean which defines if the case as a whole should be ignored. If null then
/// the case will not be ignored.
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
}
+112 -181
View File
@@ -1,200 +1,131 @@
use std::{
borrow::Cow,
collections::HashMap,
fs::File,
path::{Path, PathBuf},
};
use itertools::Itertools;
use revive_dt_common::{
iterators::{EitherIter, FilesWithExtensionIterator},
types::{Mode, ParsedMode, ParsedTestSpecifier},
};
use tracing::{debug, warn};
use revive_dt_common::iterators::FilesWithExtensionIterator;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::{
case::{Case, CaseIdx},
metadata::{Metadata, MetadataFile},
};
use crate::metadata::{Metadata, MetadataFile};
use anyhow::Context as _;
#[derive(Default)]
pub struct Corpus {
test_specifiers: HashMap<ParsedTestSpecifier, Vec<PathBuf>>,
metadata_files: HashMap<PathBuf, MetadataFile>,
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Corpus {
SinglePath { name: String, path: PathBuf },
MultiplePaths { name: String, paths: Vec<PathBuf> },
}
impl Corpus {
pub fn new() -> Self {
Default::default()
}
pub fn try_from_path(file_path: impl AsRef<Path>) -> anyhow::Result<Self> {
let mut corpus = File::open(file_path.as_ref())
.map_err(anyhow::Error::from)
.and_then(|file| serde_json::from_reader::<_, Corpus>(file).map_err(Into::into))
.with_context(|| {
format!(
"Failed to open and deserialize corpus file at {}",
file_path.as_ref().display()
)
})?;
pub fn with_test_specifier(
mut self,
test_specifier: ParsedTestSpecifier,
) -> anyhow::Result<Self> {
match &test_specifier {
ParsedTestSpecifier::FileOrDirectory {
metadata_or_directory_file_path: metadata_file_path,
}
| ParsedTestSpecifier::Case {
metadata_file_path, ..
}
| ParsedTestSpecifier::CaseWithMode {
metadata_file_path, ..
} => {
let metadata_files = enumerate_metadata_files(metadata_file_path);
self.test_specifiers.insert(
test_specifier,
metadata_files
.iter()
.map(|metadata_file| metadata_file.metadata_file_path.clone())
.collect(),
);
for metadata_file in metadata_files.into_iter() {
self.metadata_files
.insert(metadata_file.metadata_file_path.clone(), metadata_file);
}
}
};
let corpus_directory = file_path
.as_ref()
.canonicalize()
.context("Failed to canonicalize the path to the corpus file")?
.parent()
.context("Corpus file has no parent")?
.to_path_buf();
Ok(self)
}
pub fn cases_iterator(
&self,
) -> impl Iterator<Item = (&'_ MetadataFile, CaseIdx, &'_ Case, Cow<'_, Mode>)> + '_ {
let mut iterator = Box::new(std::iter::empty())
as Box<dyn Iterator<Item = (&'_ MetadataFile, CaseIdx, &'_ Case, Cow<'_, Mode>)> + '_>;
for (test_specifier, metadata_file_paths) in self.test_specifiers.iter() {
for metadata_file_path in metadata_file_paths {
let metadata_file = self
.metadata_files
.get(metadata_file_path)
.expect("Must succeed");
match test_specifier {
ParsedTestSpecifier::FileOrDirectory { .. } => {
for (case_idx, case) in metadata_file.cases.iter().enumerate() {
let case_idx = CaseIdx::new(case_idx);
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter())
.map(Cow::<'static, _>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
iterator = Box::new(
iterator.chain(
modes
.into_iter()
.map(move |mode| (metadata_file, case_idx, case, mode)),
),
)
}
}
ParsedTestSpecifier::Case { case_idx, .. } => {
let Some(case) = metadata_file.cases.get(*case_idx) else {
warn!(
test_specifier = %test_specifier,
metadata_file_path = %metadata_file_path.display(),
case_idx = case_idx,
case_count = metadata_file.cases.len(),
"Specified case not found in metadata file"
);
continue;
};
let case_idx = CaseIdx::new(*case_idx);
let modes = case.modes.as_ref().or(metadata_file.modes.as_ref());
let modes = match modes {
Some(modes) => EitherIter::A(
ParsedMode::many_to_modes(modes.iter())
.map(Cow::<'static, Mode>::Owned),
),
None => EitherIter::B(Mode::all().map(Cow::<'static, _>::Borrowed)),
};
iterator = Box::new(
iterator.chain(
modes
.into_iter()
.map(move |mode| (metadata_file, case_idx, case, mode)),
),
)
}
ParsedTestSpecifier::CaseWithMode { case_idx, mode, .. } => {
let Some(case) = metadata_file.cases.get(*case_idx) else {
warn!(
test_specifier = %test_specifier,
metadata_file_path = %metadata_file_path.display(),
case_idx = case_idx,
case_count = metadata_file.cases.len(),
"Specified case not found in metadata file"
);
continue;
};
let case_idx = CaseIdx::new(*case_idx);
let mode = Cow::Borrowed(mode);
iterator = Box::new(iterator.chain(std::iter::once((
metadata_file,
case_idx,
case,
mode,
))))
}
}
}
for path in corpus.paths_iter_mut() {
*path = corpus_directory.join(path.as_path())
}
iterator.unique_by(|item| (&item.0.metadata_file_path, item.1, item.3.clone()))
Ok(corpus)
}
pub fn metadata_file_count(&self) -> usize {
self.metadata_files.len()
pub fn enumerate_tests(&self) -> Vec<MetadataFile> {
let mut tests = self
.paths_iter()
.flat_map(|root_path| {
if !root_path.is_dir() {
Box::new(std::iter::once(root_path.to_path_buf()))
as Box<dyn Iterator<Item = _>>
} else {
Box::new(
FilesWithExtensionIterator::new(root_path)
.with_use_cached_fs(true)
.with_allowed_extension("sol")
.with_allowed_extension("json"),
)
}
.map(move |metadata_file_path| (root_path, metadata_file_path))
})
.filter_map(|(root_path, metadata_file_path)| {
Metadata::try_from_file(&metadata_file_path)
.or_else(|| {
debug!(
discovered_from = %root_path.display(),
metadata_file_path = %metadata_file_path.display(),
"Skipping file since it doesn't contain valid metadata"
);
None
})
.map(|metadata| MetadataFile {
metadata_file_path,
corpus_file_path: root_path.to_path_buf(),
content: metadata,
})
.inspect(|metadata_file| {
debug!(
metadata_file_path = %metadata_file.relative_path().display(),
"Loaded metadata file"
)
})
})
.collect::<Vec<_>>();
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
info!(
len = tests.len(),
corpus_name = self.name(),
"Found tests in Corpus"
);
tests
}
}
fn enumerate_metadata_files(path: impl AsRef<Path>) -> Vec<MetadataFile> {
let root_path = path.as_ref();
let mut tests = if !root_path.is_dir() {
Box::new(std::iter::once(root_path.to_path_buf())) as Box<dyn Iterator<Item = _>>
} else {
Box::new(
FilesWithExtensionIterator::new(root_path)
.with_use_cached_fs(true)
.with_allowed_extension("sol")
.with_allowed_extension("json"),
)
pub fn name(&self) -> &str {
match self {
Corpus::SinglePath { name, .. } | Corpus::MultiplePaths { name, .. } => name.as_str(),
}
}
pub fn paths_iter(&self) -> impl Iterator<Item = &Path> {
match self {
Corpus::SinglePath { path, .. } => {
Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>
}
Corpus::MultiplePaths { paths, .. } => {
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>
}
}
}
pub fn paths_iter_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> {
match self {
Corpus::SinglePath { path, .. } => {
Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>>
}
Corpus::MultiplePaths { paths, .. } => {
Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>>
}
}
}
pub fn path_count(&self) -> usize {
match self {
Corpus::SinglePath { .. } => 1,
Corpus::MultiplePaths { paths, .. } => paths.len(),
}
}
.map(move |metadata_file_path| (root_path, metadata_file_path))
.filter_map(|(root_path, metadata_file_path)| {
Metadata::try_from_file(&metadata_file_path)
.or_else(|| {
debug!(
discovered_from = %root_path.display(),
metadata_file_path = %metadata_file_path.display(),
"Skipping file since it doesn't contain valid metadata"
);
None
})
.map(|metadata| MetadataFile {
metadata_file_path,
corpus_file_path: root_path.to_path_buf(),
content: metadata,
})
.inspect(|metadata_file| {
debug!(
metadata_file_path = %metadata_file.relative_path().display(),
"Loaded metadata file"
)
})
})
.collect::<Vec<_>>();
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
tests
}
+1
View File
@@ -3,5 +3,6 @@
pub mod case;
pub mod corpus;
pub mod metadata;
pub mod mode;
pub mod steps;
pub mod traits;
+15 -13
View File
@@ -16,11 +16,11 @@ use revive_dt_common::{
cached_fs::read_to_string,
iterators::FilesWithExtensionIterator,
macros::define_wrapper_type,
types::{Mode, ParsedMode, VmIdentifier},
types::{Mode, VmIdentifier},
};
use tracing::error;
use crate::case::Case;
use crate::{case::Case, mode::ParsedMode};
pub const METADATA_FILE_EXTENSION: &str = "json";
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
@@ -31,8 +31,8 @@ pub struct MetadataFile {
/// The path of the metadata file. This will either be a JSON or solidity file.
pub metadata_file_path: PathBuf,
/// This is the path contained within the corpus file. This could either be the path of some dir
/// or could be the actual metadata file path.
/// This is the path contained within the corpus file. This could either be the path of some
/// dir or could be the actual metadata file path.
pub corpus_file_path: PathBuf,
/// The metadata contained within the file.
@@ -69,13 +69,13 @@ impl Deref for MetadataFile {
/// of steps and assertions that should be performed as part of the test case.
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Eq, PartialEq)]
pub struct Metadata {
/// This is an optional comment on the metadata file which has no impact on the execution in any
/// way.
/// This is an optional comment on the metadata file which has no impact on the execution in
/// any way.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// An optional boolean which defines if the metadata file as a whole should be ignored. If null
/// then the metadata file will not be ignored.
/// An optional boolean which defines if the metadata file as a whole should be ignored. If
/// null then the metadata file will not be ignored.
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
@@ -94,8 +94,8 @@ pub struct Metadata {
/// This is a map where the key is the name of the contract instance and the value is the
/// contract's path and ident in the file.
///
/// If any contract is to be used by the test then it must be included in here first so that the
/// framework is aware of its path, compiles it, and prepares it.
/// If any contract is to be used by the test then it must be included in here first so that
/// the framework is aware of its path, compiles it, and prepares it.
#[serde(skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
@@ -123,8 +123,9 @@ pub struct Metadata {
pub required_evm_version: Option<EvmVersionRequirement>,
/// A set of compilation directives that will be passed to the compiler whenever the contracts
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]
/// is just a filter for when a test can run whereas this is an instruction to the compiler.
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a
/// [`Mode`] is just a filter for when a test can run whereas this is an instruction to the
/// compiler.
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler_directives: Option<CompilationDirectives>,
}
@@ -326,7 +327,8 @@ define_wrapper_type!(
)]
#[serde(try_from = "String", into = "String")]
pub struct ContractPathAndIdent {
/// The path of the contract source code relative to the directory containing the metadata file.
/// The path of the contract source code relative to the directory containing the metadata
/// file.
pub contract_source_path: PathBuf,
/// The identifier of the contract.
+256
View File
@@ -0,0 +1,256 @@
use anyhow::Context as _;
use regex::Regex;
use revive_dt_common::{
iterators::EitherIter,
types::{Mode, ModeOptimizerSetting, ModePipeline},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, fmt::Display, str::FromStr, sync::LazyLock};
/// This represents a mode that has been parsed from test metadata.
///
/// Mode strings can take the following form (in pseudo-regex):
///
/// ```text
/// [YEILV][+-]? (M[0123sz])? <semver>?
/// ```
///
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
#[serde(try_from = "String", into = "String")]
pub struct ParsedMode {
pub pipeline: Option<ModePipeline>,
pub optimize_flag: Option<bool>,
pub optimize_setting: Option<ModeOptimizerSetting>,
pub version: Option<semver::VersionReq>,
}
impl FromStr for ParsedMode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?x)
^
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
\s*
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
\s*
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
$
").unwrap()
});
let Some(caps) = REGEX.captures(s) else {
anyhow::bail!("Cannot parse mode '{s}' from string");
};
let pipeline = match caps.name("pipeline") {
Some(m) => Some(
ModePipeline::from_str(m.as_str())
.context("Failed to parse mode pipeline from string")?,
),
None => None,
};
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
let optimize_setting = match caps.name("optimize_setting") {
Some(m) => Some(
ModeOptimizerSetting::from_str(m.as_str())
.context("Failed to parse optimizer setting from string")?,
),
None => None,
};
let version = match caps.name("version") {
Some(m) => Some(
semver::VersionReq::parse(m.as_str())
.map_err(|e| {
anyhow::anyhow!(
"Cannot parse the version requirement '{}': {e}",
m.as_str()
)
})
.context("Failed to parse semver requirement from mode string")?,
),
None => None,
};
Ok(ParsedMode {
pipeline,
optimize_flag,
optimize_setting,
version,
})
}
}
impl Display for ParsedMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut has_written = false;
if let Some(pipeline) = self.pipeline {
pipeline.fmt(f)?;
if let Some(optimize_flag) = self.optimize_flag {
f.write_str(if optimize_flag { "+" } else { "-" })?;
}
has_written = true;
}
if let Some(optimize_setting) = self.optimize_setting {
if has_written {
f.write_str(" ")?;
}
optimize_setting.fmt(f)?;
has_written = true;
}
if let Some(version) = &self.version {
if has_written {
f.write_str(" ")?;
}
version.fmt(f)?;
}
Ok(())
}
}
impl From<ParsedMode> for String {
fn from(parsed_mode: ParsedMode) -> Self {
parsed_mode.to_string()
}
}
impl TryFrom<String> for ParsedMode {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
ParsedMode::from_str(&value)
}
}
impl ParsedMode {
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|| EitherIter::A(ModePipeline::test_cases()),
|p| EitherIter::B(std::iter::once(*p)),
);
let optimize_flag_setting = self.optimize_flag.map(|flag| {
if flag {
ModeOptimizerSetting::M3
} else {
ModeOptimizerSetting::M0
}
});
let optimize_flag_iter = match optimize_flag_setting {
Some(setting) => EitherIter::A(std::iter::once(setting)),
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
};
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|| EitherIter::A(optimize_flag_iter),
|s| EitherIter::B(std::iter::once(*s)),
);
pipeline_iter.flat_map(move |pipeline| {
optimize_settings_iter
.clone()
.map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: self.version.clone(),
})
})
}
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
/// This avoids any duplicate entries.
pub fn many_to_modes<'a>(
parsed: impl Iterator<Item = &'a ParsedMode>,
) -> impl Iterator<Item = Mode> {
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
modes.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parsed_mode_from_str() {
let strings = vec![
("Mz", "Mz"),
("Y", "Y"),
("Y+", "Y+"),
("Y-", "Y-"),
("E", "E"),
("E+", "E+"),
("E-", "E-"),
("Y M0", "Y M0"),
("Y M1", "Y M1"),
("Y M2", "Y M2"),
("Y M3", "Y M3"),
("Y Ms", "Y Ms"),
("Y Mz", "Y Mz"),
("E M0", "E M0"),
("E M1", "E M1"),
("E M2", "E M2"),
("E M3", "E M3"),
("E Ms", "E Ms"),
("E Mz", "E Mz"),
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
("Y 0.8.0", "Y ^0.8.0"),
("E+ 0.8.0", "E+ ^0.8.0"),
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
("E Mz <0.7.0", "E Mz <0.7.0"),
// We can parse +- _and_ M1/M2 but the latter takes priority.
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
("E- M2 0.7.0", "E- M2 ^0.7.0"),
// We don't see this in the wild but it is parsed.
("<=0.8", "<=0.8"),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
assert_eq!(
expected,
parsed.to_string(),
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
);
}
}
#[test]
fn test_parsed_mode_to_test_modes() {
let strings = vec![
("Mz", vec!["Y Mz", "E Mz"]),
("Y", vec!["Y M0", "Y M3"]),
("E", vec!["E M0", "E M3"]),
("Y+", vec!["Y M3"]),
("Y-", vec!["Y M0"]),
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
(
"<=0.8",
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.unwrap_or_else(|_| panic!("Failed to parse mode string '{actual}'"));
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
assert_eq!(
expected_set, actual_set,
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
);
}
}
}
+31 -26
View File
@@ -1,11 +1,10 @@
use std::{collections::HashMap, fmt::Display, str::FromStr};
use alloy::primitives::{FixedBytes, utils::parse_units};
use alloy::{
eips::BlockNumberOrTag,
json_abi::Function,
network::TransactionBuilder,
primitives::{Address, Bytes, U256},
primitives::{Address, Bytes, FixedBytes, U256, utils::parse_units},
rpc::types::TransactionRequest,
};
use anyhow::Context as _;
@@ -17,8 +16,10 @@ use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type;
use tracing::{Instrument, info_span, instrument};
use crate::traits::ResolverApi;
use crate::{metadata::ContractInstance, traits::ResolutionContext};
use crate::{
metadata::ContractInstance,
traits::{ResolutionContext, ResolverApi},
};
/// A test step.
///
@@ -147,8 +148,8 @@ pub struct FunctionCallStep {
#[schemars(skip)]
pub storage: Option<HashMap<String, Calldata>>,
/// Variable assignment to perform in the framework allowing us to reference them again later on
/// during the execution.
/// Variable assignment to perform in the framework allowing us to reference them again later
/// on during the execution.
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_assignments: Option<VariableAssignments>,
}
@@ -488,21 +489,20 @@ impl FunctionCallStep {
anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref());
};
// We follow the same logic that's implemented in the matter-labs-tester where they resolve
// the function name into a function selector and they assume that he function doesn't have
// any existing overloads.
// We follow the same logic that's implemented in the matter-labs-tester where they
// resolve the function name into a function selector and they assume that he
// function doesn't have any existing overloads.
// Overloads are handled by providing the full function signature in the "function
// name".
// https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190
let selector =
if function_name.contains('(') && function_name.contains(')') {
Function::parse(function_name)
let selector = if function_name.contains('(') && function_name.contains(')') {
Function::parse(function_name)
.context(
"Failed to parse the provided function name into a function signature",
)?
.selector()
} else {
abi.functions()
} else {
abi.functions()
.find(|function| function.signature().starts_with(function_name))
.ok_or_else(|| {
anyhow::anyhow!(
@@ -511,19 +511,21 @@ impl FunctionCallStep {
&self.instance
)
})
.with_context(|| format!(
"Failed to resolve function selector for {:?} on instance {:?}",
function_name, &self.instance
))?
.with_context(|| {
format!(
"Failed to resolve function selector for {:?} on instance {:?}",
function_name, &self.instance
)
})?
.selector()
};
};
// Allocating a vector that we will be using for the calldata. The vector size will be:
// 4 bytes for the function selector.
// Allocating a vector that we will be using for the calldata. The vector size will
// be: 4 bytes for the function selector.
// function.inputs.len() * 32 bytes for the arguments (each argument is a U256).
//
// We're using indices in the following code in order to avoid the need for us to allocate
// a new buffer for each one of the resolved arguments.
// We're using indices in the following code in order to avoid the need for us to
// allocate a new buffer for each one of the resolved arguments.
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
calldata.extend(selector.0);
self.calldata
@@ -959,9 +961,12 @@ impl<'de> Deserialize<'de> for EtherValue {
#[cfg(test)]
mod tests {
use alloy::primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
use alloy::sol_types::SolValue;
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
use alloy::{
eips::BlockNumberOrTag,
json_abi::JsonAbi,
primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address},
sol_types::SolValue,
};
use std::{collections::HashMap, pin::Pin};
use super::*;
+6 -6
View File
@@ -1,10 +1,10 @@
use std::collections::HashMap;
use std::pin::Pin;
use std::{collections::HashMap, pin::Pin};
use alloy::eips::BlockNumberOrTag;
use alloy::json_abi::JsonAbi;
use alloy::primitives::TxHash;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy::{
eips::BlockNumberOrTag,
json_abi::JsonAbi,
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, U256},
};
use anyhow::Result;
use crate::metadata::{ContractIdent, ContractInstance};
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "ml-test-runner"
description = "ML-based test runner for executing differential tests file by file"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
[[bin]]
name = "ml-test-runner"
path = "src/main.rs"
[dependencies]
revive-dt-common = { workspace = true }
revive-dt-compiler = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-core = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-node = { workspace = true }
revive-dt-node-interaction = { workspace = true }
revive-dt-report = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
tokio = { workspace = true }
temp-dir = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[lints]
workspace = true
+74
View File
@@ -0,0 +1,74 @@
# ML Test Runner
A test runner for executing Revive differential tests file-by-file with cargo-test-style output.
This is similar to the `retester` binary but designed for ML-based test execution with a focus on:
- Running tests file-by-file (rather than in bulk)
- Caching passed tests to skip them in future runs
- Providing cargo-test-style output for easy integration with ML pipelines
- Single platform testing (rather than differential testing)
## Features
- **File-by-file execution**: Run tests on individual `.sol` files, corpus files (`.json`), or recursively walk directories
- **Cached results**: Skip tests that have already passed using `--cached-passed`
- **Fail fast**: Stop on first failure with `--bail`
- **Cargo-like output**: Familiar test output format with colored pass/fail indicators
- **Platform support**: Test against `geth` or `kitchensink` platforms
## Usage
```bash
# Run a single .sol file (compile-only mode, default)
./ml-test-runner path/to/test.sol --platform geth
# Run all tests in a corpus file
./ml-test-runner path/to/corpus.json --platform kitchensink
# Walk a directory recursively for .sol files
./ml-test-runner path/to/tests/ --platform geth
# Use cached results and bail on first failure
./ml-test-runner path/to/tests/ --cached-passed ./cache.txt --bail
# Start the platform and execute tests (full mode)
./ml-test-runner path/to/tests/ --platform geth --start-platform
# Enable verbose logging (info, debug, or trace level)
RUST_LOG=info ./ml-test-runner path/to/tests/
RUST_LOG=debug ./ml-test-runner path/to/tests/ --start-platform
RUST_LOG=trace ./ml-test-runner path/to/tests/ --start-platform
```
## Arguments
- `<PATH>` - Path to test file (`.sol`), corpus file (`.json`), or folder of `.sol` files
- `--cached-passed <FILE>` - File to track tests that have already passed
- `--bail` - Stop after the first file failure
- `--platform <PLATFORM>` - Platform to test against (`geth`, `kitchensink`, or `zombienet`, default: `geth`)
- `--start-platform` - Start the platform and execute tests (default: `false`, compile-only mode)
## Output Format
The runner produces cargo-test-style output:
```
test path/to/test1.sol ... ok
test path/to/test2.sol ... FAILED
test path/to/test3.sol ... cached
failures:
---- path/to/test2.sol ----
Error: ...
test result: FAILED. 1 passed; 1 failed; 1 cached; finished in 2.34s
```
## Building
```bash
cargo build --release -p ml-test-runner
```
The binary will be available at `target/release/ml-test-runner`.
+541
View File
@@ -0,0 +1,541 @@
use anyhow::Context;
use clap::Parser;
use revive_dt_common::{
iterators::FilesWithExtensionIterator,
types::{PlatformIdentifier, PrivateKeyAllocator},
};
use revive_dt_config::TestExecutionContext;
use revive_dt_core::{
CachedCompiler, Platform,
helpers::{TestDefinition, TestPlatformInformation},
};
use revive_dt_format::{
case::CaseIdx,
corpus::Corpus,
metadata::{Metadata, MetadataFile},
};
use std::{
borrow::Cow,
collections::{BTreeMap, HashSet},
fs::File,
io::{BufRead, BufReader, BufWriter, Write},
path::{Path, PathBuf},
sync::Arc,
time::Instant,
};
use temp_dir::TempDir;
use tokio::sync::Mutex;
use tracing::info;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
/// ML-based test runner for executing differential tests file by file
#[derive(Debug, Parser)]
#[command(name = "ml-test-runner")]
struct MlTestRunnerArgs {
/// Path to test file (.sol), corpus file (.json), or folder containing .sol files
#[arg(value_name = "PATH")]
path: PathBuf,
/// File to cache tests that have already passed
#[arg(long = "cached-passed")]
cached_passed: Option<PathBuf>,
/// Stop after the first file failure
#[arg(long = "bail")]
bail: bool,
/// Platform to test against (e.g., geth-evm-solc, kitchensink-polkavm-resolc)
#[arg(long = "platform", default_value = "geth-evm-solc")]
platform: PlatformIdentifier,
/// Start the platform and wait for RPC readiness
#[arg(long = "start-platform", default_value = "false")]
start_platform: bool,
/// Private key to use for wallet initialization (hex string with or without 0x prefix)
#[arg(
long = "private-key",
default_value = "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133"
)]
private_key: String,
/// RPC port to connect to when using existing node
#[arg(long = "rpc-port", default_value = "8545")]
rpc_port: u16,
}
fn main() -> anyhow::Result<()> {
let subscriber = FmtSubscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber");
let args = MlTestRunnerArgs::parse();
info!("ML test runner starting");
info!("Platform: {:?}", args.platform);
info!("Start platform: {}", args.start_platform);
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(run(args))
}
async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> {
let start_time = Instant::now();
info!("Discovering test files from: {}", args.path.display());
let test_files = discover_test_files(&args.path)?;
info!("Found {} test file(s)", test_files.len());
let cached_passed = if let Some(cache_file) = &args.cached_passed {
let cached = load_cached_passed(cache_file)?;
info!("Loaded {} cached passed test(s)", cached.len());
cached
} else {
HashSet::new()
};
let cached_passed = Arc::new(Mutex::new(cached_passed));
let mut passed_files = 0;
let mut failed_files = 0;
let mut skipped_files = 0;
let mut failures = Vec::new();
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const YELLOW: &str = "\x1B[33m";
const COLOUR_RESET: &str = "\x1B[0m";
const BOLD: &str = "\x1B[1m";
const BOLD_RESET: &str = "\x1B[22m";
for test_file in test_files {
let file_display = test_file.display().to_string();
// Check if already passed
{
let cache = cached_passed.lock().await;
if cache.contains(&file_display) {
println!("test {} ... {YELLOW}cached{COLOUR_RESET}", file_display);
skipped_files += 1;
continue;
}
}
info!("Loading metadata from: {}", test_file.display());
let metadata_file = match load_metadata_file(&test_file) {
Ok(mf) => {
info!("Loaded metadata with {} case(s)", mf.cases.len());
mf
}
Err(e) => {
println!("test {} ... {RED}FAILED{COLOUR_RESET}", file_display);
println!(" Error loading metadata: {}", e);
failed_files += 1;
failures.push((
file_display.clone(),
format!("Error loading metadata: {}", e),
));
if args.bail {
break;
}
continue;
}
};
info!("Executing test file: {}", file_display);
match execute_test_file(&args, &metadata_file).await {
Ok(_) => {
println!("test {} ... {GREEN}ok{COLOUR_RESET}", file_display);
info!("Test file passed: {}", file_display);
passed_files += 1;
{
let mut cache = cached_passed.lock().await;
cache.insert(file_display);
}
}
Err(e) => {
println!("test {} ... {RED}FAILED{COLOUR_RESET}", file_display);
failed_files += 1;
failures.push((file_display, format!("{:?}", e)));
if args.bail {
info!("Bailing after first failure");
break;
}
}
}
}
if let Some(cache_file) = &args.cached_passed {
let cache = cached_passed.lock().await;
info!("Saving {} cached passed test(s)", cache.len());
save_cached_passed(cache_file, &cache)?;
}
// Print summary
println!();
if !failures.is_empty() {
println!("{BOLD}failures:{BOLD_RESET}");
println!();
for (file, error) in &failures {
println!("---- {} ----", file);
println!("{}", error);
println!();
}
}
let elapsed = start_time.elapsed();
println!(
"test result: {}. {} passed; {} failed; {} cached; finished in {:.2}s",
if failed_files == 0 {
format!("{GREEN}ok{COLOUR_RESET}")
} else {
format!("{RED}FAILED{COLOUR_RESET}")
},
passed_files,
failed_files,
skipped_files,
elapsed.as_secs_f64()
);
if failed_files > 0 {
std::process::exit(1);
}
Ok(())
}
/// Discover test files from the given path
fn discover_test_files(path: &Path) -> anyhow::Result<Vec<PathBuf>> {
if !path.exists() {
anyhow::bail!("Path does not exist: {}", path.display());
}
let mut files = Vec::new();
if path.is_file() {
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
match extension {
"sol" => {
// Single .sol file
files.push(path.to_path_buf());
}
"json" => {
// Corpus file - enumerate its tests
let corpus = Corpus::try_from_path(path)?;
let metadata_files = corpus.enumerate_tests();
for metadata in metadata_files {
files.push(metadata.metadata_file_path);
}
}
_ => anyhow::bail!(
"Unsupported file extension: {}. Expected .sol or .json",
extension
),
}
} else if path.is_dir() {
// Walk directory recursively for .sol files
for entry in FilesWithExtensionIterator::new(path)
.with_allowed_extension("sol")
.with_use_cached_fs(true)
{
files.push(entry);
}
} else {
anyhow::bail!("Path is neither a file nor a directory: {}", path.display());
}
Ok(files)
}
/// Load metadata from a test file
fn load_metadata_file(path: &Path) -> anyhow::Result<MetadataFile> {
let metadata = Metadata::try_from_file(path)
.ok_or_else(|| anyhow::anyhow!("Failed to load metadata from {}", path.display()))?;
Ok(MetadataFile {
metadata_file_path: path.to_path_buf(),
corpus_file_path: path.to_path_buf(),
content: metadata,
})
}
/// Execute all test cases in a metadata file
async fn execute_test_file(
args: &MlTestRunnerArgs,
metadata_file: &MetadataFile,
) -> anyhow::Result<()> {
if metadata_file.cases.is_empty() {
anyhow::bail!("No test cases found in file");
}
info!("Processing {} test case(s)", metadata_file.cases.len());
// Get the platform based on CLI args
let platform: &dyn Platform = match args.platform {
PlatformIdentifier::GethEvmSolc => &revive_dt_core::GethEvmSolcPlatform,
PlatformIdentifier::LighthouseGethEvmSolc => &revive_dt_core::LighthouseGethEvmSolcPlatform,
PlatformIdentifier::KitchensinkPolkavmResolc => {
&revive_dt_core::KitchensinkPolkavmResolcPlatform
}
PlatformIdentifier::KitchensinkRevmSolc => &revive_dt_core::KitchensinkRevmSolcPlatform,
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
&revive_dt_core::ReviveDevNodePolkavmResolcPlatform
}
PlatformIdentifier::ReviveDevNodeRevmSolc => &revive_dt_core::ReviveDevNodeRevmSolcPlatform,
PlatformIdentifier::ZombienetPolkavmResolc => {
&revive_dt_core::ZombienetPolkavmResolcPlatform
}
PlatformIdentifier::ZombienetRevmSolc => &revive_dt_core::ZombienetRevmSolcPlatform,
};
let temp_dir = TempDir::new()?;
info!("Created temporary directory: {}", temp_dir.path().display());
let test_context = TestExecutionContext::default();
let context = revive_dt_config::Context::Test(Box::new(test_context));
let node: &'static dyn revive_dt_node_interaction::EthereumNode = if args.start_platform {
info!("Starting blockchain node...");
let node_handle = platform
.new_node(context.clone())
.context("Failed to spawn node thread")?;
info!("Waiting for node to start...");
let node = node_handle
.join()
.map_err(|e| anyhow::anyhow!("Node thread panicked: {:?}", e))?
.context("Failed to start node")?;
info!(
"Node started with ID: {}, connection: {}",
node.id(),
node.connection_string()
);
let node = Box::leak(node);
info!("Running pre-transactions...");
node.pre_transactions()
.await
.context("Failed to run pre-transactions")?;
info!("Pre-transactions completed");
node
} else {
info!("Using existing node");
let existing_node: Box<dyn revive_dt_node_interaction::EthereumNode> = match args.platform {
PlatformIdentifier::GethEvmSolc | PlatformIdentifier::LighthouseGethEvmSolc => {
Box::new(
revive_dt_node::node_implementations::geth::GethNode::new_existing(
&args.private_key,
args.rpc_port,
)
.await?,
)
}
PlatformIdentifier::KitchensinkPolkavmResolc
| PlatformIdentifier::KitchensinkRevmSolc
| PlatformIdentifier::ReviveDevNodePolkavmResolc
| PlatformIdentifier::ReviveDevNodeRevmSolc
| PlatformIdentifier::ZombienetPolkavmResolc
| PlatformIdentifier::ZombienetRevmSolc => Box::new(
revive_dt_node::node_implementations::substrate::SubstrateNode::new_existing(
&args.private_key,
args.rpc_port,
)
.await?,
),
};
Box::leak(existing_node)
};
info!("Initializing cached compiler");
let cached_compiler = CachedCompiler::new(temp_dir.path().join("compilation_cache"), false)
.await
.map(Arc::new)
.context("Failed to create cached compiler")?;
let private_key_allocator = Arc::new(Mutex::new(PrivateKeyAllocator::new(
alloy::primitives::U256::from(100),
)));
let (reporter, report_task) =
revive_dt_report::ReportAggregator::new(context.clone()).into_task();
tokio::spawn(report_task);
info!(
"Building test definitions for {} case(s)",
metadata_file.cases.len()
);
let mut test_definitions = Vec::new();
for (case_idx, case) in metadata_file.cases.iter().enumerate() {
info!("Building test definition for case {}", case_idx);
let test_def = build_test_definition(
metadata_file,
case,
case_idx,
platform,
node,
&context,
&reporter,
)
.await?;
if let Some(test_def) = test_def {
info!("Test definition for case {} created successfully", case_idx);
test_definitions.push(test_def);
}
}
info!("Executing {} test definition(s)", test_definitions.len());
for (idx, test_definition) in test_definitions.iter().enumerate() {
info!("─────────────────────────────────────────────────────────────────");
info!(
"Executing case {}/{}: case_idx={}, mode={}, steps={}",
idx + 1,
test_definitions.len(),
test_definition.case_idx,
test_definition.mode,
test_definition.case.steps.len()
);
info!("Creating driver for case {}", test_definition.case_idx);
let driver = revive_dt_core::differential_tests::Driver::new_root(
test_definition,
private_key_allocator.clone(),
&cached_compiler,
)
.await
.context("Failed to create driver")?;
info!(
"Running {} step(s) for case {}",
test_definition.case.steps.len(),
test_definition.case_idx
);
let steps_executed = driver.execute_all().await.context(format!(
"Failed to execute case {}",
test_definition.case_idx
))?;
info!(
"✓ Case {} completed successfully, executed {} step(s)",
test_definition.case_idx, steps_executed
);
}
info!("─────────────────────────────────────────────────────────────────");
info!(
"All {} test case(s) executed successfully",
test_definitions.len()
);
Ok(())
}
/// Build a test definition for a single test case
async fn build_test_definition<'a>(
metadata_file: &'a MetadataFile,
case: &'a revive_dt_format::case::Case,
case_idx: usize,
platform: &'a dyn Platform,
node: &'a dyn revive_dt_node_interaction::EthereumNode,
context: &revive_dt_config::Context,
reporter: &revive_dt_report::Reporter,
) -> anyhow::Result<Option<TestDefinition<'a>>> {
let mode = case
.modes
.as_ref()
.or(metadata_file.modes.as_ref())
.and_then(|modes| modes.first())
.and_then(|parsed_mode| parsed_mode.to_modes().next())
.map(Cow::Owned)
.or_else(|| revive_dt_compiler::Mode::all().next().map(Cow::Borrowed))
.unwrap();
let compiler = platform
.new_compiler(context.clone(), mode.version.clone().map(Into::into))
.await
.context("Failed to create compiler")?;
let test_reporter =
reporter.test_specific_reporter(Arc::new(revive_dt_report::TestSpecifier {
solc_mode: mode.as_ref().clone(),
metadata_file_path: metadata_file.metadata_file_path.clone(),
case_idx: CaseIdx::new(case_idx),
}));
let execution_reporter =
test_reporter.execution_specific_reporter(node.id(), platform.platform_identifier());
let mut platforms = BTreeMap::new();
platforms.insert(
platform.platform_identifier(),
TestPlatformInformation {
platform,
node,
compiler,
reporter: execution_reporter,
},
);
let test_definition = TestDefinition {
metadata: metadata_file,
metadata_file_path: &metadata_file.metadata_file_path,
mode,
case_idx: CaseIdx::new(case_idx),
case,
platforms,
reporter: test_reporter,
};
if let Err((reason, _)) = test_definition.check_compatibility() {
println!(" Skipping case {}: {}", case_idx, reason);
return Ok(None);
}
Ok(Some(test_definition))
}
/// Load cached passed tests from file
fn load_cached_passed(path: &Path) -> anyhow::Result<HashSet<String>> {
if !path.exists() {
return Ok(HashSet::new());
}
let file = File::open(path).context("Failed to open cached-passed file")?;
let reader = BufReader::new(file);
let mut cache = HashSet::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if !trimmed.is_empty() {
cache.insert(trimmed.to_string());
}
}
Ok(cache)
}
/// Save cached passed tests to file
fn save_cached_passed(path: &Path, cache: &HashSet<String>) -> anyhow::Result<()> {
let file = File::create(path).context("Failed to create cached-passed file")?;
let mut writer = BufWriter::new(file);
let mut entries: Vec<_> = cache.iter().collect();
entries.sort();
for entry in entries {
writeln!(writer, "{}", entry)?;
}
writer.flush()?;
Ok(())
}
+11 -21
View File
@@ -1,13 +1,14 @@
//! This crate implements all node interactions.
use std::pin::Pin;
use std::sync::Arc;
use std::{pin::Pin, sync::Arc};
use alloy::network::Ethereum;
use alloy::primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256};
use alloy::providers::DynProvider;
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
use alloy::{
primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
rpc::types::{
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace},
},
};
use anyhow::Result;
use futures::Stream;
@@ -77,8 +78,9 @@ pub trait EthereumNode {
>,
>;
fn provider(&self)
-> Pin<Box<dyn Future<Output = anyhow::Result<DynProvider<Ethereum>>> + '_>>;
/// Checks if the provided address is in the wallet. If it is, returns the address.
/// Otherwise, returns the default signer's address.
fn resolve_signer_or_default(&self, address: Address) -> Address;
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -97,16 +99,4 @@ pub struct MinedBlockInformation {
/// The hashes of the transactions that were mined as part of the block.
pub transaction_hashes: Vec<TxHash>,
/// The ref time for substrate based chains.
pub ref_time: u128,
/// The max ref time for substrate based chains.
pub max_ref_time: u64,
/// The proof size for substrate based chains.
pub proof_size: u128,
/// The max proof size for substrate based chains.
pub max_proof_size: u64,
}
-1
View File
@@ -29,7 +29,6 @@ serde_yaml_ng = { workspace = true }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
subxt = { workspace = true }
zombienet-sdk = { workspace = true }
[dev-dependencies]
+153 -42
View File
@@ -18,7 +18,9 @@ use alloy::{
eips::BlockNumberOrTag,
genesis::{Genesis, GenesisAccount},
network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
primitives::{
Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, StorageKey, TxHash, U256,
},
providers::{
Provider,
ext::DebugApi,
@@ -32,7 +34,7 @@ use alloy::{
},
};
use anyhow::Context as _;
use futures::{FutureExt, Stream, StreamExt};
use futures::{Stream, StreamExt};
use revive_common::EVMVersion;
use tokio::sync::OnceCell;
use tracing::{Instrument, error, instrument};
@@ -75,6 +77,7 @@ pub struct GethNode {
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager,
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
chain_id: ChainId,
}
impl GethNode {
@@ -125,12 +128,123 @@ impl GethNode {
wallet: wallet.clone(),
nonce_manager: Default::default(),
provider: Default::default(),
chain_id: CHAIN_ID,
}
}
pub async fn new_existing(private_key: &str, rpc_port: u16) -> anyhow::Result<Self> {
use alloy::{
primitives::FixedBytes,
providers::{Provider, ProviderBuilder},
signers::local::PrivateKeySigner,
};
let key_str = private_key
.trim()
.strip_prefix("0x")
.unwrap_or(private_key.trim());
let key_bytes = alloy::hex::decode(key_str)
.map_err(|e| anyhow::anyhow!("Failed to decode private key hex: {}", e))?;
if key_bytes.len() != 32 {
anyhow::bail!(
"Private key must be 32 bytes (64 hex characters), got {}",
key_bytes.len()
);
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&key_bytes);
let signer = PrivateKeySigner::from_bytes(&FixedBytes(bytes))
.map_err(|e| anyhow::anyhow!("Failed to create signer from private key: {}", e))?;
let address = signer.address();
let wallet = Arc::new(EthereumWallet::new(signer));
let connection_string = format!("http://localhost:{}", rpc_port);
let chain_id = ProviderBuilder::new()
.connect_http(connection_string.parse()?)
.get_chain_id()
.await
.context("Failed to query chain ID from RPC")?;
let node = Self {
connection_string: format!("http://localhost:{}", rpc_port),
base_directory: PathBuf::new(),
data_directory: PathBuf::new(),
logs_directory: PathBuf::new(),
geth: PathBuf::new(),
id: 0,
chain_id,
handle: None,
start_timeout: Duration::from_secs(0),
wallet,
nonce_manager: Default::default(),
provider: Default::default(),
};
// Check balance and fund if needed
node.ensure_funded(address).await?;
Ok(node)
}
/// Ensure that the given address has at least 1000 ETH, funding it from the node's managed
/// account if necessary.
async fn ensure_funded(&self, address: Address) -> anyhow::Result<()> {
use alloy::{
primitives::utils::{format_ether, parse_ether},
providers::{Provider, ProviderBuilder},
};
let provider = ProviderBuilder::new().connect_http(self.connection_string.parse()?);
let balance = provider.get_balance(address).await?;
let min_balance = parse_ether("1000")?;
if balance >= min_balance {
tracing::info!(
"Wallet {} already has sufficient balance: {} ETH",
address,
format_ether(balance)
);
return Ok(());
}
tracing::info!(
"Funding wallet {} (current: {} ETH, target: 1000 ETH)",
address,
format_ether(balance)
);
// Get the node's managed account
let accounts = provider.get_accounts().await?;
if accounts.is_empty() {
anyhow::bail!("No managed accounts available on the node to fund wallet");
}
let from_account = accounts[0];
let funding_amount = min_balance - balance;
let tx = TransactionRequest::default()
.from(from_account)
.to(address)
.value(funding_amount);
provider
.send_transaction(tx)
.await?
.get_receipt()
.await
.context("Failed to get receipt for funding transaction")?;
tracing::info!("Successfully funded wallet {}", address);
Ok(())
}
/// Create the node directory and call `geth init` to configure the genesis.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: Genesis) -> anyhow::Result<&mut Self> {
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
@@ -139,7 +253,16 @@ impl GethNode {
create_dir_all(&self.logs_directory)
.context("Failed to create logs directory for geth node")?;
let genesis = Self::node_genesis(genesis, self.wallet.as_ref());
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
// Note, the use of the entry API here means that we only modify the entries for any
// account that is not in the `alloc` field of the genesis state.
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
let genesis_path = self.base_directory.join(Self::GENESIS_JSON_FILE);
serde_json::to_writer(
File::create(&genesis_path).context("Failed to create geth genesis file")?,
@@ -246,7 +369,7 @@ impl GethNode {
construct_concurrency_limited_provider::<Ethereum, _>(
self.connection_string.as_str(),
FallbackGasFiller::default(),
ChainIdFiller::new(Some(CHAIN_ID)),
ChainIdFiller::new(Some(self.chain_id)),
NonceFiller::new(self.nonce_manager.clone()),
self.wallet.clone(),
)
@@ -256,16 +379,6 @@ impl GethNode {
.await
.cloned()
}
pub fn node_genesis(mut genesis: Genesis, wallet: &EthereumWallet) -> Genesis {
for signer_address in NetworkWallet::<Ethereum>::signer_addresses(&wallet) {
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
genesis
}
}
impl EthereumNode for GethNode {
@@ -350,23 +463,25 @@ impl EthereumNode for GethNode {
.context("Failed to submit transaction to geth node")?;
let transaction_hash = *pending_transaction.tx_hash();
// The following is a fix for the "transaction indexing is in progress" error that we used
// to get. You can find more information on this in the following GH issue in geth
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
// The following is a fix for the "transaction indexing is in progress" error that we
// used to get. You can find more information on this in the following GH issue in
// geth https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
// before we can get the receipt of the transaction it needs to have been indexed by the
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to
// implement a retry mechanism for the receipt to keep retrying to get it until it
// eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None.
// node's indexer. Just because the transaction has been confirmed it doesn't mean that
// it has been indexed. When we call alloy's `get_receipt` it checks if the
// transaction was confirmed. If it has been, then it will call
// `eth_getTransactionReceipt` method which _might_ return the above error if the tx
// has not yet been indexed yet. So, we need to implement a retry mechanism for the
// receipt to keep retrying to get it until it eventually works, but we only do that
// if the error we get back is the "transaction indexing is in progress" error or if
// the receipt is None.
//
// Getting the transaction indexed and taking a receipt can take a long time especially when
// a lot of transactions are being submitted to the node. Thus, while initially we only
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
// backoff each time we attempt to get the receipt and find that it's not available.
// Getting the transaction indexed and taking a receipt can take a long time especially
// when a lot of transactions are being submitted to the node. Thus, while initially
// we only allowed for 60 seconds of waiting with a 1 second delay in polling, we
// need to allow for a larger wait time. Therefore, in here we allow for 5 minutes of
// waiting with exponential backoff each time we attempt to get the receipt and find
// that it's not available.
poll(
Self::RECEIPT_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)),
@@ -536,10 +651,6 @@ impl EthereumNode for GethNode {
.as_hashes()
.expect("Must be hashes")
.to_vec(),
ref_time: 0,
max_ref_time: 0,
proof_size: 0,
max_proof_size: 0,
})
});
@@ -548,14 +659,14 @@ impl EthereumNode for GethNode {
})
}
fn provider(
&self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::providers::DynProvider<Ethereum>>> + '_>>
{
Box::pin(
self.provider()
.map(|provider| provider.map(|provider| provider.erased())),
)
fn resolve_signer_or_default(&self, address: Address) -> Address {
let signer_addresses: Vec<_> =
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet).collect();
if signer_addresses.contains(&address) {
address
} else {
self.wallet.default_signer().address()
}
}
}
@@ -43,7 +43,7 @@ use alloy::{
},
};
use anyhow::Context as _;
use futures::{FutureExt, Stream, StreamExt};
use futures::{Stream, StreamExt};
use revive_common::EVMVersion;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::serde_as;
@@ -222,7 +222,6 @@ impl LighthouseGethNode {
"--ws.port=8546".to_string(),
"--ws.api=eth,net,web3,txpool,engine".to_string(),
"--ws.origins=*".to_string(),
"--miner.gaslimit=30000000".to_string(),
],
consensus_layer_extra_parameters: vec![
"--disable-quic".to_string(),
@@ -248,8 +247,6 @@ impl LighthouseGethNode {
.collect::<BTreeMap<_, _>>();
serde_json::to_string(&map).unwrap()
},
gas_limit: 30_000_000,
genesis_gaslimit: 30_000_000,
},
wait_for_finalization: false,
port_publisher: Some(PortPublisherParameters {
@@ -541,16 +538,6 @@ impl LighthouseGethNode {
.await
})
}
pub fn node_genesis(mut genesis: Genesis, wallet: &EthereumWallet) -> Genesis {
for signer_address in NetworkWallet::<Ethereum>::signer_addresses(&wallet) {
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
genesis
}
}
impl EthereumNode for LighthouseGethNode {
@@ -767,10 +754,6 @@ impl EthereumNode for LighthouseGethNode {
.as_hashes()
.expect("Must be hashes")
.to_vec(),
ref_time: 0,
max_ref_time: 0,
proof_size: 0,
max_proof_size: 0,
})
});
@@ -779,14 +762,14 @@ impl EthereumNode for LighthouseGethNode {
})
}
fn provider(
&self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::providers::DynProvider<Ethereum>>> + '_>>
{
Box::pin(
self.http_provider()
.map(|provider| provider.map(|provider| provider.erased())),
)
fn resolve_signer_or_default(&self, address: Address) -> Address {
let signer_addresses: Vec<_> =
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet).collect();
if signer_addresses.contains(&address) {
address
} else {
self.wallet.default_signer().address()
}
}
}
@@ -1062,8 +1045,6 @@ struct NetworkParameters {
pub num_validator_keys_per_node: u64,
pub genesis_delay: u64,
pub genesis_gaslimit: u64,
pub gas_limit: u64,
pub prefunded_accounts: String,
}
@@ -1160,7 +1141,6 @@ mod tests {
}
#[tokio::test]
#[ignore = "Ignored since they take a long time to run"]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (context, node) = new_node();
File diff suppressed because it is too large Load Diff
+264 -196
View File
@@ -3,32 +3,24 @@
//! ## Required Binaries
//! This module requires the following binaries to be compiled and available in your PATH:
//!
//! 1. **polkadot-parachain**:
//! ```bash
//! git clone https://github.com/paritytech/polkadot-sdk.git
//! cd polkadot-sdk
//! cargo build --release --locked -p polkadot-parachain-bin --bin polkadot-parachain
//! ```
//! 1. **polkadot-parachain**: ```bash git clone https://github.com/paritytech/polkadot-sdk.git cd
//! polkadot-sdk cargo build --release --locked -p polkadot-parachain-bin --bin
//! polkadot-parachain ```
//!
//! 2. **eth-rpc** (Revive EVM RPC server):
//! ```bash
//! git clone https://github.com/paritytech/polkadot-sdk.git
//! cd polkadot-sdk
//! cargo build --locked --profile production -p pallet-revive-eth-rpc --bin eth-rpc
//! ```
//! 2. **eth-rpc** (Revive EVM RPC server): ```bash git clone https://github.com/paritytech/polkadot-sdk.git
//! cd polkadot-sdk cargo build --locked --profile production -p pallet-revive-eth-rpc --bin
//! eth-rpc ```
//!
//! 3. **polkadot** (for the relay chain):
//! ```bash
//! # In polkadot-sdk directory
//! cargo build --locked --profile testnet --features fast-runtime --bin polkadot --bin polkadot-prepare-worker --bin polkadot-execute-worker
//! ```
//! 3. **polkadot** (for the relay chain): ```bash # In polkadot-sdk directory cargo build --locked
//! --profile testnet --features fast-runtime --bin polkadot --bin polkadot-prepare-worker --bin
//! polkadot-execute-worker ```
//!
//! Make sure to add the build output directories to your PATH or provide
//! the full paths in your configuration.
use std::{
fs::{create_dir_all, remove_dir_all},
path::{Path, PathBuf},
path::PathBuf,
pin::Pin,
process::{Command, Stdio},
sync::{
@@ -40,7 +32,7 @@ use std::{
use alloy::{
eips::BlockNumberOrTag,
genesis::Genesis,
genesis::{Genesis, GenesisAccount},
network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
providers::{
@@ -55,16 +47,15 @@ use alloy::{
};
use anyhow::Context as _;
use futures::{FutureExt, Stream, StreamExt};
use futures::{Stream, StreamExt};
use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory;
use revive_dt_config::*;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
use serde_json::json;
use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use subxt::{OnlineClient, SubstrateConfig};
use tokio::sync::OnceCell;
use tracing::instrument;
use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt};
@@ -73,19 +64,17 @@ use crate::{
Node,
constants::INITIAL_BALANCE,
helpers::{Process, ProcessReadinessWaitBehavior},
provider_utils::{
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
execute_transaction,
},
node_implementations::substrate::ReviveNetwork,
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
/// A Zombienet network where collator is `polkadot-parachain` node with `eth-rpc` [`ZombieNode`]
/// abstracts away the details of managing the zombienet network and provides an interface to
/// interact with the parachain's Ethereum RPC.
/// A Zombienet network where collator is `polkadot-parachain` node with `eth-rpc`
/// [`ZombieNode`] abstracts away the details of managing the zombienet network and provides
/// an interface to interact with the parachain's Ethereum RPC.
#[derive(Debug, Default)]
pub struct ZombienetNode {
pub struct ZombieNode {
/* Node Identifier */
id: u32,
connection_string: String,
@@ -110,10 +99,10 @@ pub struct ZombienetNode {
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager,
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
provider: OnceCell<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>>,
}
impl ZombienetNode {
impl ZombieNode {
const BASE_DIRECTORY: &str = "zombienet";
const DATA_DIRECTORY: &str = "data";
const LOGS_DIRECTORY: &str = "logs";
@@ -122,8 +111,6 @@ impl ZombienetNode {
const PARACHAIN_ID: u32 = 100;
const ETH_RPC_BASE_PORT: u16 = 8545;
const PROXY_LOG_ENV: &str = "info,eth-rpc=debug";
const ETH_RPC_READY_MARKER: &str = "Running JSON-RPC server";
const EXPORT_CHAINSPEC_COMMAND: &str = "build-spec";
@@ -164,7 +151,7 @@ impl ZombienetNode {
}
}
fn init(&mut self, _: Genesis) -> anyhow::Result<&mut Self> {
fn init(&mut self, genesis: Genesis) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
@@ -174,7 +161,7 @@ impl ZombienetNode {
.context("Failed to create logs directory for zombie node")?;
let template_chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE);
self.prepare_chainspec(template_chainspec_path.clone())?;
self.prepare_chainspec(template_chainspec_path.clone(), genesis)?;
let polkadot_parachain_path = self
.polkadot_parachain_path
.to_str()
@@ -183,35 +170,25 @@ impl ZombienetNode {
let node_rpc_port = Self::NODE_BASE_RPC_PORT + self.id as u16;
let network_config = NetworkConfigBuilder::new()
.with_relaychain(|relay_chain| {
relay_chain
.with_chain("westend-local")
.with_relaychain(|r| {
r.with_chain("westend-local")
.with_default_command("polkadot")
.with_node(|node| node.with_name("alice"))
.with_node(|node| node.with_name("bob"))
})
.with_global_settings(|global_settings| {
// global_settings.with_base_dir(&self.base_directory)
global_settings
})
.with_parachain(|parachain| {
parachain
.with_id(Self::PARACHAIN_ID)
.with_chain_spec_path(template_chainspec_path.to_path_buf())
.with_global_settings(|g| g.with_base_dir(&self.base_directory))
.with_parachain(|p| {
p.with_id(Self::PARACHAIN_ID)
.with_chain_spec_path(template_chainspec_path.to_str().unwrap())
.with_chain("asset-hub-westend-local")
.with_collator(|node_config| {
node_config
.with_name("Collator")
.with_collator(|n| {
n.with_name("Collator")
.with_command(polkadot_parachain_path)
.with_rpc_port(node_rpc_port)
.with_args(vec![
("--pool-limit", u32::MAX.to_string().as_str()).into(),
("--pool-kbytes", u32::MAX.to_string().as_str()).into(),
])
})
})
.build()
.map_err(|err| anyhow::anyhow!("Failed to build zombienet network config: {err:?}"))?;
.map_err(|e| anyhow::anyhow!("Failed to build zombienet network config: {e:?}"))?;
self.node_rpc_port = Some(node_rpc_port);
self.network_config = Some(network_config);
@@ -225,9 +202,6 @@ impl ZombienetNode {
.clone()
.context("Node not initialized, call init() first")?;
// TODO: Look into the possibility of removing this in the future, perhaps by reintroducing
// the blocking runtime abstraction and making it available to the entire program so that we
// don't need to be spawning multiple different runtimes.
let rt = tokio::runtime::Runtime::new().unwrap();
let network = rt.block_on(async {
network_config
@@ -255,7 +229,6 @@ impl ZombienetNode {
.arg(u32::MAX.to_string())
.arg("--rpc-port")
.arg(eth_rpc_port.to_string())
.env("RUST_LOG", Self::PROXY_LOG_ENV)
.stdout(stdout_file)
.stderr(stderr_file);
},
@@ -286,9 +259,70 @@ impl ZombienetNode {
Ok(())
}
fn prepare_chainspec(&mut self, template_chainspec_path: PathBuf) -> anyhow::Result<()> {
let chainspec_json = Self::node_genesis(&self.polkadot_parachain_path, &self.wallet)
.context("Failed to prepare the zombienet chainspec file")?;
fn prepare_chainspec(
&mut self,
template_chainspec_path: PathBuf,
mut genesis: Genesis,
) -> anyhow::Result<()> {
let mut cmd: Command = std::process::Command::new(&self.polkadot_parachain_path);
cmd.arg(Self::EXPORT_CHAINSPEC_COMMAND)
.arg("--chain")
.arg("asset-hub-westend-local");
let output = cmd.output().context("Failed to export the chain-spec")?;
if !output.status.success() {
anyhow::bail!(
"Build chain-spec failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let content = String::from_utf8(output.stdout)
.context("Failed to decode collators chain-spec output as UTF-8")?;
let mut chainspec_json: JsonValue =
serde_json::from_str(&content).context("Failed to parse collators chain spec JSON")?;
let existing_chainspec_balances =
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
.as_array()
.cloned()
.unwrap_or_default();
let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances
.into_iter()
.filter_map(|val| {
if let Some(arr) = val.as_array() {
if arr.len() == 2 {
let account = arr[0].as_str()?.to_string();
let balance = arr[1].as_f64()? as u128;
return Some((account, balance));
}
}
None
})
.collect();
let mut eth_balances = {
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
// Note, the use of the entry API here means that we only modify the entries for any
// account that is not in the `alloc` field of the genesis state.
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
self.extract_balance_from_genesis_file(&genesis)
.context("Failed to extract balances from EVM genesis JSON")?
};
merged_balances.append(&mut eth_balances);
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
json!(merged_balances);
let writer = std::fs::File::create(&template_chainspec_path)
.context("Failed to create template chainspec file")?;
@@ -298,6 +332,21 @@ impl ZombienetNode {
Ok(())
}
fn extract_balance_from_genesis_file(
&self,
genesis: &Genesis,
) -> anyhow::Result<Vec<(String, u128)>> {
genesis
.alloc
.iter()
.try_fold(Vec::new(), |mut vec, (address, acc)| {
let polkadot_address = Self::eth_to_polkadot_address(address);
let balance = acc.balance.try_into()?;
vec.push((polkadot_address, balance));
Ok(vec)
})
}
fn eth_to_polkadot_address(address: &Address) -> String {
let eth_bytes = address.0.0;
@@ -321,12 +370,14 @@ impl ZombienetNode {
Ok(String::from_utf8_lossy(&output).trim().to_string())
}
async fn provider(&self) -> anyhow::Result<ConcreteProvider<Ethereum, Arc<EthereumWallet>>> {
async fn provider(
&self,
) -> anyhow::Result<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>> {
self.provider
.get_or_try_init(|| async move {
construct_concurrency_limited_provider::<Ethereum, _>(
construct_concurrency_limited_provider::<ReviveNetwork, _>(
self.connection_string.as_str(),
FallbackGasFiller::new(u64::MAX, 5_000_000_000, 1_000_000_000),
FallbackGasFiller::new(250_000_000, 5_000_000_000, 1_000_000_000),
ChainIdFiller::default(), // TODO: use CHAIN_ID constant
NonceFiller::new(self.nonce_manager.clone()),
self.wallet.clone(),
@@ -337,47 +388,9 @@ impl ZombienetNode {
.await
.cloned()
}
pub fn node_genesis(
node_path: &Path,
wallet: &EthereumWallet,
) -> anyhow::Result<serde_json::Value> {
let output = Command::new(node_path)
.arg(Self::EXPORT_CHAINSPEC_COMMAND)
.arg("--chain")
.arg("asset-hub-westend-local")
.env_remove("RUST_LOG")
.output()
.context("Failed to export the chainspec of the chain")?;
if !output.status.success() {
anyhow::bail!(
"Substrate-node export-chain-spec failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let content = String::from_utf8(output.stdout)
.context("Failed to decode Substrate export-chain-spec output as UTF-8")?;
let mut chainspec_json = serde_json::from_str::<serde_json::Value>(&content)
.context("Failed to parse Substrate chain spec JSON")?;
let existing_chainspec_balances =
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
.as_array_mut()
.expect("Can't fail");
for address in NetworkWallet::<Ethereum>::signer_addresses(wallet) {
let substrate_address = Self::eth_to_polkadot_address(&address);
let balance = INITIAL_BALANCE;
existing_chainspec_balances.push(json!((substrate_address, balance)));
}
Ok(chainspec_json)
}
}
impl EthereumNode for ZombienetNode {
impl EthereumNode for ZombieNode {
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
Box::pin(async move { Ok(()) })
}
@@ -427,11 +440,17 @@ impl EthereumNode for ZombienetNode {
transaction: alloy::rpc::types::TransactionRequest,
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
Box::pin(async move {
let provider = self
let receipt = self
.provider()
.await
.context("Failed to create the provider")?;
execute_transaction(provider, transaction).await
.context("Failed to create provider for transaction submission")?
.send_transaction(transaction)
.await
.context("Failed to submit transaction to proxy")?
.get_receipt()
.await
.context("Failed to fetch transaction receipt from proxy")?;
Ok(receipt)
})
}
@@ -525,75 +544,33 @@ impl EthereumNode for ZombienetNode {
+ '_,
>,
> {
#[subxt::subxt(runtime_metadata_path = "../../assets/revive_metadata.scale")]
pub mod revive {}
Box::pin(async move {
let substrate_rpc_url = format!("ws://127.0.0.1:{}", self.node_rpc_port.unwrap());
let api = OnlineClient::<SubstrateConfig>::from_url(substrate_rpc_url)
let provider = self
.provider()
.await
.context("Failed to create subxt rpc client")?;
let provider = self.provider().await.context("Failed to create provider")?;
let block_stream = api
.blocks()
.subscribe_all()
.context("Failed to create the provider for block subscription")?;
let mut block_subscription = provider
.watch_full_blocks()
.await
.context("Failed to subscribe to blocks")?;
.context("Failed to create the blocks stream")?;
block_subscription.set_channel_size(0xFFFF);
block_subscription.set_poll_interval(Duration::from_secs(1));
let block_stream = block_subscription.into_stream();
let mined_block_information_stream = block_stream.filter_map(move |block| {
let api = api.clone();
let provider = provider.clone();
async move {
let substrate_block = block.ok()?;
let revive_block = provider
.get_block_by_number(
BlockNumberOrTag::Number(substrate_block.number() as _),
)
.await
.expect("TODO: Remove")
.expect("TODO: Remove");
let used = api
.storage()
.at(substrate_block.reference())
.fetch_or_default(&revive::storage().system().block_weight())
.await
.expect("TODO: Remove");
let block_ref_time = (used.normal.ref_time as u128)
+ (used.operational.ref_time as u128)
+ (used.mandatory.ref_time as u128);
let block_proof_size = (used.normal.proof_size as u128)
+ (used.operational.proof_size as u128)
+ (used.mandatory.proof_size as u128);
let limits = api
.constants()
.at(&revive::constants().system().block_weights())
.expect("TODO: Remove");
let max_ref_time = limits.max_block.ref_time;
let max_proof_size = limits.max_block.proof_size;
Some(MinedBlockInformation {
block_number: substrate_block.number() as _,
block_timestamp: revive_block.header.timestamp,
mined_gas: revive_block.header.gas_used as _,
block_gas_limit: revive_block.header.gas_limit as _,
transaction_hashes: revive_block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
ref_time: block_ref_time,
max_ref_time,
proof_size: block_proof_size,
max_proof_size,
})
}
let mined_block_information_stream = block_stream.filter_map(|block| async {
let block = block.ok()?;
Some(MinedBlockInformation {
block_number: block.number(),
block_timestamp: block.header.timestamp,
mined_gas: block.header.gas_used as _,
block_gas_limit: block.header.gas_limit,
transaction_hashes: block
.transactions
.into_hashes()
.as_hashes()
.expect("Must be hashes")
.to_vec(),
})
});
Ok(Box::pin(mined_block_information_stream)
@@ -601,23 +578,25 @@ impl EthereumNode for ZombienetNode {
})
}
fn provider(
&self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::providers::DynProvider<Ethereum>>> + '_>>
{
Box::pin(
self.provider()
.map(|provider| provider.map(|provider| provider.erased())),
)
fn resolve_signer_or_default(&self, address: Address) -> Address {
let signer_addresses: Vec<_> =
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet).collect();
if signer_addresses.contains(&address) {
address
} else {
self.wallet.default_signer().address()
}
}
}
pub struct ZombieNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> {
pub struct ZombieNodeResolver<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> {
id: u32,
provider: FillProvider<F, P, Ethereum>,
provider: FillProvider<F, P, ReviveNetwork>,
}
impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi for ZombieNodeResolver<F, P> {
impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
for ZombieNodeResolver<F, P>
{
#[instrument(level = "info", skip_all, fields(zombie_node_id = self.id))]
fn chain_id(
&self,
@@ -740,7 +719,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi for ZombieNodeRes
}
}
impl Node for ZombienetNode {
impl Node for ZombieNode {
fn shutdown(&mut self) -> anyhow::Result<()> {
// Kill the eth_rpc process
drop(self.eth_rpc_process.take());
@@ -785,7 +764,7 @@ impl Node for ZombienetNode {
}
}
impl Drop for ZombienetNode {
impl Drop for ZombieNode {
fn drop(&mut self) {
let _ = self.shutdown();
}
@@ -809,9 +788,9 @@ mod tests {
TestExecutionContext::default()
}
pub async fn new_node() -> (TestExecutionContext, ZombienetNode) {
pub async fn new_node() -> (TestExecutionContext, ZombieNode) {
let context = test_config();
let mut node = ZombienetNode::new(
let mut node = ZombieNode::new(
context.polkadot_parachain_configuration.path.clone(),
&context,
);
@@ -829,9 +808,8 @@ mod tests {
(context, node)
}
pub async fn shared_state() -> &'static (TestExecutionContext, Arc<ZombienetNode>) {
static NODE: OnceCell<(TestExecutionContext, Arc<ZombienetNode>)> =
OnceCell::const_new();
pub async fn shared_state() -> &'static (TestExecutionContext, Arc<ZombieNode>) {
static NODE: OnceCell<(TestExecutionContext, Arc<ZombieNode>)> = OnceCell::const_new();
NODE.get_or_init(|| async {
let (context, node) = new_node().await;
@@ -840,14 +818,13 @@ mod tests {
.await
}
pub async fn shared_node() -> &'static Arc<ZombienetNode> {
pub async fn shared_node() -> &'static Arc<ZombieNode> {
&shared_state().await.1
}
}
use utils::{new_node, test_config};
#[tokio::test]
#[ignore = "Ignored for the time being"]
async fn test_transfer_transaction_should_return_receipt() {
let (ctx, node) = new_node().await;
@@ -865,6 +842,97 @@ mod tests {
.expect("Failed to get the receipt for the transfer");
}
#[tokio::test]
async fn test_init_generates_chainspec_with_balances() {
let genesis_content = r#"
{
"alloc": {
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": {
"balance": "1000000000000000000"
},
"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2": {
"balance": "2000000000000000000"
}
}
}
"#;
let context = test_config();
let mut node = ZombieNode::new(
context.polkadot_parachain_configuration.path.clone(),
&context,
);
// Call `init()`
node.init(serde_json::from_str(genesis_content).unwrap())
.expect("init failed");
// Check that the patched chainspec file was generated
let final_chainspec_path = node.base_directory.join(ZombieNode::CHAIN_SPEC_JSON_FILE);
assert!(final_chainspec_path.exists(), "Chainspec file should exist");
let contents =
std::fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec");
// Validate that the Polkadot addresses derived from the Ethereum addresses are in the file
let first_eth_addr = ZombieNode::eth_to_polkadot_address(
&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(),
);
let second_eth_addr = ZombieNode::eth_to_polkadot_address(
&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(),
);
assert!(
contents.contains(&first_eth_addr),
"Chainspec should contain Polkadot address for first Ethereum account"
);
assert!(
contents.contains(&second_eth_addr),
"Chainspec should contain Polkadot address for second Ethereum account"
);
}
#[tokio::test]
async fn test_parse_genesis_alloc() {
// Create test genesis file
let genesis_json = r#"
{
"alloc": {
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" },
"0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" },
"0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" }
}
}
"#;
let context = test_config();
let node = ZombieNode::new(
context.polkadot_parachain_configuration.path.clone(),
&context,
);
let result = node
.extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
.unwrap();
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
assert_eq!(
result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"),
Some(&1_000_000_000_000_000_000u128)
);
assert_eq!(
result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"),
Some(&1_000_000_000_000_000_000u128)
);
assert_eq!(
result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"),
Some(&123_456_789u128)
);
}
#[test]
fn print_eth_to_polkadot_mappings() {
let eth_addresses = vec![
@@ -874,7 +942,7 @@ mod tests {
];
for eth_addr in eth_addresses {
let ss58 = ZombienetNode::eth_to_polkadot_address(&eth_addr.parse().unwrap());
let ss58 = ZombieNode::eth_to_polkadot_address(&eth_addr.parse().unwrap());
println!("Ethereum: {eth_addr} -> Polkadot SS58: {ss58}");
}
@@ -902,7 +970,7 @@ mod tests {
];
for (eth_addr, expected_ss58) in cases {
let result = ZombienetNode::eth_to_polkadot_address(&eth_addr.parse().unwrap());
let result = ZombieNode::eth_to_polkadot_address(&eth_addr.parse().unwrap());
assert_eq!(
result, expected_ss58,
"Mismatch for Ethereum address {eth_addr}"
@@ -914,7 +982,7 @@ mod tests {
fn eth_rpc_version_works() {
// Arrange
let context = test_config();
let node = ZombienetNode::new(
let node = ZombieNode::new(
context.polkadot_parachain_configuration.path.clone(),
&context,
);
@@ -933,7 +1001,7 @@ mod tests {
fn version_works() {
// Arrange
let context = test_config();
let node = ZombienetNode::new(
let node = ZombieNode::new(
context.polkadot_parachain_configuration.path.clone(),
&context,
);
@@ -7,10 +7,6 @@ use alloy::{
transports::TransportResult,
};
// Percentage padding applied to estimated gas (e.g. 120 = 20% padding)
const GAS_ESTIMATE_PADDING_NUMERATOR: u64 = 120;
const GAS_ESTIMATE_PADDING_DENOMINATOR: u64 = 100;
#[derive(Clone, Debug)]
pub struct FallbackGasFiller {
inner: GasFiller,
@@ -60,6 +56,8 @@ where
provider: &P,
tx: &<N as Network>::TransactionRequest,
) -> TransportResult<Self::Fillable> {
// Try to fetch GasFillers “fillable” (gas_price, base_fee, estimate_gas, …)
// If it errors (i.e. tx would revert under eth_estimateGas), swallow it.
match self.inner.prepare(provider, tx).await {
Ok(fill) => Ok(Some(fill)),
Err(_) => Ok(None),
@@ -72,17 +70,8 @@ where
mut tx: alloy::providers::SendableTx<N>,
) -> TransportResult<SendableTx<N>> {
if let Some(fill) = fillable {
let mut tx = self.inner.fill(fill, tx).await?;
if let Some(builder) = tx.as_mut_builder() {
if let Some(estimated) = builder.gas_limit() {
let padded = estimated
.checked_mul(GAS_ESTIMATE_PADDING_NUMERATOR)
.and_then(|v| v.checked_div(GAS_ESTIMATE_PADDING_DENOMINATOR))
.unwrap_or(u64::MAX);
builder.set_gas_limit(padded);
}
}
Ok(tx)
// our inner GasFiller succeeded — use it
self.inner.fill(fill, tx).await
} else {
if let Some(builder) = tx.as_mut_builder() {
builder.set_gas_limit(self.default_gas_limit);
+2 -2
View File
@@ -1,7 +1,7 @@
mod concurrency_limiter;
mod fallback_gas_filler;
mod fallback_gas_provider;
mod provider;
pub use concurrency_limiter::*;
pub use fallback_gas_filler::*;
pub use fallback_gas_provider::*;
pub use provider::*;
+3 -7
View File
@@ -10,7 +10,7 @@ use alloy::{
};
use anyhow::{Context, Result};
use revive_dt_common::futures::{PollingWaitBehavior, poll};
use tracing::{Instrument, debug, info, info_span};
use tracing::debug;
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller};
@@ -44,7 +44,7 @@ where
// requests at any point of time and no more than that. This is done in an effort to stabilize
// the framework from some of the interment issues that we've been seeing related to RPC calls.
static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock<ConcurrencyLimiterLayer> =
LazyLock::new(|| ConcurrencyLimiterLayer::new(500));
LazyLock::new(|| ConcurrencyLimiterLayer::new(10));
let client = ClientBuilder::default()
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
@@ -117,16 +117,12 @@ where
async move {
match provider.get_transaction_receipt(tx_hash).await {
Ok(Some(receipt)) => {
info!("Found the transaction receipt");
Ok(ControlFlow::Break(receipt))
}
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
_ => Ok(ControlFlow::Continue(())),
}
}
},
)
.instrument(info_span!("Polling for receipt", %tx_hash))
.await
.context(format!("Polling for receipt failed for {tx_hash}"))
}
+15 -15
View File
@@ -11,12 +11,12 @@ use std::{
use alloy::primitives::Address;
use anyhow::{Context as _, Result};
use indexmap::IndexMap;
use revive_dt_common::types::{ParsedTestSpecifier, PlatformIdentifier};
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_compiler::{CompilerInput, CompilerOutput, Mode};
use revive_dt_config::Context;
use revive_dt_format::{case::CaseIdx, metadata::ContractInstance};
use revive_dt_format::{case::CaseIdx, corpus::Corpus, metadata::ContractInstance};
use semver::Version;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use serde_with::{DisplayFromStr, serde_as};
use tokio::sync::{
broadcast::{Sender, channel},
@@ -67,7 +67,7 @@ impl ReportAggregator {
RunnerEvent::SubscribeToEvents(event) => {
self.handle_subscribe_to_events_event(*event);
}
RunnerEvent::CorpusDiscovery(event) => {
RunnerEvent::CorpusFileDiscovery(event) => {
self.handle_corpus_file_discovered_event(*event)
}
RunnerEvent::MetadataFileDiscovery(event) => {
@@ -152,8 +152,8 @@ impl ReportAggregator {
let _ = event.tx.send(self.listener_tx.subscribe());
}
fn handle_corpus_file_discovered_event(&mut self, event: CorpusDiscoveryEvent) {
self.report.corpora.extend(event.test_specifiers);
fn handle_corpus_file_discovered_event(&mut self, event: CorpusFileDiscoveryEvent) {
self.report.corpora.push(event.corpus);
}
fn handle_metadata_file_discovery_event(&mut self, event: MetadataFileDiscoveryEvent) {
@@ -415,13 +415,12 @@ impl ReportAggregator {
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize)]
pub struct Report {
/// The context that the tool was started up with.
pub context: Context,
/// The list of corpus files that the tool found.
#[serde_as(as = "Vec<DisplayFromStr>")]
pub corpora: Vec<ParsedTestSpecifier>,
pub corpora: Vec<Corpus>,
/// The list of metadata files that were found by the tool.
pub metadata_files: BTreeSet<MetadataFilePath>,
/// Information relating to each test case.
@@ -441,9 +440,10 @@ impl Report {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
#[derive(Clone, Debug, Serialize, Default)]
pub struct TestCaseReport {
/// Information on the status of the test case and whether it succeeded, failed, or was ignored.
/// Information on the status of the test case and whether it succeeded, failed, or was
/// ignored.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<TestCaseStatus>,
/// Information related to the execution on one of the platforms.
@@ -452,7 +452,7 @@ pub struct TestCaseReport {
/// Information related to the status of the test. Could be that the test succeeded, failed, or that
/// it was ignored.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "status")]
pub enum TestCaseStatus {
/// The test case succeeded.
@@ -476,7 +476,7 @@ pub enum TestCaseStatus {
}
/// Information related to the platform node that's being used to execute the step.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize)]
pub struct TestCaseNodeInformation {
/// The ID of the node that this case is being executed on.
pub id: usize,
@@ -487,7 +487,7 @@ pub struct TestCaseNodeInformation {
}
/// Execution information tied to the platform.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize)]
pub struct ExecutionInformation {
/// Information related to the node assigned to this test case.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -507,7 +507,7 @@ pub struct ExecutionInformation {
}
/// Information related to compilation
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "status")]
pub enum CompilationStatus {
/// The compilation was successful.
+6 -5
View File
@@ -6,11 +6,12 @@ use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use alloy::primitives::Address;
use anyhow::Context as _;
use indexmap::IndexMap;
use revive_dt_common::types::ParsedTestSpecifier;
use revive_dt_common::types::PlatformIdentifier;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use revive_dt_format::metadata::ContractInstance;
use revive_dt_format::metadata::Metadata;
use revive_dt_format::{
corpus::Corpus,
metadata::{ContractInstance, Metadata},
};
use semver::Version;
use tokio::sync::{broadcast, oneshot};
@@ -482,9 +483,9 @@ define_event! {
tx: oneshot::Sender<broadcast::Receiver<ReporterEvent>>
},
/// An event emitted by runners when they've discovered a corpus file.
CorpusDiscovery {
CorpusFileDiscovery {
/// The contents of the corpus file.
test_specifiers: Vec<ParsedTestSpecifier>
corpus: Corpus
},
/// An event emitted by runners when they've discovered a metadata file.
MetadataFileDiscovery {
+21 -5
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Revive Differential Tests - Quick Start Script
# This script clones the test repository, and runs the tool
# This script clones the test repository, sets up the corpus file, and runs the tool
set -e # Exit on any error
@@ -14,6 +14,7 @@ NC='\033[0m' # No Color
# Configuration
TEST_REPO_URL="https://github.com/paritytech/resolc-compiler-tests"
TEST_REPO_DIR="resolc-compiler-tests"
CORPUS_FILE="./corpus.json"
WORKDIR="workdir"
# Optional positional argument: path to polkadot-sdk directory
@@ -67,6 +68,21 @@ else
echo -e "${YELLOW}No polkadot-sdk path provided. Using binaries from $PATH.${NC}"
fi
# Create corpus file with absolute path resolved at runtime
echo -e "${GREEN}Creating corpus file...${NC}"
ABSOLUTE_PATH=$(realpath "$TEST_REPO_DIR/fixtures/solidity/")
cat > "$CORPUS_FILE" << EOF
{
"name": "MatterLabs Solidity Simple, Complex, and Semantic Tests",
"paths": [
"$(realpath "$TEST_REPO_DIR/fixtures/solidity/simple")"
]
}
EOF
echo -e "${GREEN}Corpus file created: $CORPUS_FILE${NC}"
# Create workdir if it doesn't exist
mkdir -p "$WORKDIR"
@@ -77,17 +93,17 @@ echo ""
# Run the tool
cargo build --release;
RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \
--platform revive-dev-node-polkavm-resolc \
--test $(realpath "$TEST_REPO_DIR/fixtures/solidity") \
--platform revive-dev-node-revm-solc \
--corpus "$CORPUS_FILE" \
--working-directory "$WORKDIR" \
--concurrency.number-of-nodes 10 \
--concurrency.number-of-threads 5 \
--concurrency.number-of-concurrent-tasks 500 \
--concurrency.number-of-concurrent-tasks 1000 \
--wallet.additional-keys 100000 \
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
--eth-rpc.path "$ETH_RPC_BIN" \
> logs.log \
2> output.log
2> output.log
echo -e "${GREEN}=== Test run completed! ===${NC}"