use faucet_cli::config::PipelineConfig;
use faucet_cli::executor::{ExecuteOptions, run_expanded};
use faucet_cli::expand::expand;
fn opts(name: &str, max_concurrent: Option<usize>) -> ExecuteOptions {
ExecuteOptions {
pipeline_name: name.into(),
run_id: None,
execution: max_concurrent.map(|n| faucet_cli::config::ExecutionSpec {
max_concurrent: Some(n),
on_error: faucet_cli::config::OnError::Continue,
adaptive_batch_size: None,
}),
dry_run: false,
limit: None,
state_path_override: None,
shard: None,
auth: Default::default(),
clock: chrono::Utc::now().fixed_offset(),
cancel: None,
resilience: None,
sla: None,
reconcile: None,
#[cfg(feature = "lineage")]
lineage: None,
#[cfg(feature = "lineage")]
lineage_cfg: None,
#[cfg(feature = "notify")]
notifier: None,
#[cfg(feature = "catalog")]
catalog: None,
}
}
fn seed(dir: &std::path::Path, chunks: usize) {
for i in 0..chunks {
std::fs::write(
dir.join(format!("in-{i}.csv")),
format!("id,chunk\n{i}00,{i}\n{i}01,{i}\n"),
)
.unwrap();
}
}
fn config(dir: &std::path::Path, out: &std::path::Path, to: i64, chunk_size: u64) -> String {
format!(
r#"
version: 1
name: partitioned
pipeline:
source:
type: csv
config:
path: "{dir}/in-${{partition.start}}.csv"
sink:
type: jsonl
config:
path: "{out}"
append: true
partition:
kind: integer
from: 0
to: {to}
chunk_size: {chunk_size}
bounds: inclusive
"#,
dir = dir.display(),
out = out.display(),
)
}
async fn run(yaml: &str, dir: &std::path::Path, o: ExecuteOptions) -> usize {
let path = dir.join("p.yaml");
std::fs::write(&path, yaml).unwrap();
let cfg = PipelineConfig::from_text(yaml, &path).expect("config parses");
let nodes = expand(&cfg).expect("expand");
let n = nodes.len();
let summary = run_expanded(nodes, o).await.expect("run");
let errs: Vec<String> = summary
.invocations
.iter()
.filter_map(|i| i.error.clone())
.collect();
assert!(
!summary.had_failures(),
"every chunk should succeed: {errs:?}"
);
n
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn every_chunk_runs_and_contributes_its_own_rows() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.jsonl");
seed(dir.path(), 3);
let nodes = run(
&config(dir.path(), &out, 2, 1),
dir.path(),
opts("p", Some(4)),
)
.await;
assert_eq!(nodes, 3, "one node per chunk");
let body = std::fs::read_to_string(&out).unwrap();
assert_eq!(
body.lines().count(),
6,
"2 rows from each of 3 chunks — a chunk that silently did not run would show here"
);
for c in 0..3 {
assert!(
body.contains(&format!("\"chunk\":\"{c}\"")),
"chunk {c}'s rows are missing from the output"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn chunks_are_serialised_by_the_shared_concurrency_limit() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.jsonl");
seed(dir.path(), 4);
let nodes = run(
&config(dir.path(), &out, 3, 1),
dir.path(),
opts("p", Some(1)),
)
.await;
assert_eq!(nodes, 4);
assert_eq!(std::fs::read_to_string(&out).unwrap().lines().count(), 8);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn each_chunk_gets_a_distinct_valid_state_key() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.jsonl");
seed(dir.path(), 3);
let yaml = config(dir.path(), &out, 2, 1);
let cfg = PipelineConfig::from_text(&yaml, &dir.path().join("p.yaml")).unwrap();
let nodes = expand(&cfg).unwrap();
let mut keys = std::collections::BTreeSet::new();
for n in &nodes {
let key = format!("partitioned::{}", n.id);
faucet_core::state::validate_state_key(&key)
.unwrap_or_else(|e| panic!("state key {key} is invalid: {e}"));
assert!(keys.insert(key.clone()), "duplicate state key {key}");
}
assert_eq!(keys.len(), 3);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_failing_chunk_does_not_silently_pass() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.jsonl");
seed(dir.path(), 2);
let yaml = config(dir.path(), &out, 2, 1); let path = dir.path().join("p.yaml");
std::fs::write(&path, &yaml).unwrap();
let cfg = PipelineConfig::from_text(&yaml, &path).unwrap();
let nodes = expand(&cfg).unwrap();
let summary = run_expanded(nodes, opts("p", Some(4))).await.unwrap();
assert!(summary.had_failures(), "the missing chunk must be reported");
assert_eq!(summary.failure_count(), 1, "exactly the one bad chunk");
assert_eq!(std::fs::read_to_string(&out).unwrap().lines().count(), 4);
}