Skip to content

Commit c97f92a

Browse files
committed
test-store, tests: Add tests for VID collision prevention
Add a unit test that simulates the ipfs.map() pattern (multiple EntityCache instances sharing one SeqGenerator) and verifies VIDs are sequential. Extend the file_data_sources runner test so that two offchain triggers in the same block each create an entity, exercising the shared VID generator end-to-end.
1 parent d121b39 commit c97f92a

File tree

3 files changed

+70
-7
lines changed

3 files changed

+70
-7
lines changed

store/test-store/tests/graph/entity_cache.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,37 @@ async fn offchain_trigger_vid_no_collision_with_shared_generator() {
463463
);
464464
}
465465

466+
// Simulate the ipfs.map() pattern: multiple EntityCache instances each create
467+
// an entity using a shared SeqGenerator. VIDs must be unique and sequential.
468+
#[graph::test]
469+
async fn ipfs_map_pattern_vid_uniqueness() {
470+
let block: i32 = 42;
471+
let vid_gen = SeqGenerator::new(block);
472+
473+
let mut all_vids = Vec::new();
474+
for i in 0..5u32 {
475+
let store = Arc::new(MockStore::new(BTreeMap::new()));
476+
let mut cache = EntityCache::new(store, vid_gen.cheap_clone());
477+
let data = entity! { SCHEMA => id: format!("band{i}"), name: format!("Band {i}") };
478+
let key = make_band_key(&format!("band{i}"));
479+
cache.set(key, data, None).await.unwrap();
480+
let result = cache.as_modifications(block, &STOPWATCH).await.unwrap();
481+
let vid = match &result.modifications[0] {
482+
EntityModification::Insert { data, .. } => data.vid(),
483+
_ => panic!("expected Insert"),
484+
};
485+
all_vids.push(vid);
486+
}
487+
488+
for i in 1..all_vids.len() {
489+
assert_eq!(
490+
all_vids[i],
491+
all_vids[i - 1] + 1,
492+
"VIDs should be sequential"
493+
);
494+
}
495+
}
496+
466497
const ACCOUNT_GQL: &str = "
467498
type Account @entity {
468499
id: ID!

tests/runner-tests/file-data-sources/src/mapping.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ export function handleFile(data: Bytes): void {
126126
let contextCommand = context.getString('command');
127127

128128
if (contextCommand == SPAWN_FDS_FROM_OFFCHAIN_HANDLER) {
129+
// Create an entity for THIS file data source too, so that two offchain
130+
// triggers in the same block each write an entity. This exercises the
131+
// shared VID generator: if VIDs collide the DB will reject the insert.
132+
let entity = new FileEntity(dataSource.stringParam());
133+
entity.content = data.toString();
134+
entity.save();
135+
129136
let hash = context.getString('hash');
130137
log.info('Creating file data source from handleFile: {}', [hash]);
131138
dataSource.createWithContext('File', [hash], new DataSourceContext());

tests/tests/runner_tests.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -673,11 +673,25 @@ async fn file_data_sources() {
673673
assert!(datasources.len() == 1);
674674
}
675675

676-
// Create a File data source from a same type of file data source handler
676+
// Create a File data source from a same type of file data source handler.
677+
// Both hash_2 and hash_3 handlers create entities in the same block,
678+
// exercising the shared VID generator: colliding VIDs would cause a DB
679+
// constraint violation.
677680
{
678681
ctx.start_and_sync_to(test_ptr(4)).await;
679682

680-
let content = "EXAMPLE_3";
683+
let query_res = ctx
684+
.query(&format!(
685+
r#"{{ fileEntity(id: "{}") {{ id, content }} }}"#,
686+
hash_2.clone()
687+
))
688+
.await
689+
.unwrap();
690+
assert_json_eq!(
691+
query_res,
692+
Some(object! { fileEntity: object!{ id: hash_2.clone(), content: "EXAMPLE_2" } })
693+
);
694+
681695
let query_res = ctx
682696
.query(&format!(
683697
r#"{{ fileEntity(id: "{}") {{ id, content }} }}"#,
@@ -687,7 +701,7 @@ async fn file_data_sources() {
687701
.unwrap();
688702
assert_json_eq!(
689703
query_res,
690-
Some(object! { fileEntity: object!{ id: hash_3.clone(), content: content } })
704+
Some(object! { fileEntity: object!{ id: hash_3.clone(), content: "EXAMPLE_3" } })
691705
);
692706
}
693707

@@ -813,11 +827,22 @@ async fn file_data_sources() {
813827

814828
chain.set_block_stream(blocks);
815829

816-
let message = "error while executing at wasm backtrace:\t 0: 0x3490 - <unknown>!generated/schema/Foo#save\t 1: 0x3eb2 - <unknown>!src/mapping/handleFile: entity type `Foo` is not on the 'entities' list for data source `File`. Hint: Add `Foo` to the 'entities' list, which currently is: `FileEntity`. in handler `handleFile` at block #5 () at block #5 (0000000000000000000000000000000000000000000000000000000000000005)";
817-
818830
let err = ctx.start_and_sync_to_error(block_5.ptr()).await;
819-
820-
assert_eq!(err.to_string(), message);
831+
let err_msg = err.to_string();
832+
833+
// Don't check exact wasm hex offsets since they change with any
834+
// modification to the wasm binary.
835+
assert!(
836+
err_msg
837+
.contains("entity type `Foo` is not on the 'entities' list for data source `File`"),
838+
"unexpected error: {err_msg}"
839+
);
840+
assert!(
841+
err_msg.contains(
842+
"Hint: Add `Foo` to the 'entities' list, which currently is: `FileEntity`"
843+
),
844+
"unexpected error: {err_msg}"
845+
);
821846
}
822847
}
823848

0 commit comments

Comments
 (0)