1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! Verify every shipped example YAML loads and expands cleanly.
//!
//! Guards against accidental schema-changes (named-templates feature, etc.)
//! breaking real-world configs. Any example that fails this test is a
//! backwards-compatibility regression.
//!
//! Examples that reference `${env:VAR}` or `${file:PATH}` placeholders
//! require credentials that are not available in test environments. Those
//! load errors are treated as "skipped" — the test only fails on structural
//! errors (bad YAML, unknown fields, expand failures) that indicate a
//! backwards-compatibility regression.
use faucet_cli::config::PipelineConfig;
use faucet_cli::error::CliError;
use faucet_cli::expand::expand;
use std::path::PathBuf;
fn examples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples")
}
/// Returns `true` when the error is a missing credential (env var / file) or a
/// secrets-manager directive that the synchronous loader cannot resolve — both
/// are expected in environments without those resources and should be skipped.
/// (Examples that reference `${vault:…}` etc. are still structurally validated
/// by the `shipped_example_yamls_pass_validate` test via `validate --no-secrets`.)
fn is_credential_error(e: &CliError) -> bool {
matches!(
e,
CliError::MissingEnvVar { .. }
| CliError::ReadInterpolatedFile { .. }
| CliError::SecretsRequireAsyncLoad
)
}
#[test]
fn every_example_loads_and_expands() {
let dir = examples_dir();
let mut count = 0;
let mut skipped = 0;
let mut failures: Vec<String> = Vec::new();
for entry in std::fs::read_dir(&dir).expect("examples dir exists") {
let path = entry.expect("readable dir entry").path();
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
continue;
};
if !matches!(ext, "yaml" | "yml" | "json") {
continue;
}
// serve_minimal.yaml is a `faucet serve --default-config` partial
// (workspace defaults only, no source/sink), so it does not expand on
// its own — it is merged under each submitted run at request time.
if path.file_name().and_then(|f| f.to_str()) == Some("serve_minimal.yaml") {
skipped += 1;
continue;
}
// Skip examples that rely on a feature not compiled into this test
// binary. In CI `--all-features` covers everything; local single-
// feature runs must not fail on example YAMLs that need an orthogonal
// feature (e.g. `schedule:` requires `--features schedule`).
#[cfg(not(feature = "schedule"))]
{
let yaml_text = std::fs::read_to_string(&path).unwrap_or_default();
if yaml_text.contains("\nschedule:") || yaml_text.starts_with("schedule:") {
skipped += 1;
continue;
}
}
// `masking:` is gated on the `masking` feature (deny_unknown_fields).
#[cfg(not(feature = "masking"))]
{
let yaml_text = std::fs::read_to_string(&path).unwrap_or_default();
if yaml_text.contains("\n masking:") || yaml_text.contains("\nmasking:") {
skipped += 1;
continue;
}
}
count += 1;
let cfg = match PipelineConfig::from_path(&path, None) {
Ok(c) => c,
Err(ref e) if is_credential_error(e) => {
// Credential not available in this environment — skip.
skipped += 1;
continue;
}
Err(e) => {
failures.push(format!("load {}: {e}", path.display()));
continue;
}
};
// Topology-mode examples (`pipeline.nodes`) are not matrix rows, so
// `expand` (matrix-only) does not apply. The parse above already
// validated their structure; graph validation is covered by
// `topology_end_to_end.rs`.
if !cfg.pipeline.nodes.is_empty() {
continue;
}
if let Err(e) = expand(&cfg) {
failures.push(format!("expand {}: {e}", path.display()));
}
}
assert!(count > 10, "expected to find >10 examples, found {count}");
eprintln!(
"examples: {count} total, {skipped} skipped (missing credentials), {} checked",
count - skipped
);
assert!(
failures.is_empty(),
"{} example(s) failed:\n{}",
failures.len(),
failures.join("\n")
);
}
/// The `serve --default-config` partial is excluded from the expand loop above
/// (no source/sink), so check it parses as a structurally valid `PipelineConfig`
/// here — this still catches bad YAML / unknown fields in the example.
#[test]
fn serve_minimal_default_config_parses() {
let path = examples_dir().join("serve_minimal.yaml");
PipelineConfig::from_path(&path, None)
.expect("serve_minimal.yaml must parse as a valid PipelineConfig");
}