///|
pub struct CliResult {
stdout : String
stderr : String
status : Int32
}
///|
pub fn usage() -> String {
let text =
#|Usage: junit-delta compare --baseline BASE.xml [--baseline OLDER-TO-NEWER.xml ...]
#| --candidate CURRENT.xml [--candidate SHARD.xml ...]
#| [--format json|md|text] [--duplicates error|attempts]
#| [--fail-on LIST|none] [--slowdown-pct 50]
#| [--slowdown-floor-ms 200] [--limit 50]
#| junit-delta jsonl
#| junit-delta merge [--glob] [--suite-name NAME] INPUT... OUTPUT
#| junit-delta verify [--glob] INPUT...
#| junit-delta --help | --version
#|
#|Repeated baselines are chronological runs, not shards. Candidates are current-run shards.
#|JSONL reads one request per line from stdin. '-' reads stdin once in compare mode.
#|Gate names: new_failure,added_failing,still_failing,removed,newly_skipped,flaky,slowdown,empty_report
#|Default gates: new_failure,added_failing,removed,empty_report.
#|Exit: 0 pass, 1 gate violation, 2 input/usage error. Local tool: no network calls.
#|
text
}
///|
fn uint_arg(text : String, maximum : Int64) -> Int64 raise InputError {
if text == "" {
raise InvalidInput("expected an unsigned integer")
}
let mut value = 0L
for c in text {
if c < '0' || c > '9' {
raise InvalidInput("expected unsigned integer: \{text}")
}
let digit = (c.to_int() - 48).to_int64()
if value > (maximum - digit) / 10L {
raise InvalidInput("integer exceeds \{maximum}")
}
value = value * 10L + digit
if value > maximum {
raise InvalidInput("integer exceeds \{maximum}")
}
}
value
}
///|
/// The callback owns all I/O. This function can be tested on any MoonBit target.
pub fn run_cli(
args : Array[String],
read : (String) -> String raise,
write~ : ((String, String) -> Unit raise)? = None,
glob~ : ((String) -> Array[String] raise)? = None,
) -> CliResult {
if !args.is_empty() && (args[0] == "merge" || args[0] == "verify") {
return run_report_cli(args, read, write~, glob~)
}
try {
if args == ["--help"] || args == ["-h"] {
return { stdout: usage(), stderr: "", status: 0, }
}
if args == ["--version"] || args == ["-v"] {
return {
stdout: "junit-delta \{release_version}\n",
stderr: "",
status: 0,
}
}
if args == ["jsonl"] {
let input = read("-")
if input.length() > 20000000 {
raise InvalidInput("JSONL input limit exceeded")
}
return run_jsonl(input)
}
if args.is_empty() || args[0] != "compare" {
raise InvalidInput(usage())
}
let bases : Array[String] = []
let candidates : Array[String] = []
let seen : Map[String, Bool] = Map([])
let mut format = "text"
let mut duplicates = Reject
let mut fail_on = default_config().fail_on.iter().to_array()
let mut pct = 50
let mut floor_us = 200000L
let mut limit = 50
let mut i = 1
while i < args.length() {
let option = args[i]
if i + 1 >= args.length() {
raise InvalidInput("missing value for \{option}")
}
let value = args[i + 1]
if option != "--baseline" && option != "--candidate" {
if seen.contains(option) {
raise InvalidInput("repeated option \{option}")
}
seen[option] = true
}
match option {
"--baseline" => bases.push(value)
"--candidate" => candidates.push(value)
"--format" => format = value
"--duplicates" => duplicates = parse_duplicates(value)
"--fail-on" =>
fail_on = if value == "none" {
[]
} else {
value.split(",").map(s => s.to_owned()).collect()
}
"--slowdown-pct" => pct = uint_arg(value, 1000000L).to_int()
"--slowdown-floor-ms" => floor_us = uint_arg(value, 9000000000L) * 1000L
"--limit" => limit = uint_arg(value, 100000L).to_int()
_ => raise InvalidInput("unknown option: \{option}")
}
i = i + 2
}
if bases.is_empty() ||
bases.length() > 50 ||
candidates.is_empty() ||
candidates.length() > 50 {
raise InvalidInput("provide 1..50 baselines and 1..50 candidates")
}
if format != "json" && format != "md" && format != "text" {
raise InvalidInput("format must be json, md or text")
}
let config = diff_config(
slowdown_pct=pct,
slowdown_floor_us=floor_us,
fail_on~,
)
let mut stdin_seen = false
let mut total = 0
let load = fn(path : String) -> Report raise {
if path == "-" {
if stdin_seen {
raise InvalidInput("stdin may only be used once")
}
stdin_seen = true
}
let content = read(path)
total = total + content.length()
if total > 20000000 {
raise InvalidInput("combined XML input limit exceeded")
}
parse_report(content, source=path, duplicates~)
}
let history = bases.map(load)
let candidate = merge_reports(candidates.map(load), duplicates~)
let result = compare(history, candidate, config~)
let stdout = match format {
"json" => result.to_json_string() + "\n"
"md" => result.to_markdown(limit~)
_ => result.to_text()
}
{ stdout, stderr: "", status: result.exit_code(), }
} catch {
error =>
{
stdout: "",
stderr: "junit-delta: " +
terminal_text(clipped(error.to_string(), 2048)) +
"\n",
status: 2,
}
}
}
///|
fn jsonl_limit_line(line_number : Int32) -> String {
(
{
"ok": false,
"exit_code": 2,
"error_code": "output_limit",
"error": "JSONL output limit exceeded; remaining lines were not processed",
"line": Json(line_number),
} : Json).stringify() +
"\n"
}
///|
fn run_jsonl(
input : String,
output_limit~ : Int32 = 100000000,
) -> CliResult raise InputError {
let out = StringBuilder()
let mut status = 0
let mut output_units = 0
let mut line_number = 0
for line in input.split("\n") {
line_number = line_number + 1
if line.trim().is_empty() {
continue
}
let remaining = output_limit - output_units - 512
// Reserve a complete final error plus the success envelope and newline.
if remaining < 64 {
out.write_string(jsonl_limit_line(line_number))
status = 2
break
}
let result = evaluate_request_limited(
line.to_owned(),
(remaining - 64).to_int64(),
)
if result is { "ok": false, "error_code": "output_limit", .. } {
out.write_string(jsonl_limit_line(line_number))
status = 2
break
}
let code = integer_field(result, "exit_code", 2L, 2L).to_int()
if code > status {
status = code
}
let rendered = result.stringify() + "\n"
// Also covers bounded parse/validation errors when little room remains.
if rendered.length() > remaining {
out.write_string(jsonl_limit_line(line_number))
status = 2
break
}
output_units = output_units + rendered.length()
out.write_string(rendered)
}
{ stdout: out.to_string(), stderr: "", status, }
}