@@ -69,7 +69,7 @@ use graph_store_postgres::{ChainHeadUpdateListener, ChainStore, Store, SubgraphS
6969use std:: marker:: PhantomData ;
7070use std:: path:: { Path , PathBuf } ;
7171use std:: sync:: { Arc , Mutex } ;
72- use std:: time:: { Duration , Instant } ;
72+ use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
7373
7474#[ cfg( unix) ]
7575use 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 .
151151pub ( 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.
518521async 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.
856849async fn cleanup (
857850 subgraph_store : & SubgraphStore ,
858851 name : & SubgraphName ,
0 commit comments