Skip to content

Commit a426a32

Browse files
committed
Add tests for subgraph data source trigger scanning
1 parent ca39eb3 commit a426a32

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use std::{collections::BTreeMap, ops::Range, sync::Arc};
2+
3+
use graph::{
4+
blockchain::{
5+
block_stream::{
6+
EntityOperationKind, EntitySourceOperation, SubgraphTriggerScanRange,
7+
TriggersAdapterWrapper,
8+
},
9+
mock::MockTriggersAdapter,
10+
Block, SubgraphFilter, Trigger,
11+
},
12+
components::store::SourceableStore,
13+
data_source::CausalityRegion,
14+
prelude::{BlockHash, BlockNumber, BlockPtr, DeploymentHash, StoreError, Value},
15+
schema::{EntityType, InputSchema},
16+
};
17+
use slog::Logger;
18+
use tonic::async_trait;
19+
20+
pub struct MockSourcableStore {
21+
entities: BTreeMap<BlockNumber, Vec<EntitySourceOperation>>,
22+
schema: InputSchema,
23+
block_ptr: Option<BlockPtr>,
24+
}
25+
26+
impl MockSourcableStore {
27+
pub fn new(
28+
entities: BTreeMap<BlockNumber, Vec<EntitySourceOperation>>,
29+
schema: InputSchema,
30+
block_ptr: Option<BlockPtr>,
31+
) -> Self {
32+
Self {
33+
entities,
34+
schema,
35+
block_ptr,
36+
}
37+
}
38+
39+
pub fn set_block_ptr(&mut self, ptr: BlockPtr) {
40+
self.block_ptr = Some(ptr);
41+
}
42+
43+
pub fn clear_block_ptr(&mut self) {
44+
self.block_ptr = None;
45+
}
46+
47+
pub fn increment_block(&mut self) -> Result<(), &'static str> {
48+
if let Some(ptr) = &self.block_ptr {
49+
let new_number = ptr.number + 1;
50+
self.block_ptr = Some(BlockPtr::new(ptr.hash.clone(), new_number));
51+
Ok(())
52+
} else {
53+
Err("No block pointer set")
54+
}
55+
}
56+
57+
pub fn decrement_block(&mut self) -> Result<(), &'static str> {
58+
if let Some(ptr) = &self.block_ptr {
59+
if ptr.number == 0 {
60+
return Err("Block number already at 0");
61+
}
62+
let new_number = ptr.number - 1;
63+
self.block_ptr = Some(BlockPtr::new(ptr.hash.clone(), new_number));
64+
Ok(())
65+
} else {
66+
Err("No block pointer set")
67+
}
68+
}
69+
}
70+
71+
#[async_trait]
72+
impl SourceableStore for MockSourcableStore {
73+
fn get_range(
74+
&self,
75+
entity_types: Vec<EntityType>,
76+
_causality_region: CausalityRegion,
77+
block_range: Range<BlockNumber>,
78+
) -> Result<BTreeMap<BlockNumber, Vec<EntitySourceOperation>>, StoreError> {
79+
Ok(self
80+
.entities
81+
.range(block_range)
82+
.map(|(block_num, operations)| {
83+
let filtered_ops: Vec<EntitySourceOperation> = operations
84+
.iter()
85+
.filter(|op| entity_types.contains(&op.entity_type))
86+
.cloned()
87+
.collect();
88+
(*block_num, filtered_ops)
89+
})
90+
.filter(|(_, ops)| !ops.is_empty())
91+
.collect())
92+
}
93+
fn input_schema(&self) -> InputSchema {
94+
self.schema.clone()
95+
}
96+
97+
async fn block_ptr(&self) -> Result<Option<BlockPtr>, StoreError> {
98+
Ok(self.block_ptr.clone())
99+
}
100+
}
101+
102+
#[tokio::test]
103+
async fn test_triggers_adapter_with_entities() {
104+
// Create a schema for both User and Post entities
105+
let id = DeploymentHash::new("test_deployment").unwrap();
106+
let schema = InputSchema::parse_latest(
107+
r#"
108+
type User @entity {
109+
id: String!
110+
name: String!
111+
age: Int
112+
}
113+
type Post @entity {
114+
id: String!
115+
title: String!
116+
author: String!
117+
}
118+
"#,
119+
id.clone(),
120+
)
121+
.unwrap();
122+
123+
// Create User entities
124+
let user1 = schema
125+
.make_entity(vec![
126+
("id".into(), Value::String("user1".to_owned())),
127+
("name".into(), Value::String("Alice".to_owned())),
128+
("age".into(), Value::Int(30)),
129+
])
130+
.unwrap();
131+
132+
let user2 = schema
133+
.make_entity(vec![
134+
("id".into(), Value::String("user2".to_owned())),
135+
("name".into(), Value::String("Bob".to_owned())),
136+
("age".into(), Value::Int(25)),
137+
])
138+
.unwrap();
139+
140+
// Create a Post entity (counter-example)
141+
let post = schema
142+
.make_entity(vec![
143+
("id".into(), Value::String("post1".to_owned())),
144+
("title".into(), Value::String("Test Post".to_owned())),
145+
("author".into(), Value::String("user1".to_owned())),
146+
])
147+
.unwrap();
148+
149+
// Create EntitySourceOperation instances
150+
let user_type = schema.entity_type("User").unwrap();
151+
let post_type = schema.entity_type("Post").unwrap();
152+
153+
let entity1 = EntitySourceOperation {
154+
entity_type: user_type.clone(),
155+
entity: user1,
156+
entity_op: EntityOperationKind::Create,
157+
vid: 1,
158+
};
159+
160+
let entity2 = EntitySourceOperation {
161+
entity_type: user_type,
162+
entity: user2,
163+
entity_op: EntityOperationKind::Create,
164+
vid: 2,
165+
};
166+
167+
let post_entity = EntitySourceOperation {
168+
entity_type: post_type,
169+
entity: post,
170+
entity_op: EntityOperationKind::Create,
171+
vid: 3,
172+
};
173+
174+
// Create a BTreeMap with entities at different blocks
175+
let mut entities = BTreeMap::new();
176+
entities.insert(1, vec![entity1, post_entity]); // Block 1 has both User and Post
177+
entities.insert(2, vec![entity2]); // Block 2 has only User
178+
179+
// Create block hash and store
180+
let hash_bytes: [u8; 32] = [0u8; 32];
181+
let block_hash = BlockHash(hash_bytes.to_vec().into_boxed_slice());
182+
let initial_block = BlockPtr::new(block_hash, 0);
183+
let store = Arc::new(MockSourcableStore::new(
184+
entities,
185+
schema.clone(),
186+
Some(initial_block),
187+
));
188+
189+
let adapter = Arc::new(MockTriggersAdapter {});
190+
let wrapper = TriggersAdapterWrapper::new(adapter, vec![store]);
191+
192+
// Filter only for User entities
193+
let filter = SubgraphFilter {
194+
subgraph: id,
195+
start_block: 0,
196+
entities: vec!["User".to_string()], // Only monitoring User entities
197+
};
198+
199+
let logger = Logger::root(slog::Discard, slog::o!());
200+
let result = wrapper
201+
.blocks_with_subgraph_triggers(&logger, &[filter], SubgraphTriggerScanRange::Range(1, 2))
202+
.await;
203+
204+
assert!(result.is_ok(), "Failed to get triggers: {:?}", result.err());
205+
let blocks = result.unwrap();
206+
207+
dbg!(&blocks);
208+
209+
// Detailed assertions
210+
assert_eq!(blocks.len(), 2, "Should have found two blocks");
211+
212+
// Check block 1
213+
let block1 = &blocks[0];
214+
assert_eq!(block1.block.number(), 1, "First block should be number 1");
215+
let triggers1 = &block1.trigger_data;
216+
assert_eq!(
217+
triggers1.len(),
218+
1,
219+
"Block 1 should have exactly one trigger (User, not Post)"
220+
);
221+
222+
// Verify it's the User trigger, not the Post trigger
223+
if let Trigger::Subgraph(trigger_data) = &triggers1[0] {
224+
assert_eq!(
225+
trigger_data.entity.entity_type.as_str(),
226+
"User",
227+
"Trigger should be for User entity"
228+
);
229+
assert_eq!(
230+
trigger_data.entity.vid, 1,
231+
"Should be the first User entity"
232+
);
233+
} else {
234+
panic!("Expected subgraph trigger");
235+
}
236+
237+
// Check block 2
238+
let block2 = &blocks[1];
239+
assert_eq!(block2.block.number(), 2, "Second block should be number 2");
240+
let triggers2 = &block2.trigger_data;
241+
assert_eq!(
242+
triggers2.len(),
243+
1,
244+
"Block 2 should have exactly one trigger"
245+
);
246+
247+
// Verify second block's trigger
248+
if let Trigger::Subgraph(trigger_data) = &triggers2[0] {
249+
assert_eq!(
250+
trigger_data.entity.entity_type.as_str(),
251+
"User",
252+
"Trigger should be for User entity"
253+
);
254+
assert_eq!(
255+
trigger_data.entity.vid, 2,
256+
"Should be the second User entity"
257+
);
258+
} else {
259+
panic!("Expected subgraph trigger");
260+
}
261+
}

0 commit comments

Comments
 (0)