use active_core::fmt::format;
use active_core::{parse, Engine, Style};
fn parse_ok(src: &str) -> active_core::Module {
parse(src, "fmt").unwrap_or_else(|d| panic!("parse failed: {d:?}"))
}
fn fmt(src: &str, style: Style) -> String {
format(&parse_ok(src), style)
}
fn tree(src: &str) -> String {
let module = parse_ok(src);
let engine = Engine::new();
let (value, _) = engine.evaluate(&module).expect("must evaluate");
active_core::convert::value_to_pretty_json(&value)
}
const BRACE_DOC: &str = "[app]\nname = \"svc\"\nport = 8080\n\n[database]\nhost = \"db\"\n";
const INDENT_DOC: &str = "app:\n name = \"svc\"\n port = 8080\n\ndatabase:\n host = \"db\"\n";
#[test]
fn canonical_brace_document_round_trips_byte_for_byte() {
assert_eq!(fmt(BRACE_DOC, Style::Brace), BRACE_DOC);
assert_eq!(fmt(BRACE_DOC, Style::Preserve), BRACE_DOC);
}
#[test]
fn canonical_indent_document_round_trips_byte_for_byte() {
assert_eq!(fmt(INDENT_DOC, Style::Indent), INDENT_DOC);
assert_eq!(fmt(INDENT_DOC, Style::Preserve), INDENT_DOC);
}
#[test]
fn formatting_is_idempotent() {
for src in [BRACE_DOC, INDENT_DOC] {
for style in [Style::Brace, Style::Indent, Style::Preserve] {
let once = fmt(src, style);
let twice = fmt(&once, style);
assert_eq!(once, twice, "not idempotent for {style:?}:\n{once}");
}
}
}
#[test]
fn formatting_is_idempotent_on_the_whole_positive_corpus() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/positive");
let mut files: Vec<_> = std::fs::read_dir(&dir)
.expect("positive corpus")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "active"))
.collect();
files.sort();
assert!(files.len() >= 20, "expected the full corpus");
for f in files {
let src = std::fs::read_to_string(&f).expect("read");
let module = parse(&src, "fmt").unwrap_or_else(|d| panic!("{f:?}: {d:?}"));
for style in [Style::Brace, Style::Indent] {
let once = format(&module, style);
let reparsed = parse(&once, "fmt").unwrap_or_else(|d| {
panic!("{f:?}: formatted output must re-parse ({style:?}): {d:?}\n{once}")
});
let twice = format(&reparsed, style);
assert_eq!(once, twice, "not idempotent for {f:?} ({style:?})");
let engine = Engine::new();
match (engine.evaluate(&module), engine.evaluate(&reparsed)) {
(Ok((a, _)), Ok((b, _))) => {
let (ja, jb) = (
active_core::convert::value_to_pretty_json(&a),
active_core::convert::value_to_pretty_json(&b),
);
assert_eq!(
ja, jb,
"{f:?}: formatted output changed the tree ({style:?})"
);
}
(Err(_), Err(_)) => {}
(a, b) => panic!(
"{f:?}: formatting changed the evaluation outcome ({style:?}): \
original ok={}, formatted ok={}",
a.is_ok(),
b.is_ok()
),
}
}
}
}
#[test]
fn comment_placement_is_exact() {
let src = "# document header\n[app]\n# leading on name\nname = \"svc\" # trailing\n# before database\n\n[database]\nhost = \"db\"\n";
let out = fmt(src, Style::Brace);
assert_eq!(out, src, "canonical comment placement must round-trip");
for needle in [
"# document header",
"# leading on name",
"# trailing",
"# before database",
] {
assert_eq!(out.matches(needle).count(), 1, "{needle:?} in:\n{out}");
}
assert_eq!(
fmt(&out, Style::Brace),
out,
"comment placement must be idempotent"
);
}
#[test]
fn trailing_comment_stays_on_a_table_header_line() {
let out = fmt("[app] # the app table\nname = \"svc\"\n", Style::Brace);
assert!(
out.starts_with("[app] # the app table"),
"header comment must stay on the header line:\n{out}"
);
}
#[test]
fn trailing_comments_survive_on_every_header_form() {
let indent_src = "app: # the app block\n name = \"svc\"\n";
let indent_out = fmt(indent_src, Style::Indent);
assert!(
indent_out.starts_with("app: # the app block"),
"indent header comment lost:\n{indent_out}"
);
let nested_src = "outer {\n inner { # the inner block\n a = 1\n }\n}\n";
let nested_out = fmt(nested_src, Style::Brace);
assert!(
nested_out.contains("inner { # the inner block"),
"nested header comment lost:\n{nested_out}"
);
let empty_src = "[outer]\nempty = {} # the empty table\n";
let empty_out = fmt(empty_src, Style::Indent);
assert!(
empty_out.contains("empty = {} # the empty table"),
"empty-table comment lost:\n{empty_out}"
);
}
#[test]
fn duplicate_key_comments_are_not_dropped() {
let out = fmt("[app]\nx = 1 # first\nx = 2 # second\n", Style::Brace);
assert!(out.contains("# first"), "{out}");
assert!(out.contains("# second"), "{out}");
}
#[test]
fn empty_table_keeps_its_trailing_comment_in_indent_style() {
let src = "outer:\n empty = {}\n # belongs to outer\n";
let out = fmt(src, Style::Indent);
assert!(
out.contains("# belongs to outer"),
"block comment must survive the empty-table path:\n{out}"
);
}
#[test]
fn large_and_small_floats_re_parse() {
let src = "[nums]\nbig = 100000000000000000000.0\ntiny = 0.00000000000000000001\nplain = 2.0\n";
let module = parse_ok(src);
let out = format(&module, Style::Brace);
let reparsed = parse(&out, "fmt")
.unwrap_or_else(|d| panic!("formatted floats must re-parse: {d:?}\n{out}"));
let engine = Engine::new();
let (a, _) = engine.evaluate(&module).expect("original evaluates");
let (b, _) = engine.evaluate(&reparsed).expect("formatted evaluates");
assert_eq!(
active_core::convert::value_to_pretty_json(&a),
active_core::convert::value_to_pretty_json(&b),
"float values must survive formatting:\n{out}"
);
}
#[test]
fn env_default_operand_is_parenthesized() {
let src = "[calc]\nv = (env.ACTIVE_FMT_TEST ?: 1) + 2\n";
let module = parse_ok(src);
let out = format(&module, Style::Brace);
assert!(
out.contains("(env.ACTIVE_FMT_TEST ?: 1) + 2"),
"env default operand must be parenthesized:\n{out}"
);
let reparsed = parse(&out, "fmt").expect("must re-parse");
let engine = Engine::new();
let (a, _) = engine.evaluate(&module).expect("original evaluates");
let (b, _) = engine.evaluate(&reparsed).expect("formatted evaluates");
assert_eq!(
active_core::convert::value_to_pretty_json(&a),
active_core::convert::value_to_pretty_json(&b)
);
}
#[test]
fn long_left_associative_chains_do_not_overflow() {
let mut src = String::from("[calc]\nv = 1");
for _ in 0..20_000 {
src.push_str(" + 1");
}
src.push('\n');
let module = parse_ok(&src);
let out = format(&module, Style::Brace);
assert!(out.contains("+ 1"), "chain must be rendered");
parse(&out, "fmt").expect("formatted chain must re-parse");
}
#[test]
fn comments_survive_every_mode() {
let src = "# document header\napp:\n # leading on name\n name = \"svc\" # trailing\n port = 8080\n # trailing block\n\n# before database\n[database]\nhost = \"db\"\n";
let module = parse_ok(src);
assert_eq!(module.trivia.len(), 5, "all five comments must be recorded");
for style in [Style::Brace, Style::Indent, Style::Preserve] {
let out = format(&module, style);
for needle in [
"# document header",
"# leading on name",
"# trailing",
"# trailing block",
"# before database",
] {
assert!(out.contains(needle), "{needle:?} lost in {style:?}:\n{out}");
}
}
}
#[test]
fn annotations_are_preserved_like_comments() {
let src =
"[app]\n# @intent: application identity\nname = \"svc\"\n# @ai_immutable\nport = 8080\n";
let out = fmt(src, Style::Indent);
assert!(out.contains("# @intent: application identity"), "{out}");
assert!(out.contains("# @ai_immutable"), "{out}");
}
#[test]
fn styles_agree_on_the_tree_and_produce_each_other() {
assert_eq!(fmt(INDENT_DOC, Style::Brace), BRACE_DOC);
assert_eq!(fmt(BRACE_DOC, Style::Indent), INDENT_DOC);
assert_eq!(tree(BRACE_DOC), tree(INDENT_DOC));
}
#[test]
fn preserve_mode_follows_the_source_style() {
let from_indent = fmt(INDENT_DOC, Style::Preserve);
assert!(from_indent.contains("app:"), "{from_indent}");
let from_brace = fmt(BRACE_DOC, Style::Preserve);
assert!(from_brace.contains("[app]"), "{from_brace}");
}
#[test]
fn let_bindings_and_expressions_round_trip() {
let src = "let region = env.AWS_REGION ?: \"us-east-1\"\nlet timeout = 5s\n\n[app]\nregion = region\nurl = \"https://api.${region}.example.com\"\nwindow = timeout * 2\nflag = !false\nratio = 1.5\n";
let module = parse_ok(src);
for style in [Style::Brace, Style::Indent] {
let once = format(&module, style);
let reparsed = parse(&once, "fmt")
.unwrap_or_else(|d| panic!("must re-parse ({style:?}): {d:?}\n{once}"));
let twice = format(&reparsed, style);
assert_eq!(once, twice, "not idempotent ({style:?})");
}
}
#[test]
fn precedence_needs_no_extra_parentheses_but_keeps_meaning() {
let src = "[calc]\na = 2 + 3 * 4\nb = (2 + 3) * 4\nc = 10 - 2 - 3\n";
let module = parse_ok(src);
let out = format(&module, Style::Brace);
let reparsed = parse(&out, "fmt").expect("formatted output must re-parse");
let engine = Engine::new();
let (a, _) = engine.evaluate(&module).expect("evaluate original");
let (b, _) = engine.evaluate(&reparsed).expect("evaluate formatted");
assert_eq!(
active_core::convert::value_to_pretty_json(&a),
active_core::convert::value_to_pretty_json(&b),
"parenthesization must preserve meaning:\n{out}"
);
assert!(
out.contains("(2 + 3) * 4"),
"explicit grouping must survive:\n{out}"
);
}