reifydb_flow_operator_sdk/testing/
assertions.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4use reifydb_core::{
5	Row,
6	value::encoded::{EncodedKey, EncodedValuesLayout},
7};
8use reifydb_type::{RowNumber, Value};
9
10use crate::{FlowChange, FlowDiff, testing::TestStateStore};
11
12/// Assertions for FlowChange outputs
13pub struct FlowChangeAssertion<'a> {
14	change: &'a FlowChange,
15}
16
17impl<'a> FlowChangeAssertion<'a> {
18	/// Create a new FlowChange assertion
19	pub fn new(change: &'a FlowChange) -> Self {
20		Self {
21			change,
22		}
23	}
24
25	/// Assert the number of diffs in the change
26	pub fn has_diffs(&self, count: usize) -> &Self {
27		assert_eq!(
28			self.change.diffs.len(),
29			count,
30			"Expected {} diffs, found {}",
31			count,
32			self.change.diffs.len()
33		);
34		self
35	}
36
37	/// Assert the change is empty (no diffs)
38	pub fn is_empty(&self) -> &Self {
39		assert!(self.change.diffs.is_empty(), "Expected empty change, found {} diffs", self.change.diffs.len());
40		self
41	}
42
43	/// Assert the change has at least one insert
44	pub fn has_insert(&self) -> &Self {
45		let has_insert = self.change.diffs.iter().any(|d| matches!(d, FlowDiff::Insert { .. }));
46		assert!(has_insert, "Expected at least one insert diff");
47		self
48	}
49
50	/// Assert the change has at least one update
51	pub fn has_update(&self) -> &Self {
52		let has_update = self.change.diffs.iter().any(|d| matches!(d, FlowDiff::Update { .. }));
53		assert!(has_update, "Expected at least one update diff");
54		self
55	}
56
57	/// Assert the change has at least one remove
58	pub fn has_remove(&self) -> &Self {
59		let has_remove = self.change.diffs.iter().any(|d| matches!(d, FlowDiff::Remove { .. }));
60		assert!(has_remove, "Expected at least one remove diff");
61		self
62	}
63
64	/// Assert a specific diff exists at the given index
65	pub fn diff_at(&self, index: usize) -> DiffAssertion {
66		assert!(
67			index < self.change.diffs.len(),
68			"Diff index {} out of range (total: {})",
69			index,
70			self.change.diffs.len()
71		);
72		DiffAssertion::new(&self.change.diffs[index])
73	}
74
75	/// Get all insert diffs
76	pub fn inserts(&self) -> Vec<&Row> {
77		self.change
78			.diffs
79			.iter()
80			.filter_map(|d| match d {
81				FlowDiff::Insert {
82					post,
83				} => Some(post),
84				_ => None,
85			})
86			.collect()
87	}
88
89	/// Get all update diffs
90	pub fn updates(&self) -> Vec<(&Row, &Row)> {
91		self.change
92			.diffs
93			.iter()
94			.filter_map(|d| match d {
95				FlowDiff::Update {
96					pre,
97					post,
98				} => Some((pre, post)),
99				_ => None,
100			})
101			.collect()
102	}
103
104	/// Get all remove diffs
105	pub fn removes(&self) -> Vec<&Row> {
106		self.change
107			.diffs
108			.iter()
109			.filter_map(|d| match d {
110				FlowDiff::Remove {
111					pre,
112				} => Some(pre),
113				_ => None,
114			})
115			.collect()
116	}
117
118	/// Assert the number of inserts
119	pub fn has_inserts(&self, count: usize) -> &Self {
120		let actual = self.inserts().len();
121		assert_eq!(actual, count, "Expected {} inserts, found {}", count, actual);
122		self
123	}
124
125	/// Assert the number of updates
126	pub fn has_updates(&self, count: usize) -> &Self {
127		let actual = self.updates().len();
128		assert_eq!(actual, count, "Expected {} updates, found {}", count, actual);
129		self
130	}
131
132	/// Assert the number of removes
133	pub fn has_removes(&self, count: usize) -> &Self {
134		let actual = self.removes().len();
135		assert_eq!(actual, count, "Expected {} removes, found {}", count, actual);
136		self
137	}
138}
139
140/// Assertions for a single diff
141pub struct DiffAssertion<'a> {
142	diff: &'a FlowDiff,
143}
144
145impl<'a> DiffAssertion<'a> {
146	pub fn new(diff: &'a FlowDiff) -> Self {
147		Self {
148			diff,
149		}
150	}
151
152	/// Assert this is an insert diff
153	pub fn is_insert(&self) -> &Row {
154		match self.diff {
155			FlowDiff::Insert {
156				post,
157			} => post,
158			_ => panic!("Expected insert diff, found {:?}", self.diff),
159		}
160	}
161
162	/// Assert this is an update diff
163	pub fn is_update(&self) -> (&Row, &Row) {
164		match self.diff {
165			FlowDiff::Update {
166				pre,
167				post,
168			} => (pre, post),
169			_ => panic!("Expected update diff, found {:?}", self.diff),
170		}
171	}
172
173	/// Assert this is a remove diff
174	pub fn is_remove(&self) -> &Row {
175		match self.diff {
176			FlowDiff::Remove {
177				pre,
178			} => pre,
179			_ => panic!("Expected remove diff, found {:?}", self.diff),
180		}
181	}
182}
183
184/// Assertions for Row values
185pub struct RowAssertion<'a> {
186	row: &'a Row,
187}
188
189impl<'a> RowAssertion<'a> {
190	/// Create a new row assertion
191	pub fn new(row: &'a Row) -> Self {
192		Self {
193			row,
194		}
195	}
196
197	/// Assert the row number
198	pub fn has_number(&self, number: impl Into<RowNumber>) -> &Self {
199		let expected = number.into();
200		assert_eq!(
201			self.row.number, expected,
202			"Expected row number {:?}, found {:?}",
203			expected, self.row.number
204		);
205		self
206	}
207
208	/// Assert the row values match (using the row's layout)
209	pub fn has_values(&self, expected: &[Value]) -> &Self {
210		let actual = super::helpers::get_values_named(&self.row.layout, &self.row.encoded);
211		assert_eq!(actual, expected, "Row values mismatch. Expected: {:?}, Actual: {:?}", expected, actual);
212		self
213	}
214
215	/// Assert a specific field value (for named layouts)
216	pub fn has_field(&self, field_name: &str, expected: Value) -> &Self {
217		let values = super::helpers::get_values_named(&self.row.layout, &self.row.encoded);
218		let field_index =
219			self.row.layout
220				.names()
221				.iter()
222				.position(|n| n.as_str() == field_name)
223				.expect(&format!("Field '{}' not found in layout", field_name));
224
225		assert_eq!(
226			values[field_index], expected,
227			"Field '{}' mismatch. Expected: {:?}, Actual: {:?}",
228			field_name, expected, values[field_index]
229		);
230		self
231	}
232
233	/// Get the values from the row
234	pub fn values(&self) -> Vec<Value> {
235		super::helpers::get_values_named(&self.row.layout, &self.row.encoded)
236	}
237}
238
239/// Assertions for state store
240pub struct StateAssertion<'a> {
241	store: &'a TestStateStore,
242}
243
244impl<'a> StateAssertion<'a> {
245	/// Create a new state assertion
246	pub fn new(store: &'a TestStateStore) -> Self {
247		Self {
248			store,
249		}
250	}
251
252	/// Assert the state is empty
253	pub fn is_empty(&self) -> &Self {
254		assert!(self.store.is_empty(), "Expected empty state, found {} entries", self.store.len());
255		self
256	}
257
258	/// Assert the state has a specific number of entries
259	pub fn has_entries(&self, count: usize) -> &Self {
260		self.store.assert_count(count);
261		self
262	}
263
264	/// Assert a key exists
265	pub fn has_key(&self, key: &EncodedKey) -> &Self {
266		self.store.assert_exists(key);
267		self
268	}
269
270	/// Assert a key does not exist
271	pub fn not_has_key(&self, key: &EncodedKey) -> &Self {
272		self.store.assert_not_exists(key);
273		self
274	}
275
276	/// Assert a key has specific values
277	pub fn key_has_values(&self, key: &EncodedKey, expected: &[Value], layout: &EncodedValuesLayout) -> &Self {
278		self.store.assert_value(key, expected, layout);
279		self
280	}
281
282	/// Assert all keys match a predicate
283	pub fn all_keys<F>(&self, predicate: F) -> &Self
284	where
285		F: Fn(&EncodedKey) -> bool,
286	{
287		for key in self.store.keys() {
288			assert!(predicate(key), "Key {:?} did not match predicate", key);
289		}
290		self
291	}
292}
293
294/// Helper to create assertions
295pub trait Assertable {
296	type Assertion<'a>
297	where
298		Self: 'a;
299
300	fn assert(&self) -> Self::Assertion<'_>;
301}
302
303impl Assertable for FlowChange {
304	type Assertion<'a>
305		= FlowChangeAssertion<'a>
306	where
307		Self: 'a;
308
309	fn assert(&self) -> FlowChangeAssertion<'_> {
310		FlowChangeAssertion::new(self)
311	}
312}
313
314impl Assertable for Row {
315	type Assertion<'a>
316		= RowAssertion<'a>
317	where
318		Self: 'a;
319
320	fn assert(&self) -> RowAssertion<'_> {
321		RowAssertion::new(self)
322	}
323}
324
325impl Assertable for TestStateStore {
326	type Assertion<'a>
327		= StateAssertion<'a>
328	where
329		Self: 'a;
330
331	fn assert(&self) -> StateAssertion<'_> {
332		StateAssertion::new(self)
333	}
334}
335
336#[cfg(test)]
337mod tests {
338	use reifydb_type::Type;
339
340	use super::*;
341	use crate::testing::{
342		builders::{TestFlowChangeBuilder, TestRowBuilder},
343		helpers::encode_key,
344	};
345
346	#[test]
347	fn test_flow_change_assertions() {
348		let change = TestFlowChangeBuilder::new()
349			.insert_row(1, vec![Value::Int8(10i64)])
350			.update_row(2, vec![Value::Int8(20i64)], vec![Value::Int8(30i64)])
351			.remove_row(3, vec![Value::Int8(40i64)])
352			.build();
353
354		change.assert()
355			.has_diffs(3)
356			.has_insert()
357			.has_update()
358			.has_remove()
359			.has_inserts(1)
360			.has_updates(1)
361			.has_removes(1);
362
363		// Need to keep assertion alive for lifetime
364		let change_assert = change.assert();
365		let diff_assert = change_assert.diff_at(0);
366		let insert_row = diff_assert.is_insert();
367		insert_row.assert().has_number(1).has_values(&[Value::Int8(10i64)]);
368	}
369
370	#[test]
371	fn test_row_assertions() {
372		let row = TestRowBuilder::new(42)
373			.with_values(vec![Value::Int8(100i64), Value::Utf8("test".into())])
374			.build();
375
376		row.assert().has_number(42).has_values(&[Value::Int8(100i64), Value::Utf8("test".into())]);
377
378		assert_eq!(row.assert().values().len(), 2);
379	}
380
381	#[test]
382	fn test_state_assertions() {
383		let mut store = TestStateStore::new();
384		let layout = EncodedValuesLayout::new(&[Type::Int8]);
385		let key1 = encode_key("key1");
386		let key2 = encode_key("key2");
387
388		store.set_value(key1.clone(), &[Value::Int8(10i64)], &layout);
389		store.set_value(key2.clone(), &[Value::Int8(20i64)], &layout);
390
391		store.assert()
392			.has_entries(2)
393			.has_key(&key1)
394			.has_key(&key2)
395			.key_has_values(&key1, &[Value::Int8(10i64)], &layout)
396			.all_keys(|k| k.0.len() == 6); // "key1" and "key2" are 6 bytes (4 chars + 2-byte terminator 0xffff)
397	}
398
399	#[test]
400	#[should_panic(expected = "Expected 5 diffs, found 1")]
401	fn test_assertion_failure() {
402		let change = TestFlowChangeBuilder::new().insert_row(1, vec![Value::Int8(10i64)]).build();
403
404		change.assert().has_diffs(5);
405	}
406}