use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use super::locate;
use super::{
ExpectItem, Macro, MacroBody, MacroStep, MacroStepKind, PackSet, PackSource, PayloadForm,
RawMacro, RawStep,
};
use crate::diag::Diag;
use crate::engine::{OptionRecogniser, RawOption, RawOptionValue, StepKindSpec};
use crate::lower::macro_has_ref;
use crate::matcher;
use crate::resolve::{self, Resolution, ResolveCtx, ResolveMode};
use crate::step::Retry;
use crate::world::World;
pub const MAX_USE_DEPTH: usize = 32;
#[allow(clippy::too_many_lines)]
pub(crate) fn normalize_macro(
name: &str,
raw: &RawMacro,
pack_name: &str,
source: &PackSource,
diags: &mut Vec<Diag>,
) -> Option<Macro> {
let span = locate::macro_span(&source.text, name);
let match_span = locate::match_span(&source.text, name);
let at = |diag: Diag| {
diag.with_source(source.name.clone(), Arc::clone(&source.text))
.maybe_span(span)
};
if let Some(pattern) = &raw.match_ {
for problem in matcher::pattern_problems(pattern, &raw.params) {
diags.push(at(Diag::error(
problem.code(),
format!("macro `{name}`: {problem}"),
)));
}
}
for default_key in raw.defaults.keys() {
if !raw.params.contains(default_key) {
let suggestion = matcher::closest(default_key, raw.params.iter().map(String::as_str))
.map(|p| format!(" — did you mean `{p}`?"))
.unwrap_or_default();
diags.push(at(Diag::error(
"proef::pack::default_not_param",
format!(
"macro `{name}`: default `{default_key}` is not a declared param{suggestion}"
),
)));
}
}
if !raw.bind.is_empty() && !raw.steps.iter().any(|step| step.ref_.is_some()) {
diags.push(at(Diag::error(
"proef::pack::bind_without_ref",
format!(
"macro `{name}`: `bind:` supplies a fragment's `{{{{…}}}}` variables, but no step here has a `ref:` — a `use:` target resolves its own bindings, so this table would go unread"
),
)));
}
let body = match (&raw.steps.is_empty(), &raw.expect) {
(false, Some(_)) => {
diags.push(at(Diag::error(
"proef::pack::steps_and_expect",
format!("macro `{name}` has both `steps:` and `expect:` — a macro is a request sequence or an assert-only macro, not both"),
)));
return None;
}
(true, None) => {
diags.push(at(Diag::error(
"proef::pack::empty_macro",
format!("macro `{name}` has neither `steps:` nor `expect:`"),
)));
return None;
}
(true, Some(items)) => {
let mut expect = Vec::new();
let hurl_spans = locate::expect_hurl_line_spans(&source.text, name);
let hurl_key_count = items.iter().filter(|item| item.hurl.is_some()).count();
let spans_reliable = hurl_spans.len() == hurl_key_count;
let mut hurl_ordinal = 0usize;
for (index, item) in items.iter().enumerate() {
let has_hurl_key = item.hurl.is_some();
let fragment_is_blank = item
.hurl
.as_deref()
.is_none_or(|fragment| fragment.trim().is_empty());
if item.status.is_none() && fragment_is_blank {
let fragment_span = (spans_reliable && has_hurl_key)
.then(|| hurl_spans.get(hurl_ordinal).copied())
.flatten();
diags.push(
at(Diag::error(
"proef::pack::empty_expect",
format!("macro `{name}` expect item {index} asserts nothing — give it `status:` and/or `hurl:` assert lines"),
))
.maybe_span(fragment_span)
.with_help("an `expect:` item must carry at least one assert line, from `status:` and/or non-blank `hurl:` content"),
);
if has_hurl_key {
hurl_ordinal += 1;
}
continue;
}
if has_hurl_key {
hurl_ordinal += 1;
}
expect.push(ExpectItem {
status: item.status.clone(),
fragment: item.hurl.clone(),
});
}
MacroBody::Expect(expect)
}
(false, None) => {
let mut steps = Vec::new();
for (index, step) in raw.steps.iter().enumerate() {
if let Some(step) = normalize_step(name, index, step, &at, diags) {
steps.push(step);
}
}
MacroBody::Steps(steps)
}
};
Some(Macro {
name: name.to_owned(),
pack: pack_name.to_owned(),
params: raw.params.clone(),
defaults: raw.defaults.clone(),
pattern: raw.match_.clone(),
description: raw.description.clone(),
tags: raw.tags.clone(),
body,
bind: raw.bind.clone(),
source: Arc::clone(&source.text),
span,
match_span,
})
}
#[allow(clippy::too_many_lines)]
fn normalize_step(
macro_name: &str,
index: usize,
raw: &RawStep,
at: &impl Fn(Diag) -> Diag,
diags: &mut Vec<Diag>,
) -> Option<MacroStep> {
let mut save_as = BTreeMap::new();
if let Some(targets) = &raw.save_as {
for (capture, target) in targets {
if target == "global" {
save_as.insert(capture.clone(), target.clone());
} else {
diags.push(at(Diag::error(
"proef::pack::bad_save_target",
format!("macro `{macro_name}` step {index}: `saveAs: {{ {capture}: {target} }}` — the only target is `global`"),
)));
}
}
}
let retry = match &raw.retry {
Some(r) if i64::from(r.count) > MAX_COUNT => {
diags.push(at(Diag::error(
"proef::pack::retry_not_finite",
format!(
"macro `{macro_name}` step {index}: `retry.count` {} is budget-hostile — the cap is {MAX_COUNT}",
r.count
),
)));
None
}
Some(r) if r.count == 0 => {
diags.push(at(Diag::error(
"proef::pack::retry_not_finite",
format!("macro `{macro_name}` step {index}: `retry.count` must be ≥ 1"),
)));
None
}
Some(r) => Some(Retry {
count: r.count,
interval_ms: r.interval_ms,
}),
None => None,
};
if !raw.bind.is_empty() && raw.ref_.is_none() {
diags.push(at(Diag::error(
"proef::pack::bind_without_ref",
format!(
"macro `{macro_name}` step {index}: `bind:` supplies a fragment's `{{{{…}}}}` variables, so it needs a `ref:` — an inline `hurl:` block takes `${{…}}` instead"
),
)));
}
let kind = if let Some(target) = &raw.ref_ {
if !raw.payload.is_empty() || raw.use_.is_some() {
let other = if raw.use_.is_some() {
"use:"
} else {
"a payload"
};
diags.push(at(Diag::error(
"proef::pack::body_form_conflict",
format!(
"macro `{macro_name}` step {index}: a step is either `ref:` or {other}, not both"
),
)));
return None;
}
if raw.with.is_some() {
diags.push(at(Diag::error(
"proef::pack::with_without_use",
format!("macro `{macro_name}` step {index}: `with:` only accompanies `use:`"),
)));
}
MacroStepKind::Ref {
target: target.clone(),
}
} else {
match (&raw.use_, raw.payload.len()) {
(Some(target), 0) => {
if raw.optional || raw.when.is_some() || retry.is_some() || !save_as.is_empty() {
diags.push(at(Diag::error(
"proef::pack::use_with_modifiers",
format!("macro `{macro_name}` step {index}: `use:` steps take only `with:` (and `name:`) — modifiers belong on the target macro's steps"),
)));
}
MacroStepKind::Use {
target: target.clone(),
with: raw.with.clone().unwrap_or_default(),
}
}
(Some(_), _) => {
diags.push(at(Diag::error(
"proef::pack::use_with_payload",
format!("macro `{macro_name}` step {index}: a step is either `use:` or a payload, not both"),
)));
return None;
}
(None, 0) => {
diags.push(at(Diag::error(
"proef::pack::empty_step",
format!(
"macro `{macro_name}` step {index} has no payload (`hurl: |…`), no `ref:`, and no `use:`"
),
)));
return None;
}
(None, 1) => {
if raw.with.is_some() {
diags.push(at(Diag::error(
"proef::pack::with_without_use",
format!(
"macro `{macro_name}` step {index}: `with:` only accompanies `use:`"
),
)));
}
let (kind_key, value) = raw
.payload
.iter()
.next()
.map(|(k, v)| (k.clone(), v.clone()))?;
let payload = match value {
serde_norway::Value::String(text) => PayloadForm::Raw(text),
other => PayloadForm::Structured(
serde_json::to_value(&other).unwrap_or(serde_json::Value::Null),
),
};
MacroStepKind::Payload {
kind: kind_key,
payload,
}
}
(None, _) => {
let keys: Vec<&str> = raw.payload.keys().map(String::as_str).collect();
diags.push(at(Diag::error(
"proef::pack::multiple_payloads",
format!(
"macro `{macro_name}` step {index} has {} payload keys ({}) — one per step",
keys.len(),
keys.join(", ")
),
)));
return None;
}
}
};
let delay_ms = match raw.delay {
Some(ms) if ms > MAX_DELAY_MS => {
diags.push(at(Diag::error(
"proef::pack::delay_unbounded",
format!(
"macro `{macro_name}` step {index}: `delay: {ms}` exceeds the {MAX_DELAY_MS} ms (1 hour) cap"
),
)));
None
}
other => other,
};
Some(MacroStep {
name: raw.name.clone(),
delay_ms,
kind,
optional: raw.optional,
when: raw.when.clone(),
retry,
save_as,
bind: raw.bind.clone(),
})
}
const MAX_COUNT: i64 = 10_000;
const MAX_DELAY_MS: u64 = 3_600_000;
fn raw_duration_ms(value: &str) -> Option<u64> {
let value = value.trim();
let (number, unit_ms) = if let Some(n) = value.strip_suffix("ms") {
(n, 1)
} else if let Some(n) = value.strip_suffix('s') {
(n, 1000)
} else if let Some(n) = value.strip_suffix('m') {
(n, 60_000)
} else {
(value, 1)
};
number.trim().parse::<u64>().ok()?.checked_mul(unit_ms)
}
pub(crate) fn run_cross_macro_passes(set: &PackSet, kinds: &[StepKindSpec], diags: &mut Vec<Diag>) {
for macro_ in set.macros.values() {
let at = |diag: Diag| {
diag.with_source(macro_.pack.clone(), Arc::clone(¯o_.source))
.maybe_span(macro_.span)
};
let MacroBody::Steps(steps) = ¯o_.body else {
continue;
};
if !macro_.bind.is_empty() && macro_has_ref(macro_) {
let (readable, complete) = scope_placeholders(set, macro_);
if complete {
unread_bind_pass(
"this macro refs",
&format!("macro `{}`", macro_.name),
macro_.bind.keys(),
&readable,
&at,
diags,
);
}
}
let mut payload_ordinals: BTreeMap<&str, usize> = BTreeMap::new();
for (index, step) in steps.iter().enumerate() {
match &step.kind {
MacroStepKind::Use { target, with } => {
use_target_passes(set, macro_, index, target, with, &at, diags);
}
MacroStepKind::Ref { .. } => {
ref_target_passes(set, kinds, macro_, index, step, &at, diags);
}
MacroStepKind::Payload { kind, payload } => {
let ordinal = *payload_ordinals
.entry(kind.as_str())
.and_modify(|n| *n += 1)
.or_insert(0);
payload_passes(
macro_, index, kind, step, payload, ordinal, kinds, &at, diags,
);
}
}
}
}
pack_scope_bind_pass(set, diags);
use_graph_passes(set, diags);
}
fn ref_target_passes(
set: &PackSet,
kinds: &[StepKindSpec],
macro_: &Macro,
index: usize,
step: &MacroStep,
at: &impl Fn(Diag) -> Diag,
diags: &mut Vec<Diag>,
) {
let MacroStepKind::Ref { target } = &step.kind else {
return;
};
let Some(fragment) = set.find_fragment(target) else {
let suggestion = matcher::closest(
target.rsplit('#').next().unwrap_or(target),
set.fragments.keys().map(String::as_str),
)
.map(|f| format!(" — did you mean `{f}`?"))
.unwrap_or_default();
diags.push(
at(Diag::error(
"proef::pack::unknown_ref",
format!(
"macro `{}` step {index}: `ref: {target}` names no loaded fragment{suggestion}",
macro_.name
),
))
.with_help(if set.fragments.is_empty() {
"no fragment files were loaded — set `[run] fragments` in proef.toml to the \
directory holding them"
} else {
"a fragment is one hurl entry marked `# @proef <name>` in a scanned file"
}),
);
return;
};
for violation in recogniser(kinds, &fragment.kind)
.map(|recognise| scan_option_values(&fragment.text, recognise))
.unwrap_or_default()
{
diags.push(
at(Diag::error(
violation.code,
format!(
"macro `{}` step {index}: in fragment `{}` (`{}` line {}), {}",
macro_.name,
fragment.name,
fragment.file,
fragment.line + violation.line - 1,
violation.detail
),
))
.with_help(
"the cap applies to the executed request, whichever file it was written in — \
edit the fragment, or point this step at one that stays within budget",
),
);
}
for family in step.declared_options() {
if fragment.declared_options.iter().any(|o| o == family) {
diags.push(at(Diag::error(
"proef::pack::option_declared_twice",
format!(
"macro `{}` step {index}: `{family}` is declared twice — in fragment `{}` (`{}` line {}) and as this step's own `{family}:`",
macro_.name, fragment.name, fragment.file, fragment.line
),
)).with_help(
"an entry carries one policy per option — delete whichever of the two is not authoritative",
));
}
}
unread_bind_pass(
"this step refs",
&format!("macro `{}` step {index}", macro_.name),
step.bind.keys(),
&fragment.placeholders.iter().map(String::as_str).collect(),
at,
diags,
);
for name in &fragment.supplied_variables {
let Some(scope) = binding_scope(set, macro_, step, name) else {
continue;
};
diags.push(
at(Diag::error(
"proef::pack::option_declared_twice",
format!(
"macro `{}` step {index}: `{name}` is supplied twice — by fragment `{}` (`{}` line {}) and by the {scope} `bind:`",
macro_.name, fragment.name, fragment.file, fragment.line
),
))
.with_help(format!(
"delete whichever is not authoritative — both reach the entry as \
`variable: {name}=`, where the fragment's own line lands last and the bound \
value would never reach the request",
)),
);
}
}
fn unread_bind_pass<'a>(
scope: &str,
where_: &str,
bind: impl Iterator<Item = &'a String>,
readable: &BTreeSet<&str>,
at: &impl Fn(Diag) -> Diag,
diags: &mut Vec<Diag>,
) {
for key in bind {
if readable.contains(key.as_str()) {
continue;
}
let suggestion = matcher::closest(key, readable.iter().copied())
.map(|near| format!(" — did you mean `{near}`?"))
.unwrap_or_default();
diags.push(
at(Diag::error(
"proef::pack::unread_bind_key",
format!("{where_}: `bind:` supplies `{key}`, which no fragment {scope} reads{suggestion}"),
))
.with_help(if readable.is_empty() {
"no fragment in scope reads any variable — delete the table".to_owned()
} else {
format!(
"the fragments in scope read: `{}`",
readable.iter().copied().collect::<Vec<_>>().join("`, `")
)
}),
);
}
}
fn binding_scope(
set: &PackSet,
macro_: &Macro,
step: &MacroStep,
name: &str,
) -> Option<&'static str> {
if step.bind.contains_key(name) {
Some("step's")
} else if macro_.bind.contains_key(name) {
Some("macro's")
} else if set
.bind
.get(¯o_.pack)
.is_some_and(|table| table.contains_key(name))
{
Some("pack's")
} else {
None
}
}
fn use_target_passes(
set: &PackSet,
macro_: &Macro,
index: usize,
target: &str,
with: &BTreeMap<String, String>,
at: &impl Fn(Diag) -> Diag,
diags: &mut Vec<Diag>,
) {
let Some(target_macro) = set.find_use_target(target) else {
let suggestion = matcher::closest(
target.rsplit('#').next().unwrap_or(target),
set.macros.keys().map(String::as_str),
)
.map(|m| format!(" — did you mean `{m}`?"))
.unwrap_or_default();
diags.push(at(Diag::error(
"proef::pack::unknown_use",
format!(
"macro `{}` step {index}: `use: {target}` names no loaded macro{suggestion}",
macro_.name
),
)));
return;
};
for key in with.keys() {
if !target_macro.params.contains(key) {
let suggestion = matcher::closest(key, target_macro.params.iter().map(String::as_str))
.map(|p| format!(" — did you mean `{p}`?"))
.unwrap_or_default();
diags.push(at(Diag::error(
"proef::pack::unknown_with_key",
format!(
"macro `{}` step {index}: `with:` key `{key}` is not a param of `{}`{suggestion}",
macro_.name, target_macro.name
),
)));
}
}
for param in &target_macro.params {
if !with.contains_key(param) && !target_macro.defaults.contains_key(param) {
diags.push(at(Diag::error(
"proef::pack::missing_use_param",
format!(
"macro `{}` step {index}: `use: {}` needs `with: {{ {param}: … }}` (no default exists)",
macro_.name, target_macro.name
),
)));
}
}
}
#[allow(clippy::too_many_arguments)]
fn payload_passes(
macro_: &Macro,
index: usize,
kind: &str,
step: &MacroStep,
payload: &PayloadForm,
ordinal: usize,
kinds: &[StepKindSpec],
at: &impl Fn(Diag) -> Diag,
diags: &mut Vec<Diag>,
) {
let Some(spec) = kinds.iter().find(|s| s.prefix == kind) else {
let suggestion = matcher::closest(kind, kinds.iter().map(|s| s.prefix))
.map(|p| format!(" — did you mean `{p}:`?"))
.unwrap_or_default();
diags.push(at(Diag::error(
"proef::pack::unknown_step_kind",
format!(
"macro `{}` step {index}: step kind `{kind}:` is not claimed by any registered engine{suggestion}",
macro_.name
),
)));
return;
};
let text = match payload {
PayloadForm::Raw(text) => text,
PayloadForm::Structured(value) => {
if let Some(validate) = spec.validate
&& let Ok(json) = serde_json::to_string(value)
&& let Err(err) = validate(&json)
{
diags.push(at(Diag::error(
"proef::pack::payload_invalid",
format!(
"macro `{}` step {index}: `{kind}:` payload is invalid — {}",
macro_.name, err.message
),
)));
}
return;
}
};
lint_raw_options(
macro_,
index,
kind,
step,
ordinal,
text,
spec.options,
at,
diags,
);
let Some(validate) = spec.validate else {
return;
};
match probe_lower(macro_, text) {
Err(err) => {
diags.push(at(Diag::error(
"proef::pack::bad_reference",
format!("macro `{}` step {index}: {err}", macro_.name),
)));
}
Ok(candidates) => {
let mut first_error = None;
let mut passed = false;
for candidate in &candidates {
match validate(candidate) {
Ok(()) => {
passed = true;
break;
}
Err(err) => first_error = first_error.or(Some(err)),
}
}
if !passed && let Some(err) = first_error {
diags.push(
at(Diag::error(
"proef::pack::invalid_hurl",
format!(
"macro `{}` step {index}: payload does not parse: {} (payload line {}, column {})",
macro_.name, err.message, err.line, err.column
),
))
.maybe_span(locate::payload_line_span(
¯o_.source,
¯o_.name,
kind,
ordinal,
err.line,
)),
);
}
}
}
}
fn recogniser(kinds: &[StepKindSpec], kind: &str) -> Option<OptionRecogniser> {
kinds
.iter()
.find(|spec| spec.prefix == kind)
.and_then(|spec| spec.options)
}
struct OptionViolation {
line: usize,
code: &'static str,
detail: String,
}
struct OptionLine<'a> {
line: usize,
key: &'a str,
value: &'a str,
option: RawOption,
in_options: bool,
}
fn option_lines(text: &str, recognise: OptionRecogniser) -> Vec<OptionLine<'_>> {
let mut out = Vec::new();
let mut in_fence = false;
let mut in_options = false;
for (line_no, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("```") {
in_fence = !in_fence;
continue;
}
if in_fence {
continue; }
if trimmed.starts_with('[') {
in_options = trimmed == "[Options]";
} else if crate::lower::is_method_line(trimmed) {
in_options = false;
}
let Some((key, value)) = trimmed.split_once(':') else {
continue;
};
let Some(option) = recognise(key.trim()) else {
continue;
};
out.push(OptionLine {
line: line_no + 1,
key: key.trim(),
value,
option,
in_options,
});
}
out
}
fn scan_option_values(text: &str, recognise: OptionRecogniser) -> Vec<OptionViolation> {
let mut found = Vec::new();
for OptionLine {
line,
key,
value,
option,
in_options,
} in option_lines(text, recognise)
{
if !in_options {
continue;
}
let mut push = |code: &'static str, detail: String| {
found.push(OptionViolation { line, code, detail });
};
match option.value {
Some(RawOptionValue::Count) => match value.trim().parse::<i64>() {
Ok(-1) => push(
"proef::pack::retry_not_finite",
format!("`{key}: -1` is infinite — budgets require a finite count (ADR-0007)"),
),
Ok(n) if n > MAX_COUNT => push(
"proef::pack::retry_not_finite",
format!("`{key}: {n}` is budget-hostile — the cap is {MAX_COUNT}"),
),
_ => {}
},
Some(RawOptionValue::Duration) => {
if let Some(ms) = raw_duration_ms(value)
&& ms > MAX_DELAY_MS
{
push(
"proef::pack::delay_unbounded",
format!(
"`{key}: {}` exceeds the {MAX_DELAY_MS} ms (1 hour) cap",
value.trim()
),
);
}
}
None => {}
}
}
found
}
#[allow(clippy::too_many_arguments)]
fn lint_raw_options(
macro_: &Macro,
index: usize,
kind: &str,
step: &MacroStep,
ordinal: usize,
text: &str,
recognise: Option<OptionRecogniser>,
at: &impl Fn(Diag) -> Diag,
diags: &mut Vec<Diag>,
) {
let Some(recognise) = recognise else {
return;
};
for violation in scan_option_values(text, recognise) {
diags.push(
at(Diag::error(
violation.code,
format!("macro `{}` step {index}: {}", macro_.name, violation.detail),
))
.maybe_span(locate::payload_line_span(
¯o_.source,
¯o_.name,
kind,
ordinal,
violation.line,
)),
);
}
let mut said: Vec<&'static str> = Vec::new();
for entry in option_lines(text, recognise) {
let Some(option) = entry.option.family.filter(|_| entry.in_options) else {
continue;
};
let line_no = entry.line - 1;
if step.declared_options().any(|f| f == option) && !said.contains(&option) {
said.push(option);
diags.push(
at(Diag::error(
"proef::pack::option_declared_twice",
format!(
"macro `{}` step {index}: `{option}` is declared twice — here in `[Options]`, and as the step's own `{option}:`",
macro_.name
),
))
.maybe_span(locate::payload_line_span(
¯o_.source,
¯o_.name,
kind,
ordinal,
line_no + 1,
))
.with_help(
"an entry carries one policy per option — delete whichever of the two is not authoritative",
),
);
}
}
}
fn probe_lower(macro_: &Macro, text: &str) -> Result<Vec<String>, resolve::ResolveError> {
let world = World::default();
let empty = BTreeMap::new();
let mut candidates = Vec::new();
for placeholder in ["{{probe}}", "1"] {
let args: BTreeMap<String, String> = macro_
.params
.iter()
.map(|p| (p.clone(), placeholder.to_owned()))
.collect();
let ctx = ResolveCtx {
args: &args,
defaults: ¯o_.defaults,
env: &empty,
config_vars: &empty,
run_id: "probe-run",
world: &world,
mode: ResolveMode::Probe,
};
let mut fakes = 0;
let Resolution { text, .. } = resolve::resolve(text, &ctx, &mut fakes)?;
candidates.push(text);
}
Ok(candidates)
}
fn scope_placeholders<'a>(set: &'a PackSet, macro_: &Macro) -> (BTreeSet<&'a str>, bool) {
let MacroBody::Steps(steps) = ¯o_.body else {
return (BTreeSet::new(), true);
};
let mut readable = BTreeSet::new();
let mut complete = true;
for step in steps {
let MacroStepKind::Ref { target } = &step.kind else {
continue;
};
match set.find_fragment(target) {
Some(fragment) => readable.extend(fragment.placeholders.iter().map(String::as_str)),
None => complete = false,
}
}
(readable, complete)
}
fn pack_scope_bind_pass(set: &PackSet, diags: &mut Vec<Diag>) {
for (pack, table) in &set.bind {
if table.is_empty() {
continue;
}
if diags.iter().any(|d| {
d.code == "proef::pack::body_form_conflict"
&& d.source_name.as_deref() == Some(pack.as_str())
}) {
continue;
}
let from_pack = || set.macros.values().filter(|m| &m.pack == pack);
if from_pack().any(macro_has_ref) {
let mut readable: BTreeSet<&str> = BTreeSet::new();
let mut complete = true;
for macro_ in from_pack() {
let (reads, whole) = scope_placeholders(set, macro_);
readable.extend(reads);
complete &= whole;
}
if let Some(anchor) = from_pack().next().filter(|_| complete) {
let at = |d: Diag| {
d.with_source(anchor.pack.clone(), Arc::clone(&anchor.source))
.maybe_span(anchor.span)
};
unread_bind_pass(
"in this pack",
&format!("pack `{pack}`"),
table.keys(),
&readable,
&at,
diags,
);
}
continue;
}
let Some(anchor) = from_pack().next() else {
continue;
};
diags.push(
Diag::error(
"proef::pack::bind_without_ref",
format!(
"pack `{pack}`: `bind:` supplies a fragment's `{{{{…}}}}` variables, but no macro in this pack has a `ref:` step — the table would go unread"
),
)
.with_source(anchor.pack.clone(), Arc::clone(&anchor.source))
.maybe_span(anchor.span)
.with_help(
"delete the table, or give the step that needs it a `ref: <fragment>` body",
),
);
}
}
fn use_graph_passes(set: &PackSet, diags: &mut Vec<Diag>) {
let mut colors: BTreeMap<&str, Color> = BTreeMap::new();
let mut chains: BTreeMap<&str, usize> = BTreeMap::new();
for macro_ in set.macros.values() {
visit_uses(set, macro_, &mut colors, &mut chains, diags);
}
for macro_ in set.macros.values() {
if chains.get(macro_.name.as_str()).copied().unwrap_or(1) <= MAX_USE_DEPTH {
continue;
}
let path = longest_use_path(set, macro_, &chains);
let Some(deep) = path.get(MAX_USE_DEPTH).copied() else {
continue; };
diags.push(
Diag::error(
"proef::pack::use_too_deep",
format!(
"`use:` nesting exceeds depth {MAX_USE_DEPTH} (via `{}`)",
path[..=MAX_USE_DEPTH]
.iter()
.map(|m| m.name.as_str())
.collect::<Vec<_>>()
.join("` → `")
),
)
.with_source(deep.pack.clone(), Arc::clone(&deep.source))
.maybe_span(deep.span),
);
}
}
#[derive(Clone, Copy)]
enum Color {
Gray,
Black,
}
enum Frame<'a> {
Enter(&'a Macro),
Exit(&'a Macro),
}
fn use_targets<'a>(set: &'a PackSet, macro_: &'a Macro) -> Vec<&'a Macro> {
let MacroBody::Steps(steps) = ¯o_.body else {
return Vec::new();
};
steps
.iter()
.filter_map(|step| {
let MacroStepKind::Use { target, .. } = &step.kind else {
return None;
};
set.find_use_target(target) })
.collect()
}
fn visit_uses<'a>(
set: &'a PackSet,
root: &'a Macro,
colors: &mut BTreeMap<&'a str, Color>,
chains: &mut BTreeMap<&'a str, usize>,
diags: &mut Vec<Diag>,
) {
if colors.contains_key(root.name.as_str()) {
return;
}
let mut work = vec![Frame::Enter(root)];
let mut path: Vec<&'a Macro> = Vec::new();
while let Some(frame) = work.pop() {
match frame {
Frame::Enter(m) => {
if colors.contains_key(m.name.as_str()) {
continue; }
colors.insert(m.name.as_str(), Color::Gray);
path.push(m);
work.push(Frame::Exit(m));
for next in use_targets(set, m).into_iter().rev() {
match colors.get(next.name.as_str()).copied() {
Some(Color::Gray) => report_use_cycle(&path, next, diags),
Some(Color::Black) => {} None => work.push(Frame::Enter(next)),
}
}
}
Frame::Exit(m) => {
path.pop();
let chain = 1 + use_targets(set, m)
.into_iter()
.filter_map(|next| chains.get(next.name.as_str()).copied())
.max()
.unwrap_or(0);
colors.insert(m.name.as_str(), Color::Black);
chains.insert(m.name.as_str(), chain);
}
}
}
}
fn report_use_cycle(path: &[&Macro], next: &Macro, diags: &mut Vec<Diag>) {
let pos = path.iter().position(|m| m.name == next.name).unwrap_or(0);
let ring = &path[pos..];
let min_ix = ring
.iter()
.enumerate()
.min_by_key(|(_, m)| m.name.as_str())
.map_or(0, |(i, _)| i);
let rotated: Vec<&Macro> = ring[min_ix..]
.iter()
.chain(&ring[..min_ix])
.copied()
.collect();
let Some(closer) = rotated.last().copied() else {
return;
};
let names: Vec<&str> = rotated.iter().map(|m| m.name.as_str()).collect();
diags.push(
Diag::error(
"proef::pack::use_cycle",
format!("`use:` cycle: `{}` → `{}`", names.join("` → `"), names[0]),
)
.with_source(closer.pack.clone(), Arc::clone(&closer.source))
.maybe_span(closer.span),
);
}
fn longest_use_path<'a>(
set: &'a PackSet,
from: &'a Macro,
chains: &BTreeMap<&'a str, usize>,
) -> Vec<&'a Macro> {
let mut path = vec![from];
while path.len() <= MAX_USE_DEPTH {
let cur = path[path.len() - 1];
let next = use_targets(set, cur)
.into_iter()
.filter(|cand| !path.iter().any(|m| m.name == cand.name))
.max_by_key(|cand| chains.get(cand.name.as_str()).copied().unwrap_or(1));
match next {
Some(next) => path.push(next),
None => break,
}
}
path
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use crate::diag::FrontError;
use crate::engine::{PayloadProbeError, StepKindSpec};
use crate::pack::{self, PackSource};
fn deny(_json: &str) -> Result<(), PayloadProbeError> {
Err(PayloadProbeError {
line: 1,
column: 1,
message: "unknown alt verb".into(),
})
}
const KINDS: &[StepKindSpec] = &[StepKindSpec {
prefix: "alt",
schema: "true",
validate: Some(deny),
fragments: None,
options: None,
}];
#[test]
fn whitespace_only_expect_fragment_is_rejected() {
let source = PackSource {
name: "expect.yaml".into(),
text: Arc::from(
"macros:\n empty:\n match: nothing binds this\n expect:\n - hurl: |\n\n",
),
};
let err = pack::load(&[source], &crate::pack::FragmentCorpus::empty(), KINDS).unwrap_err();
let FrontError::Diagnostics(diags) = err else {
panic!("diagnostics expected");
};
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::empty_expect")
.unwrap_or_else(|| panic!("expected proef::pack::empty_expect in {diags:?}"));
assert!(diag.help.is_some(), "a remediation hint is expected");
let text = diag.source_text.as_ref().unwrap();
let span = diag
.span
.unwrap_or_else(|| panic!("expected a span: {diag:?}"));
assert_eq!(
&text[span.start..span.end],
"hurl: |",
"span should land on the empty fragment's `hurl:` line, not the whole macro"
);
}
#[test]
fn flow_style_hurl_key_falls_back_to_the_macro_span() {
let text: Arc<str> = Arc::from(concat!(
"macros:\n",
" mixed:\n",
" match: nothing binds this\n",
" expect:\n",
" - {status: \"200\", hurl: 'jsonpath \"$.a\" exists'}\n",
" - hurl: |\n",
"\n",
" - hurl: |\n",
" jsonpath \"$.b\" exists\n",
));
let source = PackSource {
name: "mixed.yaml".into(),
text: Arc::clone(&text),
};
let err = pack::load(&[source], &crate::pack::FragmentCorpus::empty(), KINDS).unwrap_err();
let FrontError::Diagnostics(diags) = err else {
panic!("diagnostics expected");
};
let empty_expect: Vec<_> = diags
.iter()
.filter(|d| d.code == "proef::pack::empty_expect")
.collect();
assert_eq!(
empty_expect.len(),
1,
"only the blank second item should be flagged: {diags:?}"
);
let diag = empty_expect[0];
assert!(
diag.message.contains("expect item 1"),
"the blank item is index 1: {diag:?}"
);
let macro_span =
crate::pack::locate::macro_span(&text, "mixed").unwrap_or_else(|| panic!("macro span"));
assert_eq!(
diag.span,
Some(macro_span),
"an unreliable line-scan pairing must anchor on the macro, not a later item's line"
);
}
#[test]
fn structured_payloads_run_the_engine_validator() {
let source = PackSource {
name: "alt.yaml".into(),
text: Arc::from(
"macros:\n probe:\n match: the alternate step runs\n steps:\n - alt:\n bogus: 1\n",
),
};
let err = pack::load(&[source], &crate::pack::FragmentCorpus::empty(), KINDS).unwrap_err();
let FrontError::Diagnostics(diags) = err else {
panic!("diagnostics expected");
};
assert!(
diags
.iter()
.any(|d| d.code == "proef::pack::payload_invalid"
&& d.message.contains("unknown alt verb")),
"{diags:?}"
);
}
#[test]
fn use_graph_walk_is_linear_on_multi_edge_dags() {
const PLAIN: &[StepKindSpec] = &[StepKindSpec {
prefix: "alt",
schema: "true",
validate: None,
fragments: None,
options: None,
}];
use std::fmt::Write as _;
let mut yaml = String::from("macros:\n");
for i in 0..31 {
writeln!(yaml, " m{i:02}:").unwrap();
if i == 0 {
yaml.push_str(" match: the chain runs\n");
}
writeln!(
yaml,
" steps:\n - use: m{next:02}\n - use: m{next:02}",
next = i + 1
)
.unwrap();
}
yaml.push_str(" m31:\n steps:\n - alt:\n probe: 1\n");
let packs = pack::load(
&[PackSource {
name: "chain.yaml".into(),
text: Arc::from(yaml.as_str()),
}],
&crate::pack::FragmentCorpus::empty(),
PLAIN,
)
.unwrap();
assert_eq!(packs.macros.len(), 32);
}
const RAW: &[StepKindSpec] = &[StepKindSpec {
prefix: "alt",
schema: "true",
validate: None,
fragments: None,
options: Some(fake_recognise),
}];
#[test]
fn an_option_set_in_both_places_is_rejected() {
let source = PackSource {
name: "twice.yaml".into(),
text: Arc::from(concat!(
"macros:\n",
" twiceOver:\n",
" match: I set the retry in both places\n",
" steps:\n",
" - retry: { count: 3, interval_ms: 200 }\n",
" alt: |\n",
" GET http://x\n",
" [Options]\n",
" retry: 5\n",
" HTTP 200\n",
)),
};
let FrontError::Diagnostics(diags) =
pack::load(&[source], &crate::pack::FragmentCorpus::empty(), RAW).unwrap_err()
else {
panic!("diagnostics expected");
};
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::option_declared_twice")
.unwrap_or_else(|| panic!("expected the clash in {diags:?}"));
assert!(diag.help.is_some(), "a remediation hint is expected");
let text = diag.source_text.as_ref().unwrap();
let span = diag
.span
.unwrap_or_else(|| panic!("expected a span: {diag:?}"));
assert_eq!(
&text[span.start..span.end],
"retry: 5",
"span should land on the raw option line, not the whole macro"
);
assert_eq!(
diags
.iter()
.filter(|d| d.code == "proef::pack::option_declared_twice")
.count(),
1,
"one report per option family, however many entries repeat it"
);
}
#[test]
fn a_request_header_named_retry_is_not_a_clash() {
let source = PackSource {
name: "header.yaml".into(),
text: Arc::from(concat!(
"macros:\n",
" headerRetry:\n",
" match: the request header is named retry\n",
" steps:\n",
" - retry: { count: 3, interval_ms: 200 }\n",
" alt: |\n",
" GET http://x\n",
" retry: 5\n",
" HTTP 200\n",
)),
};
pack::load(&[source], &crate::pack::FragmentCorpus::empty(), RAW)
.unwrap_or_else(|err| panic!("a header named `retry` must not clash: {err:?}"));
}
fn fake_scan(
text: &str,
) -> Result<crate::engine::ScannedFile, crate::engine::FragmentScanError> {
let mut out: Vec<crate::engine::ScannedFragment> = Vec::new();
for (index, line) in text.lines().enumerate() {
let line = line.trim();
if line == "@!boom" {
return Err(crate::engine::FragmentScanError {
line: index + 1,
column: 1,
message: "unreadable entry".to_owned(),
});
}
if let Some(name) = line.strip_prefix('@') {
out.push(crate::engine::ScannedFragment {
name: name.to_owned(),
text: format!("GET http://x/{name}\n"),
line: index + 1,
placeholders: Vec::new(),
declared_options: Vec::new(),
supplied_variables: Vec::new(),
});
} else if let Some(last) = out.last_mut() {
if line == "retry" {
last.declared_options.push("retry".to_owned());
} else if let Some(read) = line.strip_prefix('?') {
last.placeholders.push(read.to_owned());
} else if let Some(supplied) = line.strip_prefix('=') {
use std::fmt::Write as _;
let _ = write!(last.text, "[Options]\nvariable: {supplied}=from-fragment\n");
last.supplied_variables.push(supplied.to_owned());
} else if let Some(raw) = line.strip_prefix('+') {
use std::fmt::Write as _;
let _ = write!(last.text, "[Options]\n{raw}\n");
}
}
}
Ok(crate::engine::ScannedFile {
fragments: out,
unannotated: Vec::new(),
})
}
const SCANNING: &[StepKindSpec] = &[StepKindSpec {
prefix: "alt",
schema: "true",
validate: None,
fragments: Some(crate::engine::FragmentSupport {
ext: "frag",
scan: fake_scan,
}),
options: Some(fake_recognise),
}];
fn fake_recognise(key: &str) -> Option<crate::engine::RawOption> {
use crate::engine::{RawOption, RawOptionValue};
let (family, value) = match key {
"retry" => (Some("retry"), Some(RawOptionValue::Count)),
"repeat" => (None, Some(RawOptionValue::Count)),
"delay" => (Some("delay"), Some(RawOptionValue::Duration)),
"retry-interval" => (Some("retry"), None),
_ => return None,
};
Some(RawOption { family, value })
}
fn source(name: &str, text: &str) -> PackSource {
PackSource {
name: name.to_owned(),
text: Arc::from(text),
}
}
fn diags_of(packs: &[PackSource], fragments: &[PackSource]) -> Vec<crate::diag::Diag> {
let corpus = pack::FragmentCorpus::new(fragments.to_vec(), SCANNING);
match pack::load(packs, &corpus, SCANNING) {
Ok(_) => Vec::new(),
Err(FrontError::Diagnostics(diags)) => diags,
Err(other) => panic!("diagnostics expected, got {other:?}"),
}
}
fn has(diags: &[crate::diag::Diag], code: &str) -> bool {
diags.iter().any(|d| d.code == code)
}
#[test]
fn a_ref_names_a_loaded_fragment() {
let packs = pack::load(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: admin.search\n",
)],
&pack::FragmentCorpus::new(vec![source("api.frag", "@admin.search\n")], SCANNING),
SCANNING,
)
.unwrap_or_else(|err| panic!("should load: {err:?}"));
assert_eq!(packs.fragments.len(), 1);
assert!(packs.find_fragment("admin.search").is_some());
assert!(packs.find_fragment("api.frag#admin.search").is_some());
assert!(packs.find_fragment("other.frag#admin.search").is_none());
}
#[test]
fn a_ref_to_an_unknown_fragment_is_rejected_with_a_suggestion() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: admin.serch\n",
)],
&[source("api.frag", "@admin.search\n")],
);
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::unknown_ref")
.unwrap_or_else(|| panic!("expected unknown_ref in {diags:?}"));
assert!(diag.message.contains("did you mean `admin.search`?"));
}
#[test]
fn an_unknown_ref_with_no_fragments_loaded_points_at_the_config() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: admin.search\n",
)],
&[],
);
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::unknown_ref")
.unwrap_or_else(|| panic!("expected unknown_ref in {diags:?}"));
assert!(
diag.help
.as_deref()
.unwrap_or_default()
.contains("fragments"),
"{:?}",
diag.help
);
}
#[test]
fn a_step_is_one_body_form_only() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: f\n alt: |\n GET http://x\n",
)],
&[source("api.frag", "@f\n")],
);
assert!(has(&diags, "proef::pack::body_form_conflict"), "{diags:?}");
}
#[test]
fn a_fragments_option_values_are_capped_like_an_inline_blocks() {
for (line, code) in [
("retry: -1", "proef::pack::retry_not_finite"),
("repeat: -1", "proef::pack::retry_not_finite"),
("delay: 99999999", "proef::pack::delay_unbounded"),
] {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: f\n",
)],
&[source("api.frag", &format!("@f\n+{line}\n"))],
);
assert!(has(&diags, code), "`{line}` in a fragment: {diags:?}");
}
}
#[test]
fn a_conflicted_body_form_is_still_a_ref_at_every_scope() {
let diags = diags_of(
&[source(
"p.yaml",
"bind:\n a: b\nmacros:\n m:\n match: it runs\n steps:\n - ref: f\n alt: |\n GET http://x\n",
)],
&[source("api.frag", "@f\n")],
);
assert!(has(&diags, "proef::pack::body_form_conflict"), "{diags:?}");
assert!(
!has(&diags, "proef::pack::bind_without_ref"),
"the pack does have a `ref:` — {diags:?}"
);
}
#[test]
fn bind_without_a_ref_is_rejected() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - bind: { a: b }\n alt: |\n GET http://x\n",
)],
&[],
);
assert!(has(&diags, "proef::pack::bind_without_ref"), "{diags:?}");
}
#[test]
fn a_macro_scope_bind_with_no_ref_step_is_rejected() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n target:\n match: the target\n steps:\n - ref: f\n m:\n match: it runs\n bind:\n a: b\n steps:\n - use: target\n",
)],
&[source("api.frag", "@f\n")],
);
assert!(has(&diags, "proef::pack::bind_without_ref"), "{diags:?}");
}
#[test]
fn a_macro_scope_bind_beside_a_ref_step_is_accepted() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n bind:\n a: b\n steps:\n - ref: f\n",
)],
&[source("api.frag", "@f\n")],
);
assert!(!has(&diags, "proef::pack::bind_without_ref"), "{diags:?}");
}
#[test]
fn a_variable_the_fragment_supplies_and_the_pack_binds_is_refused() {
for (scope, pack) in [
(
"step's",
"macros:\n m:\n match: it runs\n steps:\n - ref: f\n bind:\n token: v\n",
),
(
"macro's",
"macros:\n m:\n match: it runs\n bind:\n token: v\n steps:\n - ref: f\n",
),
(
"pack's",
"bind:\n token: v\nmacros:\n m:\n match: it runs\n steps:\n - ref: f\n",
),
] {
let diags = diags_of(
&[source("p.yaml", pack)],
&[source("api.frag", "@f\n=token\n")],
);
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::option_declared_twice")
.unwrap_or_else(|| panic!("expected {scope} clash in {diags:?}"));
assert!(
diag.message.contains("token") && diag.message.contains(scope),
"{scope}: {}",
diag.message
);
}
}
#[test]
fn a_variable_only_the_fragment_supplies_is_accepted() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: f\n",
)],
&[source("api.frag", "@f\n=token\n")],
);
assert!(
!has(&diags, "proef::pack::option_declared_twice"),
"{diags:?}"
);
}
#[test]
fn a_pack_scope_bind_with_no_ref_anywhere_is_rejected() {
let diags = diags_of(
&[source(
"p.yaml",
"bind:\n unused: v\nmacros:\n m:\n match: it runs\n steps:\n - hurl: |\n GET http://x\n HTTP 200\n",
)],
&[source("api.frag", "@f\n")],
);
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::bind_without_ref")
.unwrap_or_else(|| panic!("expected bind_without_ref in {diags:?}"));
assert!(
diag.message.contains("no macro in this pack"),
"{}",
diag.message
);
}
#[test]
fn a_pack_scope_bind_beside_a_ref_is_accepted() {
let diags = diags_of(
&[source(
"p.yaml",
"bind:\n used: v\nmacros:\n m:\n match: it runs\n steps:\n - ref: f\n",
)],
&[source("api.frag", "@f\n")],
);
assert!(!has(&diags, "proef::pack::bind_without_ref"), "{diags:?}");
}
#[test]
fn fragment_names_are_global() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: dup\n",
)],
&[source("a.frag", "@dup\n"), source("b.frag", "@dup\n")],
);
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::duplicate_fragment")
.unwrap_or_else(|| panic!("expected duplicate_fragment in {diags:?}"));
assert!(diag.message.contains("a.frag") && diag.message.contains("b.frag"));
}
#[test]
fn an_unreadable_fragment_file_does_not_sink_the_others() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: ok\n",
)],
&[source("bad.frag", "@!boom\n"), source("good.frag", "@ok\n")],
);
assert!(has(&diags, "proef::pack::bad_annotation"), "{diags:?}");
assert!(
!has(&diags, "proef::pack::unknown_ref"),
"the readable file still loaded: {diags:?}"
);
}
#[test]
fn retry_declared_by_both_fragment_and_step_is_rejected() {
let diags = diags_of(
&[source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: poll\n retry: { count: 3, interval_ms: 200 }\n",
)],
&[source("api.frag", "@poll\nretry\n")],
);
let diag = diags
.iter()
.find(|d| d.code == "proef::pack::option_declared_twice")
.unwrap_or_else(|| panic!("expected option_declared_twice in {diags:?}"));
assert!(diag.message.contains("fragment `poll`"), "{}", diag.message);
}
#[test]
fn bind_scopes_survive_loading() {
let packs = pack::load(
&[source(
"p.yaml",
"bind:\n base: ${url:base}\nmacros:\n m:\n match: it runs\n bind:\n q: ${q}\n steps:\n - ref: f\n bind:\n id: \"{{recordId}}\"\n",
)],
&pack::FragmentCorpus::new(
vec![source("api.frag", "@f\n?base\n?q\n?id\n")],
SCANNING,
),
SCANNING,
)
.unwrap_or_else(|err| panic!("should load: {err:?}"));
assert_eq!(packs.bind["p.yaml"]["base"], "${url:base}");
let macro_ = &packs.macros["m"];
assert_eq!(macro_.bind["q"], "${q}");
let crate::pack::MacroBody::Steps(steps) = ¯o_.body else {
panic!("steps expected");
};
assert_eq!(steps[0].bind["id"], "{{recordId}}");
}
#[test]
fn one_corpus_is_scanned_once_however_many_loads_read_it() {
let packs = [source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: admin.search\n",
)];
let corpus =
pack::FragmentCorpus::new(vec![source("api.frag", "@admin.search\n")], SCANNING);
let (first, _) = pack::load_collecting(&packs, &corpus, SCANNING);
let (second, _) = pack::load_collecting(&packs, &corpus, SCANNING);
assert!(first.find_fragment("admin.search").is_some());
assert!(
Arc::ptr_eq(&first.fragments, &second.fragments),
"a second load must reuse the first scan, not repeat it"
);
}
#[test]
fn a_corpus_no_pack_refs_is_never_scanned() {
let unreadable = vec![source("api.frag", "@!boom\n")];
let no_ref = [source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - alt: GET /x\n",
)];
let corpus = pack::FragmentCorpus::new(unreadable.clone(), SCANNING);
let (_, diags) = pack::load_collecting(&no_ref, &corpus, SCANNING);
assert!(
!diags
.iter()
.any(|d| d.code == "proef::pack::bad_annotation"),
"no `ref:` anywhere, so the corpus must never be read: {diags:?}"
);
let with_ref = [source(
"p.yaml",
"macros:\n m:\n match: it runs\n steps:\n - ref: whatever\n",
)];
let corpus = pack::FragmentCorpus::new(unreadable, SCANNING);
let (_, diags) = pack::load_collecting(&with_ref, &corpus, SCANNING);
assert!(
diags
.iter()
.any(|d| d.code == "proef::pack::bad_annotation"),
"a pack with a `ref:` must reach the scanner: {diags:?}"
);
}
}