use crate::compiler::compile_after::{compile_after, CompileAfterError, CompiledFile};
use crate::compiler::env_interpolate::{interpolate, InterpolateError};
use crate::compiler::expand::{expand, ExpandError, PackResolver};
use crate::compiler::normalize::{normalize, NormalizeError};
use crate::compiler::parse::{parse, ParseError};
use crate::compiler::prepare::{prepare, PrepareError};
use crate::config::ScenarioEntry;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CompileError {
#[error("env interpolation error")]
EnvInterpolate(#[from] InterpolateError),
#[error("parse error")]
Parse(#[from] ParseError),
#[error("normalize error")]
Normalize(#[from] NormalizeError),
#[error("expand error")]
Expand(#[from] ExpandError),
#[error("compile_after error")]
CompileAfter(#[from] CompileAfterError),
#[error("prepare error")]
Prepare(#[from] PrepareError),
#[error(
"scenario `{id}` uses {clause} (continuous coupling); call \
`compile_scenario_file_compiled` and feed the result to \
`run_multi_compiled` to preserve gate semantics"
)]
GatedClauseRequiresCompiledPath {
id: String,
clause: &'static str,
},
}
pub fn compile_scenario_file(
yaml: &str,
resolver: &dyn PackResolver,
) -> Result<Vec<ScenarioEntry>, CompileError> {
let compiled = compile_scenario_file_compiled(yaml, resolver)?;
for (idx, entry) in compiled.entries.iter().enumerate() {
let entry_label = || entry.id.clone().unwrap_or_else(|| format!("entry[{idx}]"));
if entry.while_clause.is_some() {
return Err(CompileError::GatedClauseRequiresCompiledPath {
id: entry_label(),
clause: "while:",
});
}
if entry.delay_clause.is_some() {
return Err(CompileError::GatedClauseRequiresCompiledPath {
id: entry_label(),
clause: "delay:",
});
}
}
Ok(prepare(compiled)?)
}
pub fn compile_scenario_file_compiled(
yaml: &str,
resolver: &dyn PackResolver,
) -> Result<CompiledFile, CompileError> {
let wrapped = DynPackResolver(resolver);
let interpolated = interpolate(yaml)?;
let parsed = parse(&interpolated)?;
let normalized = normalize(parsed)?;
let expanded = expand(normalized, &wrapped)?;
Ok(compile_after(expanded)?)
}
struct DynPackResolver<'a>(&'a dyn PackResolver);
impl<'a> PackResolver for DynPackResolver<'a> {
fn resolve(
&self,
reference: &str,
) -> Result<crate::packs::MetricPackDef, crate::compiler::expand::PackResolveError> {
self.0.resolve(reference)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiler::expand::InMemoryPackResolver;
fn empty_resolver() -> InMemoryPackResolver {
InMemoryPackResolver::new()
}
#[test]
fn one_shot_compiles_minimal_inline_scenario() {
let yaml = r#"
version: 2
defaults:
rate: 10
duration: 500ms
scenarios:
- id: cpu
signal_type: metrics
name: cpu_usage
generator:
type: constant
value: 1.0
"#;
let resolver = empty_resolver();
let entries = compile_scenario_file(yaml, &resolver).expect("one-shot must succeed");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].base().name, "cpu_usage");
assert_eq!(entries[0].base().rate, 10.0);
}
#[test]
fn parse_failure_surfaces_as_parse_variant() {
let yaml = "version: 1\nscenarios: []\n";
let resolver = empty_resolver();
let err = compile_scenario_file(yaml, &resolver).expect_err("v1 yaml must fail");
assert!(
matches!(err, CompileError::Parse(_)),
"v1 version must surface as Parse, got {err:?}"
);
}
#[test]
fn yaml_with_while_clause_rejected_with_compiled_path_hint() {
let yaml = r#"
version: 2
defaults:
rate: 1
duration: 30s
scenarios:
- id: upstream
signal_type: metrics
name: upstream
generator:
type: flap
up_duration: 5s
down_duration: 5s
- id: downstream
signal_type: metrics
name: downstream
generator:
type: constant
value: 1.0
while:
ref: upstream
op: "<"
value: 1
"#;
let resolver = empty_resolver();
let err = compile_scenario_file(yaml, &resolver)
.expect_err("while: must reject through the lossy entry point");
match err {
CompileError::GatedClauseRequiresCompiledPath { id, clause } => {
assert_eq!(id, "downstream");
assert_eq!(clause, "while:");
}
other => panic!("expected GatedClauseRequiresCompiledPath, got {other:?}"),
}
}
#[test]
fn yaml_with_delay_clause_rejected_with_compiled_path_hint() {
let yaml = r#"
version: 2
defaults:
rate: 1
duration: 30s
scenarios:
- id: upstream
signal_type: metrics
name: upstream
generator:
type: flap
up_duration: 5s
down_duration: 5s
- id: downstream
signal_type: metrics
name: downstream
generator:
type: constant
value: 1.0
while:
ref: upstream
op: "<"
value: 1
delay:
open: 2s
close: 0s
"#;
let resolver = empty_resolver();
let err = compile_scenario_file(yaml, &resolver)
.expect_err("delay: must reject through the lossy entry point");
match err {
CompileError::GatedClauseRequiresCompiledPath { id, clause } => {
assert_eq!(id, "downstream");
assert!(
clause == "while:" || clause == "delay:",
"expected while: or delay:, got {clause}"
);
}
other => panic!("expected GatedClauseRequiresCompiledPath, got {other:?}"),
}
}
#[test]
fn normalize_failure_surfaces_as_normalize_variant() {
let yaml = r#"
version: 2
scenarios:
- id: no_rate
signal_type: metrics
name: no_rate
generator:
type: constant
value: 1.0
"#;
let resolver = empty_resolver();
let err = compile_scenario_file(yaml, &resolver).expect_err("missing rate must fail");
assert!(
matches!(err, CompileError::Normalize(_)),
"missing rate must surface as Normalize, got {err:?}"
);
}
#[test]
fn expand_failure_surfaces_as_expand_variant() {
let yaml = r#"
version: 2
defaults:
rate: 1
scenarios:
- signal_type: metrics
pack: unknown_pack_xyz
"#;
let resolver = empty_resolver();
let err = compile_scenario_file(yaml, &resolver).expect_err("unknown pack must fail");
assert!(
matches!(err, CompileError::Expand(_)),
"unresolvable pack must surface as Expand, got {err:?}"
);
}
#[test]
fn compile_after_failure_surfaces_as_compile_after_variant() {
let yaml = r#"
version: 2
defaults:
rate: 1
scenarios:
- id: loopy
signal_type: metrics
name: loopy
generator:
type: flap
up_duration: 60s
down_duration: 30s
after:
ref: loopy
op: "<"
value: 1
"#;
let resolver = empty_resolver();
let err = compile_scenario_file(yaml, &resolver).expect_err("self-ref must fail");
assert!(
matches!(err, CompileError::CompileAfter(_)),
"self-reference must surface as CompileAfter, got {err:?}"
);
}
#[test]
fn compile_error_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<CompileError>();
}
}