1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3pub mod doubles;
51
52use std::collections::HashMap;
53
54use faucet_core::{Sink, Source, Value};
55use futures::StreamExt;
56
57pub trait HasConfigSchema {
63 fn conformance_schema(&self) -> Value;
65 fn conformance_label(&self) -> String;
67}
68
69impl<T: Source + ?Sized> HasConfigSchema for T {
70 fn conformance_schema(&self) -> Value {
71 self.config_schema()
72 }
73 fn conformance_label(&self) -> String {
74 self.connector_name().to_string()
75 }
76}
77
78pub fn assert_config_schema_valid<C: HasConfigSchema + ?Sized>(connector: &C) {
85 assert_config_schema_valid_value(
86 &connector.conformance_schema(),
87 &connector.conformance_label(),
88 );
89}
90
91pub fn assert_config_schema_valid_value(schema: &Value, label: &str) {
94 let obj = schema.as_object().unwrap_or_else(|| {
95 panic!("[{label}] config_schema() must be a JSON object, got: {schema}")
96 });
97
98 let recognized = [
100 "type",
101 "properties",
102 "$ref",
103 "oneOf",
104 "allOf",
105 "anyOf",
106 "$schema",
107 "enum",
108 ]
109 .iter()
110 .any(|k| obj.contains_key(*k));
111 assert!(
112 recognized,
113 "[{label}] config_schema() has no recognizable JSON Schema keyword: {schema}"
114 );
115
116 if let Some(props) = obj.get("properties") {
117 assert!(
118 props.is_object(),
119 "[{label}] config_schema().properties must be an object, got: {props}"
120 );
121 }
122 if let Some(ty) = obj.get("type") {
123 assert!(
124 ty.is_string() || ty.is_array(),
125 "[{label}] config_schema().type must be a string or array, got: {ty}"
126 );
127 }
128
129 let text = serde_json::to_string(schema).expect("schema serializes");
131 let reparsed: Value = serde_json::from_str(&text).expect("schema re-parses");
132 assert_eq!(
133 &reparsed, schema,
134 "[{label}] config_schema() does not round-trip through serde_json"
135 );
136}
137
138pub async fn assert_bounded_memory<S: Source + ?Sized>(
149 source: &S,
150 batch_size: usize,
151 total: usize,
152) {
153 assert!(
154 batch_size > 0,
155 "batch_size must be > 0 for a bounded-memory check"
156 );
157 assert!(
158 total > batch_size,
159 "total ({total}) must exceed batch_size ({batch_size}) for a meaningful check"
160 );
161 let label = source.connector_name();
162
163 let ctx: HashMap<String, Value> = HashMap::new();
164 let mut stream = source.stream_pages(&ctx, batch_size);
165 let mut seen = 0usize;
166 let mut peak = 0usize;
167 while let Some(page) = stream.next().await {
168 let page = page.unwrap_or_else(|e| panic!("[{label}] stream_pages errored: {e}"));
169 peak = peak.max(page.records.len());
170 seen += page.records.len();
171 }
173
174 assert_eq!(
175 seen, total,
176 "[{label}] streamed {seen} records, expected {total}"
177 );
178 assert!(
179 peak <= batch_size,
180 "[{label}] peak page {peak} exceeds batch_size {batch_size} (not bounded)"
181 );
182 assert!(
183 peak < total,
184 "[{label}] peak page {peak} == total: source buffered the whole set into one page"
185 );
186}
187
188pub async fn assert_bookmark_roundtrip<S: Source + ?Sized>(source: &S) {
200 let label = source.connector_name();
201 let ctx: HashMap<String, Value> = HashMap::new();
202
203 let (first_records, bookmark) = drain(source, &ctx, label).await;
206 assert!(
207 first_records > 0,
208 "[{label}] produced no records — cannot exercise bookmark round-trip"
209 );
210 let bookmark = bookmark.unwrap_or_else(|| {
211 panic!("[{label}] produced no bookmark to round-trip (stream_pages never set one)")
212 });
213
214 source
216 .apply_start_bookmark(bookmark.clone())
217 .await
218 .unwrap_or_else(|e| panic!("[{label}] apply_start_bookmark errored: {e}"));
219
220 let (second_records, _) = drain(source, &ctx, label).await;
221 assert!(
222 second_records < first_records,
223 "[{label}] resumed run replayed {second_records} records (first run: {first_records}); \
224 the bookmark {bookmark} was ignored — no incremental resume"
225 );
226}
227
228async fn drain<S: Source + ?Sized>(
230 source: &S,
231 ctx: &HashMap<String, Value>,
232 label: &str,
233) -> (usize, Option<Value>) {
234 let mut stream = source.stream_pages(ctx, 100);
235 let mut count = 0usize;
236 let mut last_bookmark = None;
237 while let Some(page) = stream.next().await {
238 let page = page.unwrap_or_else(|e| panic!("[{label}] stream_pages errored: {e}"));
239 count += page.records.len();
240 if page.bookmark.is_some() {
241 last_bookmark = page.bookmark;
242 }
243 }
244 (count, last_bookmark)
245}
246
247pub async fn assert_idempotent_replay<S, F, Fut>(sink: &S, distinct_count: F)
266where
267 S: Sink + ?Sized,
268 F: Fn() -> Fut,
269 Fut: std::future::Future<Output = usize>,
270{
271 let label = sink.connector_name();
272 if sink.supports_idempotent_writes() {
273 assert_watermark_idempotent(sink, &distinct_count, label).await;
274 } else if sink.dedups_by_key() {
275 assert_keyed_convergence(sink, &distinct_count, label).await;
276 } else {
277 panic!(
278 "[{label}] advertises no idempotency mechanism \
279 (supports_idempotent_writes=false, dedups_by_key=false) — nothing to verify"
280 );
281 }
282}
283
284fn rows(ids: &[i64]) -> Vec<Value> {
288 ids.iter()
289 .map(|i| serde_json::json!({ "id": i, "v": format!("v{i}") }))
290 .collect()
291}
292
293async fn assert_watermark_idempotent<S, F, Fut>(sink: &S, count: &F, label: &str)
294where
295 S: Sink + ?Sized,
296 F: Fn() -> Fut,
297 Fut: std::future::Future<Output = usize>,
298{
299 let scope = "conformance::idem";
300 let before = count().await;
301
302 let t1 = faucet_core::format_token(1);
304 let p1 = rows(&[1, 2, 3]);
305 sink.write_batch_idempotent(&p1, scope, &t1)
306 .await
307 .unwrap_or_else(|e| panic!("[{label}] write_batch_idempotent(page 1) errored: {e}"));
308 let after_first = count().await;
309 assert_eq!(
310 after_first - before,
311 3,
312 "[{label}] first idempotent write did not add all 3 rows"
313 );
314
315 let committed = sink
318 .last_committed_token(scope)
319 .await
320 .unwrap_or_else(|e| panic!("[{label}] last_committed_token errored: {e}"));
321 assert_eq!(
322 committed.as_deref(),
323 Some(t1.as_str()),
324 "[{label}] did not durably record its commit token — cannot skip a replay"
325 );
326
327 let committed_seq = faucet_core::parse_token(committed.as_deref().unwrap_or_default())
336 .unwrap_or_else(|| panic!("[{label}] committed token {committed:?} does not parse"));
337 assert!(
338 committed_seq >= faucet_core::parse_token(&t1).unwrap_or(0),
339 "[{label}] committed token did not reach the written page's token — \
340 run_stream could not skip the replay and would re-write the page"
341 );
342
343 let t2 = faucet_core::format_token(2);
345 let p2 = rows(&[4, 5]);
346 sink.write_batch_idempotent(&p2, scope, &t2)
347 .await
348 .unwrap_or_else(|e| panic!("[{label}] write_batch_idempotent(page 2) errored: {e}"));
349 let after_second = count().await;
350 assert_eq!(
351 after_second - after_first,
352 2,
353 "[{label}] forward progress after a new token did not add the new rows"
354 );
355}
356
357async fn assert_keyed_convergence<S, F, Fut>(sink: &S, count: &F, label: &str)
358where
359 S: Sink + ?Sized,
360 F: Fn() -> Fut,
361 Fut: std::future::Future<Output = usize>,
362{
363 let before = count().await;
364 sink.write_batch(&rows(&[1, 2, 3]))
365 .await
366 .unwrap_or_else(|e| panic!("[{label}] write_batch(page 1) errored: {e}"));
367 sink.write_batch(&rows(&[2, 3, 4]))
369 .await
370 .unwrap_or_else(|e| panic!("[{label}] write_batch(overlapping page) errored: {e}"));
371 let after = count().await;
372 assert_eq!(
373 after - before,
374 4,
375 "[{label}] overlapping keys did not converge: expected 4 distinct rows (ids 1-4), \
376 got {}",
377 after - before
378 );
379}
380
381pub async fn assert_capabilities_truthful<S, F, Fut>(sink: &S, distinct_count: F)
393where
394 S: Sink + ?Sized,
395 F: Fn() -> Fut,
396 Fut: std::future::Future<Output = usize>,
397{
398 let label = sink.connector_name();
399
400 assert!(
402 sink.supported_write_modes()
403 .contains(&faucet_core::write_mode::WriteMode::Append),
404 "[{label}] does not advertise Append — every sink must support append"
405 );
406
407 if sink.supports_idempotent_writes() || sink.dedups_by_key() {
408 assert_idempotent_replay(sink, &distinct_count).await;
410 } else {
411 let before = distinct_count().await;
413 sink.write_batch(&rows(&[100]))
414 .await
415 .unwrap_or_else(|e| panic!("[{label}] write_batch (append probe) errored: {e}"));
416 assert_eq!(
417 distinct_count().await - before,
418 1,
419 "[{label}] Append is advertised but write_batch did not add a row"
420 );
421 assert_eq!(
422 sink.last_committed_token("conformance::honest")
423 .await
424 .unwrap_or_else(|e| panic!("[{label}] last_committed_token errored: {e}")),
425 None,
426 "[{label}] is not idempotent yet reports a committed token"
427 );
428 }
429
430 if sink.supports_schema_evolution() {
431 let empty = faucet_core::drift::SchemaEvolution::default();
434 sink.evolve_schema(&empty).await.unwrap_or_else(|e| {
435 panic!("[{label}] advertises schema evolution but evolve_schema(no-op) errored: {e}")
436 });
437 }
438}
439
440pub async fn assert_errors_not_panics<S: Source + ?Sized>(source: &S) {
451 use futures::FutureExt;
452 let label = source.connector_name();
453
454 let outcome = std::panic::AssertUnwindSafe(source.fetch_all())
456 .catch_unwind()
457 .await;
458 match outcome {
459 Err(_) => panic!("[{label}] panicked instead of returning Err from fetch_all"),
460 Ok(Ok(_)) => panic!("[{label}] expected a failure but fetch_all succeeded"),
461 Ok(Err(_e)) => { }
462 }
463
464 let ctx: HashMap<String, Value> = HashMap::new();
466 let stream_outcome = std::panic::AssertUnwindSafe(async {
467 let mut s = source.stream_pages(&ctx, 100);
468 s.next().await
469 })
470 .catch_unwind()
471 .await;
472 match stream_outcome {
473 Err(_) => panic!("[{label}] panicked instead of returning Err from stream_pages"),
474 Ok(Some(Err(_e))) => { }
475 Ok(None) => panic!("[{label}] stream_pages yielded no pages (expected an error)"),
476 Ok(Some(Ok(_))) => {
477 panic!("[{label}] expected a failure but stream_pages produced a page")
478 }
479 }
480}
481
482pub async fn assert_write_modes_truthful<S, F, Fut>(sink: &S, distinct_count: F)
504where
505 S: Sink + ?Sized,
506 F: Fn() -> Fut,
507 Fut: std::future::Future<Output = usize>,
508{
509 use faucet_core::write_mode::{WriteMode, WriteSpec, plan_writes};
510
511 let label = sink.connector_name();
512 let modes = sink.supported_write_modes();
513 let has_upsert = modes.contains(&WriteMode::Upsert);
514 let has_delete = modes.contains(&WriteMode::Delete);
515
516 if !has_upsert && !has_delete {
517 return;
519 }
520
521 assert!(
524 sink.dedups_by_key(),
525 "[{label}] advertises {modes:?} but dedups_by_key()=false — pass a sink \
526 configured `write_mode: upsert` with `key: [\"id\"]` so the mode can be exercised"
527 );
528
529 if has_upsert {
531 let before = distinct_count().await;
532 sink.write_batch(&rows(&[1, 2]))
534 .await
535 .unwrap_or_else(|e| panic!("[{label}] write_batch(upsert seed) errored: {e}"));
536 sink.write_batch(&[serde_json::json!({ "id": 1, "v": "updated" })])
537 .await
538 .unwrap_or_else(|e| panic!("[{label}] write_batch(upsert overwrite) errored: {e}"));
539 let after = distinct_count().await;
540 assert_eq!(
541 after - before,
542 2,
543 "[{label}] upsert did not converge: re-writing key id=1 left {} distinct rows \
544 (expected 2: ids 1 and 2) — it appended a duplicate instead of updating",
545 after - before
546 );
547 }
548
549 if has_delete {
551 let before = distinct_count().await;
552 sink.write_batch(&[serde_json::json!({ "id": 777, "v": "doomed" })])
553 .await
554 .unwrap_or_else(|e| panic!("[{label}] write_batch(delete seed) errored: {e}"));
555 let seeded = distinct_count().await;
556 assert_eq!(
557 seeded - before,
558 1,
559 "[{label}] delete precondition failed: the row to delete was not written"
560 );
561 let mut del = serde_json::Map::new();
562 del.insert("id".to_string(), serde_json::json!(777));
563 del.insert(
564 doubles::DELETE_MARKER_FIELD.to_string(),
565 Value::String(doubles::DELETE_MARKER_VALUE.to_string()),
566 );
567 sink.write_batch(&[Value::Object(del)])
568 .await
569 .unwrap_or_else(|e| panic!("[{label}] write_batch(delete) errored: {e}"));
570 let after = distinct_count().await;
571 assert_eq!(
572 after, before,
573 "[{label}] a delete-marked record did not remove the row: {after} rows remain \
574 (expected {before}) — the delete was ignored"
575 );
576 }
577
578 let spec = WriteSpec {
580 write_mode: WriteMode::Upsert,
581 key: vec!["id".to_string()],
582 delete_marker: None,
583 };
584 let plan = plan_writes(
585 &[
586 serde_json::json!({ "id": 9, "v": "ok" }),
587 serde_json::json!({ "no_key": 1 }),
588 serde_json::json!({ "id": null }),
589 ],
590 &spec,
591 );
592 assert_eq!(
593 plan.upserts.len(),
594 1,
595 "[{label}] the one keyed row should be planned as an upsert"
596 );
597 assert_eq!(
598 plan.failed.len(),
599 2,
600 "[{label}] plan_writes did not report the missing-key and null-key rows as failed \
601 (they would be silently dropped or written): {:?}",
602 plan.failed
603 );
604}
605
606pub async fn assert_schema_evolution_effective<S: Sink + ?Sized>(sink: &S) {
621 use faucet_core::drift::{ColumnChange, SchemaEvolution};
622
623 let label = sink.connector_name();
624 assert!(
625 sink.supports_schema_evolution(),
626 "[{label}] does not advertise schema evolution — call this only on an evolvable sink \
627 (assert_capabilities_truthful covers the no-op case)"
628 );
629
630 let before = sink
631 .current_schema()
632 .await
633 .unwrap_or_else(|e| panic!("[{label}] current_schema() errored: {e}"))
634 .unwrap_or_else(|| {
635 panic!(
636 "[{label}] advertises schema evolution but current_schema() is None — \
637 cannot verify an added column appears"
638 )
639 });
640
641 let new_col = "faucet_conformance_evolved";
644 let already = before
645 .get("properties")
646 .and_then(|p| p.as_object())
647 .is_some_and(|p| p.contains_key(new_col));
648 assert!(
649 !already,
650 "[{label}] test column `{new_col}` already exists in current_schema() — \
651 cannot prove evolution added it"
652 );
653
654 let evolution = SchemaEvolution {
655 additions: vec![ColumnChange {
656 name: new_col.to_string(),
657 from: None,
658 to: serde_json::json!({ "type": "string" }),
659 }],
660 widenings: Vec::new(),
661 relax_nullability: Vec::new(),
662 };
663 sink.evolve_schema(&evolution)
664 .await
665 .unwrap_or_else(|e| panic!("[{label}] evolve_schema(add `{new_col}`) errored: {e}"));
666
667 let after = sink
668 .current_schema()
669 .await
670 .unwrap_or_else(|e| panic!("[{label}] current_schema() errored after evolve: {e}"))
671 .unwrap_or_else(|| panic!("[{label}] current_schema() became None after evolve_schema"));
672 let after_props = after
673 .get("properties")
674 .and_then(|p| p.as_object())
675 .unwrap_or_else(|| {
676 panic!("[{label}] current_schema() has no `properties` object after evolve: {after}")
677 });
678 assert!(
679 after_props.contains_key(new_col),
680 "[{label}] evolve_schema reported success but the added column `{new_col}` does not \
681 appear in a fresh current_schema() — the evolution was not effective: {after}"
682 );
683}
684
685pub async fn assert_batch_size_zero_single_page<S: Source + ?Sized>(source: &S) {
696 let label = source.connector_name();
697 let ctx: HashMap<String, Value> = HashMap::new();
698 let mut stream = source.stream_pages(&ctx, 0);
699 let mut pages = 0usize;
700 let mut non_empty = 0usize;
701 let mut records = 0usize;
702 while let Some(page) = stream.next().await {
703 let page = page.unwrap_or_else(|e| panic!("[{label}] stream_pages errored: {e}"));
704 pages += 1;
705 if !page.records.is_empty() {
706 non_empty += 1;
707 }
708 records += page.records.len();
709 }
710 assert!(
711 records > 0,
712 "[{label}] produced no records under batch_size=0 — cannot verify single-page batching"
713 );
714 assert_eq!(
715 non_empty, 1,
716 "[{label}] batch_size=0 must yield the entire result set as a single page, but \
717 {non_empty} non-empty pages were emitted ({pages} pages total, {records} records)"
718 );
719}
720
721pub fn assert_connector_name_nonempty<S: Source + ?Sized>(source: &S) {
732 assert_connector_name_nonempty_value(source.connector_name(), source.connector_name());
733}
734
735pub fn assert_connector_name_nonempty_value(name: &str, label: &str) {
737 assert!(
738 !name.is_empty(),
739 "[{label}] connector_name() returned an empty string — it would surface as the \
740 \"unknown\" metric label (a cardinality-rule violation)"
741 );
742 assert!(
743 !name.trim().is_empty(),
744 "[{label}] connector_name() is whitespace-only ({name:?}) — same effect as empty"
745 );
746}
747
748pub async fn assert_preflight_check_wellformed<S: Source + ?Sized>(
759 source: &S,
760 ctx: &faucet_core::check::CheckContext,
761) {
762 assert_report_wellformed(source.check(ctx).await, source.connector_name());
763}
764
765pub async fn assert_sink_preflight_check_wellformed<S: Sink + ?Sized>(
767 sink: &S,
768 ctx: &faucet_core::check::CheckContext,
769) {
770 assert_report_wellformed(sink.check(ctx).await, sink.connector_name());
771}
772
773fn assert_report_wellformed(
776 outcome: Result<faucet_core::check::CheckReport, faucet_core::FaucetError>,
777 label: &str,
778) {
779 use faucet_core::check::ProbeStatus;
780
781 let report = outcome.unwrap_or_else(|e| {
782 panic!(
783 "[{label}] check() returned Err({e}) — a probe failure must surface as a Fail \
784 probe inside Ok(report), not as an Err from check()"
785 )
786 });
787 assert!(
788 !report.probes.is_empty(),
789 "[{label}] check() returned an empty report — a well-formed report carries at least \
790 one probe"
791 );
792 for probe in &report.probes {
793 assert!(
794 !probe.name.is_empty(),
795 "[{label}] check() returned a probe with an empty name"
796 );
797 match &probe.status {
798 ProbeStatus::Pass => {}
799 ProbeStatus::Fail { reason } => assert!(
800 !reason.trim().is_empty(),
801 "[{label}] Fail probe `{}` has an empty reason",
802 probe.name
803 ),
804 ProbeStatus::Skip { reason } => assert!(
805 !reason.trim().is_empty(),
806 "[{label}] Skip probe `{}` has an empty reason",
807 probe.name
808 ),
809 }
810 }
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816 use doubles::{
817 CountingSource, EmptyNameSource, ErringCheckSink, ErringCheckSource, EvolvingSink,
818 FailingSource, LyingIdempotentSink, LyingKeyedSink, MultiPageZeroSource, NoOpEvolvingSink,
819 PanickingSource, TestSink,
820 };
821
822 #[test]
823 fn check1_accepts_a_valid_source_schema() {
824 let s = CountingSource::new(10, 2);
825 assert_config_schema_valid(&s);
826 }
827
828 #[test]
829 fn check1_value_form_works_for_a_sink() {
830 let sink = TestSink::new();
831 assert_config_schema_valid_value(&sink.config_schema(), sink.connector_name());
832 }
833
834 #[test]
835 #[should_panic(expected = "no recognizable JSON Schema keyword")]
836 fn check1_rejects_a_non_schema() {
837 assert_config_schema_valid_value(&serde_json::json!({"nope": 1}), "bogus");
838 }
839
840 #[tokio::test]
841 async fn check2_passes_for_a_paging_source() {
842 let s = CountingSource::new(1000, 100);
843 assert_bounded_memory(&s, 100, 1000).await;
844 }
845
846 #[tokio::test]
847 #[should_panic(expected = "not bounded")]
848 async fn check2_fails_when_source_emits_one_big_page() {
849 let s = CountingSource::new(500, 0);
851 assert_bounded_memory(&s, 100, 500).await;
852 }
853
854 #[tokio::test]
857 async fn check3_passes_for_a_resumable_source() {
858 let s = CountingSource::new(500, 100);
859 assert_bookmark_roundtrip(&s).await;
860 }
861
862 #[tokio::test]
863 #[should_panic(expected = "was ignored")]
864 async fn check3_fails_when_source_ignores_the_bookmark() {
865 let s = CountingSource::non_resumable(500, 100);
866 assert_bookmark_roundtrip(&s).await;
867 }
868
869 #[tokio::test]
872 async fn check4_passes_for_a_watermark_sink() {
873 let sink = TestSink::idempotent("id");
874 let s = sink.clone();
875 assert_idempotent_replay(&sink, || {
876 let s = s.clone();
877 async move { s.len() }
878 })
879 .await;
880 }
881
882 #[tokio::test]
883 async fn check4_passes_for_a_keyed_upsert_sink() {
884 let sink = TestSink::keyed("id");
885 let s = sink.clone();
886 assert_idempotent_replay(&sink, || {
887 let s = s.clone();
888 async move { s.len() }
889 })
890 .await;
891 }
892
893 #[tokio::test]
894 #[should_panic(expected = "did not durably record its commit token")]
895 async fn check4_fails_for_a_lying_idempotent_sink() {
896 let sink = LyingIdempotentSink::new();
897 let s = sink.clone();
898 assert_idempotent_replay(&sink, || {
899 let s = s.clone();
900 async move { s.len() }
901 })
902 .await;
903 }
904
905 #[tokio::test]
906 #[should_panic(expected = "did not converge")]
907 async fn check4_fails_for_a_lying_keyed_sink() {
908 let sink = LyingKeyedSink::new();
909 let s = sink.clone();
910 assert_idempotent_replay(&sink, || {
911 let s = s.clone();
912 async move { s.len() }
913 })
914 .await;
915 }
916
917 #[tokio::test]
918 #[should_panic(expected = "no idempotency mechanism")]
919 async fn check4_fails_for_an_append_only_sink() {
920 let sink = TestSink::new();
921 let s = sink.clone();
922 assert_idempotent_replay(&sink, || {
923 let s = s.clone();
924 async move { s.len() }
925 })
926 .await;
927 }
928
929 #[tokio::test]
932 async fn check5_passes_for_an_honest_append_sink() {
933 let sink = TestSink::new();
934 let s = sink.clone();
935 assert_capabilities_truthful(&sink, || {
936 let s = s.clone();
937 async move { s.len() }
938 })
939 .await;
940 }
941
942 #[tokio::test]
943 async fn check5_passes_for_an_honest_idempotent_sink() {
944 let sink = TestSink::idempotent("id");
945 let s = sink.clone();
946 assert_capabilities_truthful(&sink, || {
947 let s = s.clone();
948 async move { s.len() }
949 })
950 .await;
951 }
952
953 #[tokio::test]
954 #[should_panic(expected = "did not durably record its commit token")]
955 async fn check5_fails_for_a_lying_idempotent_sink() {
956 let sink = LyingIdempotentSink::new();
957 let s = sink.clone();
958 assert_capabilities_truthful(&sink, || {
959 let s = s.clone();
960 async move { s.len() }
961 })
962 .await;
963 }
964
965 #[tokio::test]
968 async fn check6_passes_for_a_source_that_returns_err() {
969 assert_errors_not_panics(&FailingSource).await;
970 }
971
972 #[tokio::test]
973 #[should_panic(expected = "panicked instead of returning Err")]
974 async fn check6_fails_for_a_source_that_panics() {
975 assert_errors_not_panics(&PanickingSource).await;
976 }
977
978 #[tokio::test]
979 #[should_panic(expected = "expected a failure but fetch_all succeeded")]
980 async fn check6_fails_for_a_source_that_succeeds() {
981 assert_errors_not_panics(&CountingSource::new(3, 1)).await;
984 }
985
986 #[tokio::test]
989 async fn check7_passes_for_an_upsert_delete_sink() {
990 let sink = TestSink::keyed_upsert("id");
991 let s = sink.clone();
992 assert_write_modes_truthful(&sink, || {
993 let s = s.clone();
994 async move { s.len() }
995 })
996 .await;
997 }
998
999 #[tokio::test]
1000 async fn check7_skips_an_append_only_sink() {
1001 let sink = TestSink::new();
1004 let s = sink.clone();
1005 assert_write_modes_truthful(&sink, || {
1006 let s = s.clone();
1007 async move { s.len() }
1008 })
1009 .await;
1010 assert!(sink.is_empty(), "append-only skip must not write anything");
1011 }
1012
1013 #[tokio::test]
1014 #[should_panic(expected = "did not converge")]
1015 async fn check7_fails_for_a_lying_keyed_sink() {
1016 let sink = LyingKeyedSink::new();
1017 let s = sink.clone();
1018 assert_write_modes_truthful(&sink, || {
1019 let s = s.clone();
1020 async move { s.len() }
1021 })
1022 .await;
1023 }
1024
1025 #[tokio::test]
1028 async fn check8_passes_for_an_evolving_sink() {
1029 let sink = EvolvingSink::new();
1030 assert_schema_evolution_effective(&sink).await;
1031 assert_eq!(sink.column_count(), 2, "evolve must have added a column");
1032 }
1033
1034 #[tokio::test]
1035 #[should_panic(expected = "was not effective")]
1036 async fn check8_fails_for_a_noop_evolving_sink() {
1037 assert_schema_evolution_effective(&NoOpEvolvingSink).await;
1038 }
1039
1040 #[tokio::test]
1043 async fn check9_passes_for_a_single_page_source() {
1044 let s = CountingSource::new(6, 0);
1045 assert_batch_size_zero_single_page(&s).await;
1046 }
1047
1048 #[tokio::test]
1049 #[should_panic(expected = "single page")]
1050 async fn check9_fails_for_a_multi_page_source() {
1051 let s = MultiPageZeroSource::new(6);
1052 assert_batch_size_zero_single_page(&s).await;
1053 }
1054
1055 #[test]
1058 fn check10_passes_for_a_named_source() {
1059 assert_connector_name_nonempty(&CountingSource::new(1, 1));
1060 }
1061
1062 #[test]
1063 fn check10_value_form_works_for_a_sink() {
1064 let sink = TestSink::new();
1065 assert_connector_name_nonempty_value(sink.connector_name(), sink.connector_name());
1066 }
1067
1068 #[test]
1069 #[should_panic(expected = "empty string")]
1070 fn check10_fails_for_an_empty_name_source() {
1071 assert_connector_name_nonempty(&EmptyNameSource);
1072 }
1073
1074 #[test]
1075 #[should_panic(expected = "empty string")]
1076 fn check10_value_form_rejects_empty() {
1077 assert_connector_name_nonempty_value("", "bogus");
1078 }
1079
1080 #[tokio::test]
1083 async fn check11_passes_for_a_source_with_a_fail_probe() {
1084 let ctx = faucet_core::check::CheckContext::default();
1087 assert_preflight_check_wellformed(&FailingSource, &ctx).await;
1088 }
1089
1090 #[tokio::test]
1091 async fn check11_passes_for_a_healthy_source() {
1092 let ctx = faucet_core::check::CheckContext::default();
1093 assert_preflight_check_wellformed(&CountingSource::new(3, 1), &ctx).await;
1094 }
1095
1096 #[tokio::test]
1097 async fn check11_passes_for_a_sink() {
1098 let ctx = faucet_core::check::CheckContext::default();
1099 assert_sink_preflight_check_wellformed(&TestSink::new(), &ctx).await;
1100 }
1101
1102 #[tokio::test]
1103 #[should_panic(expected = "returned Err")]
1104 async fn check11_fails_when_source_check_returns_err() {
1105 let ctx = faucet_core::check::CheckContext::default();
1106 assert_preflight_check_wellformed(&ErringCheckSource, &ctx).await;
1107 }
1108
1109 #[tokio::test]
1110 #[should_panic(expected = "returned Err")]
1111 async fn check11_fails_when_sink_check_returns_err() {
1112 let ctx = faucet_core::check::CheckContext::default();
1113 assert_sink_preflight_check_wellformed(&ErringCheckSink, &ctx).await;
1114 }
1115}