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