Skip to content

Commit cbfbecc

Browse files
gnd(test): Use Qm-prefixed SHA-1 hash as deployment hash
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
1 parent c9aa0dc commit cbfbecc

File tree

1 file changed

+64
-71
lines changed

1 file changed

+64
-71
lines changed

gnd/src/commands/test/runner.rs

Lines changed: 64 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use graph_store_postgres::{ChainHeadUpdateListener, ChainStore, Store, SubgraphS
6969
use std::marker::PhantomData;
7070
use std::path::{Path, PathBuf};
7171
use std::sync::{Arc, Mutex};
72-
use std::time::{Duration, Instant};
72+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7373

7474
#[cfg(unix)]
7575
use pgtemp::PgTempDBBuilder;
@@ -144,28 +144,48 @@ pub(super) struct TestContext {
144144
/// Loaded once at the start of `run_test` and passed to each test,
145145
/// avoiding redundant manifest parsing and noisy log output.
146146
///
147-
/// All tests share the same `hash` (derived from the built manifest path).
148-
/// `cleanup()` removes prior deployments with that hash before each test,
149-
/// so tests MUST run sequentially. If parallelism is ever added, each test
150-
/// will need a unique hash (e.g., by incorporating the test name).
147+
/// All tests share the same `hash` and `subgraph_name` (derived from the built
148+
/// manifest path and a per-run seed), which are unique across runs. This means
149+
/// tests MUST still run sequentially within a single `gnd test` invocation, but
150+
/// sequential `gnd test` invocations never conflict with each other in the store.
151151
pub(super) struct ManifestInfo {
152152
/// The build directory containing compiled WASM, schema, and built manifest.
153153
pub build_dir: PathBuf,
154+
/// Canonical path to the built manifest file (e.g., `build/subgraph.yaml`).
155+
/// Registered as an alias for `hash` in `FileLinkResolver` so that
156+
/// `clone_for_manifest` can resolve the Qm hash to a real filesystem path.
157+
pub manifest_path: PathBuf,
154158
/// Network name from the manifest (e.g., "mainnet").
155159
pub network_name: ChainName,
156160
/// Minimum `startBlock` across all data sources.
157161
pub min_start_block: u64,
158162
/// Override for on-chain block validation when startBlock > 0.
159163
pub start_block_override: Option<BlockPtr>,
160-
/// Deployment hash derived from the built manifest path.
164+
/// Deployment hash derived from the built manifest path and a per-run seed.
165+
/// Unique per run so that concurrent and sequential runs never conflict.
161166
pub hash: DeploymentHash,
162-
/// Subgraph name derived from the manifest's root directory (e.g., "test/my-subgraph").
163-
/// Fixed across all tests so that `cleanup` can always find and remove the
164-
/// previous test's entry — per-test names left dangling FK references that
165-
/// prevented `drop_chain` from clearing the chain head.
167+
/// Subgraph name derived from the manifest's root directory with a per-run seed suffix
168+
/// (e.g., "test/my-subgraph-1739800000000"). Unique per run for the same reason.
166169
pub subgraph_name: SubgraphName,
167170
}
168171

172+
/// Compute a `DeploymentHash` for a local test run from a filesystem path and a seed.
173+
///
174+
/// Produces `"Qm" + hex(sha1(path + '\0' + seed))` — 42 alphanumeric characters that
175+
/// pass `DeploymentHash` validation. Not a real IPFS CIDv0, but visually consistent
176+
/// with one and requires no additional dependencies (`sha1` is already used by `gnd build`).
177+
///
178+
/// The `seed` (typically the current Unix epoch in milliseconds) makes each run produce a
179+
/// distinct hash so sequential or concurrent test runs never collide in the store.
180+
fn deployment_hash_from_path_and_seed(path: &Path, seed: u128) -> Result<DeploymentHash> {
181+
use sha1::{Digest, Sha1};
182+
183+
let input = format!("{}\0{}", path.display(), seed);
184+
let digest = Sha1::digest(input.as_bytes());
185+
let qm = format!("Qm{:x}", digest);
186+
DeploymentHash::new(qm).map_err(|e| anyhow!("Failed to create deployment hash: {}", e))
187+
}
188+
169189
/// Load and pre-compute manifest data for the test run.
170190
///
171191
/// Resolves paths relative to the manifest location, loads the built manifest,
@@ -208,15 +228,16 @@ pub(super) fn load_manifest_info(opt: &TestOpt) -> Result<ManifestInfo> {
208228
None
209229
};
210230

211-
let deployment_id = built_manifest_path.display().to_string();
212-
let hash = DeploymentHash::new(&deployment_id).map_err(|_| {
213-
anyhow!(
214-
"Failed to create deployment hash from path: {}",
215-
deployment_id
216-
)
217-
})?;
231+
// Use Unix epoch millis as a per-run seed so each invocation gets a unique
232+
// deployment hash and subgraph name, avoiding conflicts with previous runs.
233+
let seed = SystemTime::now()
234+
.duration_since(UNIX_EPOCH)
235+
.unwrap_or_default()
236+
.as_millis();
218237

219-
// Derive subgraph name from the root directory (e.g., "my-subgraph" → "test/my-subgraph").
238+
let hash = deployment_hash_from_path_and_seed(&built_manifest_path, seed)?;
239+
240+
// Derive subgraph name from the root directory (e.g., "my-subgraph" → "test/my-subgraph-<seed>").
220241
// Sanitize to alphanumeric + hyphens + underscores for SubgraphName compatibility.
221242
let root_dir_name = manifest_dir
222243
.canonicalize()
@@ -227,11 +248,12 @@ pub(super) fn load_manifest_info(opt: &TestOpt) -> Result<ManifestInfo> {
227248
.chars()
228249
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
229250
.collect::<String>();
230-
let subgraph_name =
231-
SubgraphName::new(format!("test/{}", root_dir_name)).map_err(|e| anyhow!("{}", e))?;
251+
let subgraph_name = SubgraphName::new(format!("test/{}-{}", root_dir_name, seed))
252+
.map_err(|e| anyhow!("{}", e))?;
232253

233254
Ok(ManifestInfo {
234255
build_dir,
256+
manifest_path: built_manifest_path,
235257
network_name,
236258
min_start_block,
237259
start_block_override,
@@ -324,30 +346,14 @@ pub async fn run_single_test(
324346
let logger = make_test_logger(opt.verbose).new(o!("test" => test_file.name.clone()));
325347

326348
// Initialize stores with the network name from the manifest.
327-
let stores = setup_stores(
328-
&logger,
329-
&db,
330-
&manifest_info.network_name,
331-
&manifest_info.subgraph_name,
332-
&manifest_info.hash,
333-
)
334-
.await?;
349+
let stores = setup_stores(&logger, &db, &manifest_info.network_name).await?;
335350

336351
// Create the mock Ethereum chain that will feed our pre-built blocks.
337352
let chain = setup_chain(&logger, blocks.clone(), &stores).await?;
338353

339354
// Wire up all graph-node components (instance manager, provider, registrar, etc.)
340355
// and deploy the subgraph.
341-
let ctx = setup_context(
342-
&logger,
343-
&stores,
344-
&chain,
345-
&manifest_info.build_dir,
346-
manifest_info.hash.clone(),
347-
manifest_info.subgraph_name.clone(),
348-
manifest_info.start_block_override.clone(),
349-
)
350-
.await?;
356+
let ctx = setup_context(&logger, &stores, &chain, manifest_info).await?;
351357

352358
// Populate eth_call cache with mock responses before starting indexer.
353359
// This ensures handlers can successfully retrieve mocked contract call results.
@@ -404,10 +410,9 @@ pub async fn run_single_test(
404410
.await;
405411

406412
// For persistent databases, clean up the deployment after the test so the
407-
// database is left in a clean state. Without this, the last test's deployment
408-
// (which uses a file path as its hash) remains in the DB and breaks unrelated
409-
// unit test suites that call remove_all_subgraphs_for_test_use_only(), since
410-
// they don't set GRAPH_NODE_DISABLE_DEPLOYMENT_HASH_VALIDATION.
413+
// database is left in a clean state. Each run generates a unique hash and
414+
// subgraph name (via the seed), so pre-test cleanup is not needed — only
415+
// post-test cleanup of the current run's deployment.
411416
if db.needs_cleanup() {
412417
cleanup(
413418
&ctx.store,
@@ -513,14 +518,10 @@ impl TestDatabase {
513518
/// - A `StoreBuilder` that runs database migrations and creates connection pools
514519
/// - A chain store for the test chain with a synthetic genesis block (hash=0x0)
515520
///
516-
/// Uses a filtered logger to suppress the expected "Store event stream ended"
517-
/// error that occurs when pgtemp is dropped during cleanup.
518521
async fn setup_stores(
519522
logger: &Logger,
520523
db: &TestDatabase,
521524
network_name: &ChainName,
522-
subgraph_name: &SubgraphName,
523-
hash: &DeploymentHash,
524525
) -> Result<TestStores> {
525526
// Minimal graph-node config: one primary shard, no chain providers.
526527
// The chain→shard mapping defaults to "primary" in StoreBuilder::make_store,
@@ -557,22 +558,7 @@ ingestor = "default"
557558
let network_identifiers: Vec<ChainName> = vec![network_name.clone()];
558559
let network_store = store_builder.network_store(network_identifiers).await;
559560

560-
// Persistent databases accumulate state across test runs and need cleanup.
561-
// Temporary (pgtemp) databases start fresh — no cleanup needed.
562-
//
563-
// Pre-test cleanup: removes stale state left by a previously interrupted run.
564-
// Each test also runs post-test cleanup (at the end of run_single_test) for
565-
// the normal case. Together they ensure the DB is always clean before and
566-
// after each test, even if a previous run was interrupted mid-way.
567561
let block_store = network_store.block_store();
568-
if db.needs_cleanup() {
569-
// Order matters: deployments must be removed before the chain can be dropped,
570-
// because deployment_schemas has a FK constraint on the chains table.
571-
let subgraph_store = network_store.subgraph_store();
572-
cleanup(&subgraph_store, subgraph_name, hash).await.ok();
573-
574-
let _ = block_store.drop_chain(network_name).await;
575-
}
576562

577563
// Synthetic chain identifier — net_version "1" with zero genesis hash.
578564
let ident = ChainIdentifier {
@@ -712,11 +698,14 @@ async fn setup_context(
712698
logger: &Logger,
713699
stores: &TestStores,
714700
chain: &Arc<Chain>,
715-
build_dir: &Path,
716-
hash: DeploymentHash,
717-
subgraph_name: SubgraphName,
718-
start_block_override: Option<BlockPtr>,
701+
manifest_info: &ManifestInfo,
719702
) -> Result<TestContext> {
703+
let build_dir = &manifest_info.build_dir;
704+
let manifest_path = &manifest_info.manifest_path;
705+
let hash = manifest_info.hash.clone();
706+
let subgraph_name = manifest_info.subgraph_name.clone();
707+
let start_block_override = manifest_info.start_block_override.clone();
708+
720709
let env_vars = Arc::new(EnvVars::from_env().unwrap_or_default());
721710
let mock_registry = Arc::new(MetricsRegistry::mock());
722711
let logger_factory = LoggerFactory::new(logger.clone(), None, mock_registry.clone());
@@ -730,9 +719,14 @@ async fn setup_context(
730719
let blockchain_map = Arc::new(blockchain_map);
731720

732721
// FileLinkResolver loads the manifest and WASM from the build directory
733-
// instead of fetching from IPFS. This matches gnd dev's approach.
734-
let link_resolver: Arc<dyn graph::components::link_resolver::LinkResolver> =
735-
Arc::new(FileLinkResolver::with_base_dir(build_dir));
722+
// instead of fetching from IPFS. The alias maps the Qm deployment hash to the
723+
// actual manifest path so that clone_for_manifest can resolve it without
724+
// treating the hash as a filesystem path.
725+
let aliases =
726+
std::collections::HashMap::from([(hash.to_string(), manifest_path.to_path_buf())]);
727+
let link_resolver: Arc<dyn graph::components::link_resolver::LinkResolver> = Arc::new(
728+
FileLinkResolver::new(Some(build_dir.to_path_buf()), aliases),
729+
);
736730

737731
// IPFS client is required by the instance manager constructor but not used
738732
// for manifest loading (FileLinkResolver handles that).
@@ -849,10 +843,9 @@ async fn setup_context(
849843
})
850844
}
851845

852-
/// Remove a previous subgraph deployment and its data.
846+
/// Remove a subgraph deployment and its data after a test run.
853847
///
854-
/// Called before each test to ensure a clean slate. Errors are ignored
855-
/// (the deployment might not exist on first run).
848+
/// Errors are ignored — the deployment is removed on a best-effort basis.
856849
async fn cleanup(
857850
subgraph_store: &SubgraphStore,
858851
name: &SubgraphName,

0 commit comments

Comments
 (0)