Skip to main content

reifydb_sdk/testing/
assertions.rs

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