gnd(test): Add mock-based subgraph test runner#6361
gnd(test): Add mock-based subgraph test runner#6361dimitrovmaksim merged 40 commits intographprotocol:masterfrom
Conversation
6c6f19f to
117e2bb
Compare
lutter
left a comment
There was a problem hiding this comment.
Very nice! I think this is a really useful tool
gnd/README.md
Outdated
| | `--recompile` | `-r` | Force recompilation before testing | | ||
| | `--version` | `-v` | Matchstick version to use | | ||
| | `--manifest` | `-m` | Path to subgraph manifest (default: `subgraph.yaml`) | | ||
| | `--skip-build` | | Skip building the subgraph before testing | |
There was a problem hiding this comment.
I would think that people will use this a lot when developing tests; maybe it should have a short option, too?
| | `--docker` | `-d` | Run Matchstick in Docker (requires `--matchstick`) | | ||
| | `--coverage` | `-c` | Run with coverage reporting (requires `--matchstick`) | | ||
| | `--recompile` | `-r` | Force recompilation (requires `--matchstick`) | | ||
| | `--force` | `-f` | Force redownload of Matchstick binary (requires `--matchstick`) | |
There was a problem hiding this comment.
It would be nice to communicate somehow that we want to get rid of Matchstick and give people a bit of time to migrate their tests - it really feels like nobody is using it.
There was a problem hiding this comment.
I can add a warn notification when using matchstick, also add a note about that in the docs.
| } | ||
|
|
||
| /// Assert that `asc` (AssemblyScript compiler) is available in PATH or in local node_modules. | ||
| fn verify_asc_available(subgraph_dir: &Path) { |
There was a problem hiding this comment.
gnd/src/compiler/asc.rs has is_asc_available and get_asc_version. Maybe they can be reused here?
There was a problem hiding this comment.
It seems I'm overwriting is_asc_available in my PR with find_asc_binary
There was a problem hiding this comment.
Ah yes, I remember now. is_asc_available was looking onl for a globally installed asc. I added a lookup into the local node_modules as well.
gnd/tests/gnd_test.rs
Outdated
| copy_dir_recursive(&fixture, &subgraph_dir).expect("Failed to copy fixture to temp directory"); | ||
|
|
||
| // Install npm dependencies (graph-ts, graph-cli) | ||
| let npm_output = Command::new("npm") |
There was a problem hiding this comment.
In other places like integration tests, we already require pnpm, maybe we should do that here, too
| ) | ||
| .await | ||
| .ok(); | ||
| } |
There was a problem hiding this comment.
At least for failed tests, it might be nicer to not clean up afterwards and leave the data in place to make it easier to troubleshoot what went wrong
There was a problem hiding this comment.
This works for persisted DBs. For PgTemp, it is doable, it could be persisted, but then on success needs to be manually deleted, so it kind of becomes an inverted logic.
| /// in a different order — every line shows as changed even if only one field | ||
| /// differs. This function reorders `actual` so that elements are paired with | ||
| /// their closest match in `expected`, producing a diff that highlights only | ||
| /// real value differences. |
gnd/src/commands/test/assertion.rs
Outdated
| /// Convert graph-node's internal `r::Value` (GraphQL result) to `serde_json::Value`. | ||
| /// | ||
| /// Graph-node uses its own value type for GraphQL results. This converts to | ||
| /// standard JSON for comparison with the expected values in the test file. |
There was a problem hiding this comment.
r::Value implements Serialize, so this should not be needed, and by using the existing implementation, we catch any quirks that that serialization might have.
|
|
||
| ### Event Signature Format | ||
|
|
||
| **Important:** Include `indexed` keywords in the signature: |
There was a problem hiding this comment.
That's the exact same form in which the event must be listed in the manifest, right? I wonder if there is a way to make the notation more concise, maybe by allowing the use of <ABI>.<Event>, i.e. something like ERC20.Transfer in addition to the exact event signature. But definitely not something that needs to go into this PR
There was a problem hiding this comment.
Yeah, it is needed to know if the params should be set as topics (and potentially padded to 32 bytes or keccak hashed if dynamically sized) in case they are indexed. But this should be doable, scan the abi, look at the event definition and encode accordignly. This will be a good QOL improvement.
Replaces monolithic gnd/src/commands/test.rs with organized test/ directory containing: - mod.rs: Main entry point and test orchestration - runner.rs: Test execution and infrastructure setup - assertion.rs: GraphQL assertion logic - block_stream.rs: Mock block stream implementation - noop.rs: Stub trait implementations - schema.rs: JSON schema and test types - trigger.rs: ABI encoding for test triggers - output.rs: Test result formatting - mock_chain.rs: Block pointer helpers Updates main.rs to make Test command async (.await). Adds dependencies for test runner (graph-chain-ethereum, graph-graphql, graph-store-postgres).
Adds supporting modules for test infrastructure: - mock_chain: Helpers for block pointer construction - schema: JSON schema types and parsing - output: Console output formatting - trigger: ABI encoding of test triggers
Adds module declarations for refactored components: - mod assertion - mod block_stream - mod noop Updates module documentation to reflect the new structure and improved separation of concerns.
Removes ~500 lines from runner.rs by delegating to new focused modules: - block_stream: Mock block delivery infrastructure - noop: Stub trait implementations - assertion: GraphQL assertion logic runner.rs now focuses exclusively on test orchestration: - setup_stores: Initialize PostgreSQL and chain store - setup_chain: Construct mock Ethereum chain - setup_context: Wire up graph-node components - wait_for_sync: Poll store until indexing completes Reduced from 1198 to 729 lines (39% reduction). Improves readability by separating concerns.
Moves assertion execution to gnd/src/commands/test/assertion.rs: - run_assertions: Execute all test assertions - run_single_assertion: Execute and compare a single query - r_value_to_json: Convert graph-node's r::Value to serde_json - json_equal: Compare JSON with string-vs-number coercion Makes TestContext fields pub(super) to allow assertion module access.
Moves unused adapter stubs to gnd/src/commands/test/noop.rs: - StaticBlockRefetcher - NoopRuntimeAdapter / NoopRuntimeAdapterBuilder - NoopAdapterSelector - NoopTriggersAdapter These satisfy Chain constructor trait bounds but are never called during normal test execution since triggers are pre-built and host functions are not available in mocks.
- Add baseFeePerGas field to TestBlock schema - Parse and apply base fee when creating test blocks - Replace graph-node helper functions with direct alloy types - Extract dummy_transaction creation into dedicated function - Use alloy Block::empty() constructor for cleaner block creation
- Rename 'triggers' field to 'events' in TestBlock - Remove TestTrigger enum and BlockTrigger type - Keep LogEvent as the only event type users specify - Auto-inject Start and End block triggers for every block - This ensures block handlers fire correctly without explicit config - Update docs to reflect that block triggers are automatic
- Extract min startBlock from manifest in extract_start_block_from_manifest() - Use startBlock as default test block numbering base - Create start_block_override to bypass on-chain validation - Pass override through setup_context() to SubgraphRegistrar - This allows testing subgraphs that specify startBlock without needing a real chain
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
- Replace positional `manifest` arg with `--manifest` / `-m` flag (default: subgraph.yaml) - Add positional `tests` args accepting file or directory - When no args given, default to scanning `tests/` - Bare filenames resolve to `tests/<filename>` for convenience (e.g., `gnd test foo.json` → `tests/foo.json`) - Remove `--test-dir` flag (replaced by positional args) - Update README with new usage examples Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
…ersion
- Add Array, FixedArray, Tuple branches to json_to_sol_value
- Fix strip_prefix("0x") usage (trim_start_matches incorrectly strips repeated 0s)
- Fix i64::MIN two's complement handling via I256::into_raw()
- Add block number overflow check against i32::MAX
- discover_test_files now walks subdirectories recursively - Skip entries starting with non-alphanumeric chars (.hidden, _fixture)
Add upfront arity checks in encode_function_call and encode_return_value so mismatches produce clear errors instead of silently truncating. Also simplify redundant .with_context() wrappers in populate_single_call.
- Remove FilterStoreEventEndedDrain log filter and unused logger field - Always call stop_subgraph after test, even on error - Warn when a test has blocks but no assertions - Add block number overflow check against i32::MAX
…ker mode Add alphanumeric/hyphen/underscore validation before interpolating the datasource name into Docker's sh -c command. Also simplify redundant .with_context() wrappers.
- block_stream: .unwrap() -> .expect() on mutex lock - noop: unimplemented!() -> Err(anyhow!(...)) - mod: Fail early with bail! on missing test file
Signed-off-by: Maksim Dimitrov <dimitrov.maksim@gmail.com>
Add `-v` / `--verbose` flag with count semantics for controlling graph-node log verbosity during tests (-v=info, -vv=debug, -vvv=trace). GRAPH_LOG env var always takes precedence when set. Extract manifest loading into `ManifestInfo` struct loaded once per run, avoiding redundant parsing across tests. Thread a single logger through setup_stores/setup_chain instead of creating ad-hoc loggers.
Add gnd_test integration test suite with fixture subgraph and test
cases for blocks, transfers, templates, and expected failures. Fix
pgtemp Unix socket path overflow on macOS by overriding
unix_socket_directories to /tmp. Reduce default pool_size to 2.
Makes the two database lifecycle paths explicit and self-documenting. `TestDatabase::Temporary` vs `TestDatabase::Persistent` (--postgres-url, needs cleanup) replaces the opaque `Option<TempPgHandle>`. Cleanup in `setup_stores` is now gated on `db.needs_cleanup()` instead of running unconditionally.
When using --postgres-url, cleanup only ran at the start of each test (to remove stale state from a previous run). The last test's deployment was left in the DB, which broke unrelated unit test suites calling remove_all_subgraphs_for_test_use_only() — they don't set GRAPH_NODE_DISABLE_DEPLOYMENT_HASH_VALIDATION, so parsing the file-path deployment hash fails. Add post-test cleanup for persistent databases, mirroring the pre-test cleanup. Pre-test handles interrupted runs; post-test handles the normal case. Together they keep the DB clean regardless of how the run ends.
Each test run now computes a fake-but-valid DeploymentHash as "Qm" + hex(sha1(manifest_path + seed)) where seed is the Unix epoch in milliseconds. This: - Passes DeploymentHash validation without bypassing it - Produces a unique hash and subgraph name per run, so sequential runs never conflict in the store - Removes the pre-test cleanup (it would never match a fresh hash) - Registers the hash as a FileLinkResolver alias so clone_for_manifest can resolve it to the real manifest path - Reuses the existing sha1 dep — no new dependencies
…efault - Flatten TestResult from enum to struct (handler_error + assertions fields) - Remove AssertionOutcome/TestResult accessor methods; use public fields directly - Remove baseFeePerGas from test block format (YAGNI) - Change default block timestamp from number * 12 to number (chain-agnostic, avoids future timestamps on high-block-number chains like Arbitrum)
9fe6d41 to
bad0c9c
Compare
Update the contract address in the fixture subgraph manifest and transfer.json test from the old placeholder address to the correct one used by the test assertions.
dummy_transaction was returning Transaction<TxEnvelope> instead of the required AnyTransaction (Transaction<AnyTxEnvelope>). Wrap the envelope in AnyTxEnvelope::Ethereum(...) to match the expected type. Also wrap ConsensusHeader in AnyHeader::from(...) when constructing the block Header, producing Header<AnyHeader> as required by LightEthereumBlock::new.
- Add -s short flag for --skip-build - Print deprecation warning when --matchstick is used; update README - Replace npm with pnpm in integration test setup - Replace hand-rolled r_value_to_json with serde_json::to_value via r::Value's built-in Serialize impl (also fixes Timestamp serialization) - Make find_asc_binary pub and re-export from compiler module; reuse it in gnd_test.rs instead of duplicating the lookup logic - Skip DB cleanup on test failure for persistent databases and print the connection URL so the data can be inspected
graph-node returns all hex-encoded values (addresses, tx hashes, bytes) in lowercase. Add a note in the Assertions section's comparison behavior table and in Tips & Best Practices. Event inputs are normalised automatically and can be mixed case. Also fix the transfer.json fixture assertion to use lowercase token ID.
bad0c9c to
6a8fae9
Compare
Summary
Implements a native test runner for
gnd testthat executes JSON-defined tests through real graph-node infrastructure (store, WASM runtime, trigger processing) with only the blockchain layer mocked. Provides a faster, integrated alternative to Matchstick while maintaining backward compatibility.Key Features
once,polling, no filter)--matchstickflag preserves legacy behaviorArchitecture
Design decisions:
StaticStreamBuilder)--postgres-url)Usage
Test Format Example
{ "name": "Transfer creates entity", "blocks": [ { "number": 1, "events": [ { "address": "0x1234...", "event": "Transfer(address indexed from, address indexed to, uint256 value)", "params": {"from": "0xaaaa...", "to": "0xbbbb...", "value": "1000"} } ], "ethCalls": [ { "address": "0x1234...", "function": "balanceOf(address)(uint256)", "params": ["0xaaaa..."], "returns": ["1000000000000000000"] } ] } ], "assertions": [ { "query": "{ transfer(id: \"1\") { from to value } }", "expected": {"transfer": {"from": "0xaaaa...", "to": "0xbbbb...", "value": "1000"}} } ] }Implementation
Modified Graph-Node Files
graph/src/data_source/subgraph.rs- Minor exports for startBlock supportSupported Features
once,polling, no filter)call)Breaking Changes
None. Adds native testing while preserving
--matchstickfor backward compatibility.Code Attribution
Human contributions: Initial requirements, code review, validation, and bug fixes.
Checklist
--matchstickcall)