1use std::collections::{HashMap, HashSet, VecDeque};
9
10use crate::{
11 embedded::EmbeddedTask,
12 processor::{
13 erased::{OutputRecord, ProcessorError},
14 serde::{Consumed, Produced, Serde, SerdeAssociate},
15 },
16 topology::BuiltTopology,
17};
18
19type PendingRecord = (String, Option<Vec<u8>>, Vec<u8>, i64);
21
22pub struct TopologyTestDriver {
28 task: EmbeddedTask,
29 source_topics: HashSet<String>,
30 output: HashMap<String, VecDeque<OutputRecord>>,
31 mock_wall_ms: i64,
35 offsets: HashMap<String, i64>,
39 changelog_captured: Vec<(String, bytes::Bytes, Option<bytes::Bytes>, Option<i64>)>,
44}
45
46impl TopologyTestDriver {
47 pub fn new(built: &BuiltTopology) -> Result<Self, ProcessorError> {
55 let source_topics: HashSet<String> = built.list_source_topics().into_iter().collect();
56 let task = EmbeddedTask::instantiate(built, "app", HashSet::new(), None)?;
63 Ok(Self {
64 task,
65 source_topics,
66 output: HashMap::new(),
67 mock_wall_ms: 0,
68 offsets: HashMap::new(),
69 changelog_captured: Vec::new(),
70 })
71 }
72
73 pub fn pipe_input<KS, VS>(
79 &mut self,
80 topic: &str,
81 consumed: impl Into<Consumed<KS, VS>>,
82 key: Option<KS::Target>,
83 value: VS::Target,
84 timestamp: i64,
85 ) where
86 KS: SerdeAssociate + Serde<KS::Target>,
87 VS: SerdeAssociate + Serde<VS::Target>,
88 {
89 let consumed = consumed.into();
90 let kb = key.as_ref().map(|k| consumed.key_serde.serialize(topic, k));
91 let vb = consumed.value_serde.serialize(topic, &value);
92 drop(key);
93 drop(value);
94 self.pipe_bytes(topic, kb.as_deref(), &vb, timestamp);
95 }
96
97 fn next_offset(&mut self, topic: &str) -> i64 {
99 let offset = self.offsets.entry(topic.to_string()).or_insert(0);
100 let claimed = *offset;
101 *offset += 1;
102 claimed
103 }
104
105 fn pipe_bytes(&mut self, topic: &str, key: Option<&[u8]>, value: &[u8], timestamp: i64) {
106 let mut queue: VecDeque<PendingRecord> = VecDeque::from([(
107 topic.to_string(),
108 key.map(<[u8]>::to_vec),
109 value.to_vec(),
110 timestamp,
111 )]);
112 while let Some((t, k, v, ts)) = queue.pop_front() {
113 let offset = self.next_offset(&t);
115 let _ = self.task.pipe(&t, 0, offset, k.as_deref(), &v, ts);
116 self.route_outputs(&mut queue);
117 let _ = self.task.punctuate_stream_time(self.task.stream_time());
120 self.route_outputs(&mut queue);
121 }
122 }
123
124 pub fn advance_wall_clock_time(&mut self, by: std::time::Duration) {
129 self.mock_wall_ms += i64::try_from(by.as_millis()).unwrap_or(i64::MAX);
130 let mut queue: VecDeque<PendingRecord> = VecDeque::new();
131 let _ = self.task.punctuate_wall_clock(self.mock_wall_ms);
132 self.route_outputs(&mut queue);
133 while let Some((t, k, v, ts)) = queue.pop_front() {
135 let offset = self.next_offset(&t);
136 let _ = self.task.pipe(&t, 0, offset, k.as_deref(), &v, ts);
137 self.route_outputs(&mut queue);
138 let _ = self.task.punctuate_stream_time(self.task.stream_time());
139 self.route_outputs(&mut queue);
140 }
141 }
142
143 fn route_outputs(&mut self, queue: &mut VecDeque<PendingRecord>) {
153 for out in self.task.take_output() {
154 if self.source_topics.contains(&out.topic) {
155 let vv = out.value.clone().unwrap_or_default().to_vec();
157 queue.push_back((
158 out.topic.clone(),
159 out.key.as_ref().map(|b| b.to_vec()),
160 vv,
161 out.timestamp,
162 ));
163 } else {
164 self.output
165 .entry(out.topic.clone())
166 .or_default()
167 .push_back(out);
168 }
169 }
170 self.changelog_captured.extend(
175 self.task
176 .drain_changelogs()
177 .into_iter()
178 .map(|r| (r.topic, r.key, r.value, r.timestamp)),
179 );
180 }
181
182 pub fn get_key_value_store<K: Send + Sync + 'static, V: Send + 'static>(
187 &mut self,
188 name: &str,
189 ) -> Option<&mut dyn crate::store::api::KeyValueStore<K, V>> {
190 self.task.graph_mut().stores.get_kv::<K, V>(name)
191 }
192
193 pub fn store_get<K: Send + Sync + 'static, V: Send + 'static>(
199 &mut self,
200 store: &str,
201 key: &K,
202 ) -> Option<V> {
203 let s = self.task.graph_mut().stores.get_kv::<K, V>(store)?;
204 pollster::block_on(s.get(key))
205 }
206
207 pub fn store_get_versioned<K: Send + Sync + 'static, V: Send + 'static>(
209 &mut self,
210 store_name: &str,
211 key: &K,
212 ) -> Option<V> {
213 let store = self
214 .task
215 .graph_mut()
216 .stores
217 .get_versioned::<K, V>(store_name)?;
218 pollster::block_on(store.get(key)).map(|r| r.value)
219 }
220
221 pub fn drain_changelog(
229 &mut self,
230 ) -> Vec<(String, bytes::Bytes, Option<bytes::Bytes>, Option<i64>)> {
231 std::mem::take(&mut self.changelog_captured)
232 }
233
234 pub fn pipe_global<K, V>(&mut self, store_name: &str, key: K, value: V)
241 where
242 K: Send + Sync + 'static,
243 V: Send + 'static,
244 {
245 pollster::block_on(self.task.graph().globals.put(store_name, key, value));
246 }
247
248 pub async fn iq_kv_get<K: 'static, V: 'static>(
261 &self,
262 store: &str,
263 key: &K,
264 ks: &dyn Serde<K>,
265 vs: &dyn Serde<V>,
266 ) -> Option<V> {
267 let q = self.task.graph().stores.iq_get(store)?;
268 let kb = ks.serialize(store, key);
269 let vb = q.iq_kv_get(&kb).await?;
270 Some(vs.deserialize(store, &vb).expect("iq deserialize"))
271 }
272
273 pub async fn iq_kv_range<K: 'static, V: 'static>(
279 &self,
280 store: &str,
281 lo: &K,
282 hi: &K,
283 ks: &dyn Serde<K>,
284 vs: &dyn Serde<V>,
285 ) -> Vec<(K, V)> {
286 let Some(q) = self.task.graph().stores.iq_get(store) else {
287 return Vec::new();
288 };
289 let lob = ks.serialize(store, lo);
290 let hib = ks.serialize(store, hi);
291 q.iq_kv_range(&lob, &hib)
292 .await
293 .into_iter()
294 .map(|(k, v)| {
295 (
296 ks.deserialize(store, &k).expect("iq deserialize"),
297 vs.deserialize(store, &v).expect("iq deserialize"),
298 )
299 })
300 .collect()
301 }
302
303 pub async fn iq_kv_all<K: 'static, V: 'static>(
307 &self,
308 store: &str,
309 ks: &dyn Serde<K>,
310 vs: &dyn Serde<V>,
311 ) -> Vec<(K, V)> {
312 let Some(q) = self.task.graph().stores.iq_get(store) else {
313 return Vec::new();
314 };
315 q.iq_kv_all()
316 .await
317 .into_iter()
318 .map(|(k, v)| {
319 (
320 ks.deserialize(store, &k).expect("iq deserialize"),
321 vs.deserialize(store, &v).expect("iq deserialize"),
322 )
323 })
324 .collect()
325 }
326
327 pub async fn iq_kv_count(&self, store: &str) -> u64 {
330 match self.task.graph().stores.iq_get(store) {
331 Some(q) => q.iq_kv_approx_count().await,
332 None => 0,
333 }
334 }
335
336 pub async fn iq_window_fetch<K: 'static, V: 'static>(
343 &self,
344 store: &str,
345 key: &K,
346 from: i64,
347 to: i64,
348 ks: &dyn Serde<K>,
349 vs: &dyn Serde<V>,
350 ) -> Vec<(i64, V)> {
351 let Some(q) = self.task.graph().stores.iq_get(store) else {
352 return Vec::new();
353 };
354 let kb = ks.serialize(store, key);
355 q.iq_window_fetch(&kb, from, to)
356 .await
357 .into_iter()
358 .map(|(start, v)| (start, vs.deserialize(store, &v).expect("iq deserialize")))
359 .collect()
360 }
361
362 pub async fn iq_window_fetch_single<K: 'static, V: 'static>(
367 &self,
368 store: &str,
369 key: &K,
370 window_start: i64,
371 ks: &dyn Serde<K>,
372 vs: &dyn Serde<V>,
373 ) -> Option<V> {
374 let q = self.task.graph().stores.iq_get(store)?;
375 let kb = ks.serialize(store, key);
376 let vb = q.iq_window_fetch_single(&kb, window_start).await?;
377 Some(vs.deserialize(store, &vb).expect("iq deserialize"))
378 }
379
380 pub async fn query<Q: crate::runtime::iqv2::Query>(
385 &self,
386 req: crate::runtime::iqv2::StateQuery<Q>,
387 ) -> crate::runtime::iqv2::StateQueryResult<Q::Result> {
388 use std::collections::BTreeMap;
389
390 use crate::runtime::iqv2::{
391 request::Position,
392 result::{FailureReason, QueryResult, StateQueryResult},
393 };
394
395 let store_name = req.store.clone();
396 let kind = req.query.store_kind();
397 let Some(store) = self.task.graph().stores.iq_get(&store_name) else {
398 let mut m = BTreeMap::new();
403 m.insert(
404 0,
405 QueryResult::Failure {
406 reason: FailureReason::DoesNotExist,
407 message: format!("no state store named {store_name:?}"),
408 },
409 );
410 return StateQueryResult::new(m);
411 };
412 if store.kind() != kind {
413 let mut m = BTreeMap::new();
416 m.insert(
417 0,
418 QueryResult::Failure {
419 reason: FailureReason::NotPresent,
420 message: "wrong store kind".into(),
421 },
422 );
423 return StateQueryResult::new(m);
424 }
425 let lowered = req.query.lower();
426 let qr = match store.iq2_execute(&lowered).await {
427 Ok(boxed) => match boxed.downcast::<Q::Result>() {
428 Ok(r) => QueryResult::Success {
429 result: *r,
430 position: Position::default(),
431 },
432 Err(_) => QueryResult::Failure {
433 reason: FailureReason::StoreException,
434 message: "IQv2 result type mismatch".into(),
435 },
436 },
437 Err(crate::store::iq::Iq2Failure::UnknownQueryType) => QueryResult::Failure {
438 reason: FailureReason::UnknownQueryType,
439 message: "unknown query type".into(),
440 },
441 Err(crate::store::iq::Iq2Failure::KeyTypeMismatch) => QueryResult::Failure {
442 reason: FailureReason::StoreException,
443 message: "key type mismatch".into(),
444 },
445 };
446 let mut m = BTreeMap::new();
447 m.insert(0, qr);
448 StateQueryResult::new(m)
449 }
450
451 pub async fn iq_session_fetch<K: 'static, V: 'static>(
457 &self,
458 store: &str,
459 key: &K,
460 ks: &dyn Serde<K>,
461 vs: &dyn Serde<V>,
462 ) -> Vec<((i64, i64), V)> {
463 let Some(q) = self.task.graph().stores.iq_get(store) else {
464 return Vec::new();
465 };
466 let kb = ks.serialize(store, key);
467 q.iq_session_fetch_key(&kb)
468 .await
469 .into_iter()
470 .map(|(win, v)| (win, vs.deserialize(store, &v).expect("iq deserialize")))
471 .collect()
472 }
473
474 pub fn read_output<KS, VS>(
481 &mut self,
482 topic: &str,
483 produced: impl Into<Produced<KS, VS>>,
484 ) -> Option<(Option<KS::Target>, VS::Target)>
485 where
486 KS: SerdeAssociate + Serde<KS::Target>,
487 VS: SerdeAssociate + Serde<VS::Target>,
488 {
489 let produced = produced.into();
490 let out = self.output.get_mut(topic)?.pop_front()?;
491 let key = out.key.map(|b| {
492 produced
493 .key_serde
494 .deserialize(topic, &b)
495 .expect("test: deserialize output key")
496 });
497 let value = produced
498 .value_serde
499 .deserialize(topic, &out.value.unwrap_or_default())
500 .expect("test: deserialize output value");
501 Some((key, value))
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use assert2::check;
508 use async_trait::async_trait;
509 use krabka_units::prelude::*;
510
511 use super::*;
512 use crate::{
513 processor::{
514 api::{Processor, ProcessorContext},
515 record::Record,
516 serde::StringSerde,
517 },
518 topology::{NodeHandle, Topology},
519 };
520
521 struct Upper;
522 #[async_trait]
523 impl Processor<String, String, String, String> for Upper {
524 async fn process(
525 &mut self,
526 ctx: &mut ProcessorContext<'_, '_, String, String>,
527 r: Record<String, String>,
528 ) {
529 ctx.forward(Record::new(r.key, r.value.to_uppercase(), r.timestamp));
530 }
531 }
532 struct DropEmpty;
533 #[async_trait]
534 impl Processor<String, String, String, String> for DropEmpty {
535 async fn process(
536 &mut self,
537 ctx: &mut ProcessorContext<'_, '_, String, String>,
538 r: Record<String, String>,
539 ) {
540 if !r.value.is_empty() {
541 ctx.forward(r);
542 }
543 }
544 }
545 struct Identity;
546 #[async_trait]
547 impl Processor<String, String, String, String> for Identity {
548 async fn process(
549 &mut self,
550 ctx: &mut ProcessorContext<'_, '_, String, String>,
551 r: Record<String, String>,
552 ) {
553 ctx.forward(r);
554 }
555 }
556
557 fn map_filter() -> crate::topology::BuiltTopology {
558 let mut t = Topology::new();
559 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
560 let up = t.add_processor("up", || Upper, [&src]);
561 let flt = t.add_processor("flt", || DropEmpty, [&up]);
562 t.add_sink("out", "out", [&flt]);
563 t.build("app").unwrap()
564 }
565
566 #[test]
567 fn test_driver_disables_cache() {
568 let built = map_filter();
571 let d = TopologyTestDriver::new(&built).unwrap();
572 check!(d.task.graph().cache_max_bytes() == ByteSize::ZERO);
573 }
574
575 #[test]
576 fn map_filter_through() {
577 let built = map_filter();
578 let mut d = TopologyTestDriver::new(&built).unwrap();
579 d.pipe_input(
580 "in",
581 Consumed::with(StringSerde, StringSerde),
582 Some("k".to_string()),
583 "hello".to_string(),
584 0,
585 );
586 check!(
587 d.read_output("out", Produced::with(StringSerde, StringSerde))
588 == Some((Some("k".to_string()), "HELLO".to_string()))
589 );
590 d.pipe_input(
591 "in",
592 Consumed::with(StringSerde, StringSerde),
593 Some("k2".to_string()),
594 String::new(),
595 1,
596 );
597 check!(
598 d.read_output("out", Produced::with(StringSerde, StringSerde))
599 == None::<(Option<String>, String)>
600 );
601 }
602
603 #[test]
604 fn repartition_loops_through() {
605 let mut t = Topology::new();
607 t.add_repartition_topic("rp");
608 let s1: NodeHandle<String, String> = t.add_source("s1", ["in"]);
609 let id = t.add_processor("id", || Identity, [&s1]);
610 t.add_sink("to_rp", "rp", [&id]);
611 let s2: NodeHandle<String, String> = t.add_source("s2", ["rp"]);
612 let up = t.add_processor("up", || Upper, [&s2]);
613 t.add_sink("out", "out", [&up]);
614 let built = t.build("app").unwrap();
615
616 let mut d = TopologyTestDriver::new(&built).unwrap();
617 d.pipe_input(
618 "in",
619 Consumed::with(StringSerde, StringSerde),
620 None,
621 "hi".to_string(),
622 0,
623 );
624 check!(
625 d.read_output("out", Produced::with(StringSerde, StringSerde))
626 == Some((None, "HI".to_string()))
627 );
628 }
629
630 #[test]
631 fn branch_to_two_sinks() {
632 let mut t = Topology::new();
633 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
634 let up = t.add_processor("up", || Upper, [&src]);
635 t.add_sink("a", "out-a", [&up]);
636 t.add_sink("b", "out-b", [&up]);
637 let built = t.build("app").unwrap();
638 let mut d = TopologyTestDriver::new(&built).unwrap();
639 d.pipe_input(
640 "in",
641 Consumed::with(StringSerde, StringSerde),
642 None,
643 "x".to_string(),
644 0,
645 );
646 check!(
647 d.read_output("out-a", Produced::with(StringSerde, StringSerde))
648 == Some((None, "X".to_string()))
649 );
650 check!(
651 d.read_output("out-b", Produced::with(StringSerde, StringSerde))
652 == Some((None, "X".to_string()))
653 );
654 }
655
656 #[test]
657 fn stateful_count_and_store_inspection() {
658 use crate::processor::serde::I64Serde;
659 struct Counter;
660 #[async_trait]
661 impl Processor<String, String, String, i64> for Counter {
662 async fn process(
663 &mut self,
664 ctx: &mut ProcessorContext<'_, '_, String, i64>,
665 r: Record<String, String>,
666 ) {
667 let n = {
668 let s = ctx.get_state_store::<String, i64>("counts").unwrap();
669 let n = s.get(&r.value).await.unwrap_or(0) + 1;
670 s.put(r.value.clone(), n).await;
671 n
672 };
673 ctx.forward(Record::new(Some(r.value), n, r.timestamp));
674 }
675 }
676 let mut t = Topology::new();
677 let src: NodeHandle<String, String> = t.add_source("src", ["in"]);
678 let c = t.add_processor("c", || Counter, [&src]);
679 t.add_state_store("counts", StringSerde, I64Serde, [c.name()]);
680 t.add_sink("out", "out", [&c]);
681 let mut d = TopologyTestDriver::new(&t.build("app").unwrap()).unwrap();
682 d.pipe_input(
683 "in",
684 Consumed::with(StringSerde, StringSerde),
685 None,
686 "a".to_string(),
687 0,
688 );
689 d.pipe_input(
690 "in",
691 Consumed::with(StringSerde, StringSerde),
692 None,
693 "a".to_string(),
694 1,
695 );
696 check!(
697 d.read_output("out", Produced::with(StringSerde, I64Serde))
698 == Some((Some("a".to_string()), 1))
699 );
700 check!(
701 d.read_output("out", Produced::with(StringSerde, I64Serde))
702 == Some((Some("a".to_string()), 2))
703 );
704 check!(d.store_get::<String, i64>("counts", &"a".to_string()) == Some(2));
705 }
706
707 #[tokio::test]
708 async fn iqv2_query_kv_via_driver() {
709 use crate::{
710 processor::serde::{Consumed, StringSerde},
711 runtime::iqv2::{KeyQuery, StateQueryRequest},
712 };
713
714 let b = crate::StreamsBuilder::new();
715 b.stream::<String, String>(["in"])
716 .group_by_key()
717 .count("counts");
718 let built = b.build("app").unwrap();
719 let mut d = TopologyTestDriver::new(&built).unwrap();
720 for v in ["a", "a", "b"] {
721 d.pipe_input(
722 "in",
723 Consumed::with(StringSerde, StringSerde),
724 Some(v.to_string()),
725 v.to_string(),
726 0,
727 );
728 }
729
730 let res = d
731 .query(
732 StateQueryRequest::in_store("counts")
733 .with_query(KeyQuery::<String, i64>::with_key("a".into())),
734 )
735 .await;
736 let only = res.only_partition_result().unwrap();
737 assert_eq!(only.result(), Some(&Some(2)));
738 }
739}