///|
/// Text rendering of audit results.
///
/// Every line of the report is stable and greppable, so the output can be
/// pasted into a change ticket, diffed between pipeline runs or filtered with
/// `grep`. The format is covered by tests.
///|
/// Render diagnostics one per line, without indentation. An empty list renders
/// as an empty string.
pub fn render_diagnostics(diagnostics : Array[Diagnostic]) -> String {
let builder = StringBuilder()
for index = 0; index < diagnostics.length(); index = index + 1 {
if index > 0 {
builder.write_char('\n')
}
builder.write_string(diagnostics[index].to_line())
}
builder.to_string()
}
///|
/// Append a titled, indented section. `(none)` marks an empty section.
fn write_section(
builder : StringBuilder,
title : String,
lines : Array[String],
) -> Unit {
builder.write_char('\n')
builder.write_string(title)
builder.write_char('\n')
if lines.length() == 0 {
builder.write_string(" (none)\n")
} else {
for line in lines {
builder.write_string(" ")
builder.write_string(line)
builder.write_char('\n')
}
}
}
///|
/// Render a whole audit as a text report.
///
/// The summary carries the counts, the blocked time and the `status` line:
/// `FAIL` when at least one error was found, `PASS` otherwise. Warnings alone
/// keep the status at `PASS`.
pub fn render_report(report : AuditReport) -> String {
let builder = StringBuilder()
builder.write_string("Moon Change Window Audit\n")
builder.write_string("windows: " + report.change_count().to_string() + "\n")
builder.write_string("freezes: " + report.freeze_count().to_string() + "\n")
builder.write_string("errors: " + report.error_count().to_string() + "\n")
builder.write_string("warnings: " + report.warning_count().to_string() + "\n")
builder.write_string(
"overlaps: " + report.overlaps.length().to_string() + "\n",
)
builder.write_string(
"freeze conflicts: " + report.freeze_conflicts.length().to_string() + "\n",
)
builder.write_string(
"dependency issues: " + report.dependency_issues.length().to_string() + "\n",
)
builder.write_string(
"blocked: " + format_duration(report.blocked_minutes) + "\n",
)
builder.write_string("status: " + report.status() + "\n")
let window_lines : Array[String] = []
for window in sort_windows_by_start(report.plan.changes()) {
window_lines.push(window.to_line())
}
write_section(builder, "WINDOWS", window_lines)
let freeze_lines : Array[String] = []
for freeze in sort_windows_by_start(report.plan.freezes()) {
freeze_lines.push(freeze.to_freeze_line())
}
write_section(builder, "FREEZES", freeze_lines)
let overlap_lines : Array[String] = []
for overlap in report.overlaps {
overlap_lines.push(overlap.to_line())
}
write_section(builder, "OVERLAPS", overlap_lines)
let conflict_lines : Array[String] = []
for conflict in report.freeze_conflicts {
conflict_lines.push(conflict.to_line())
}
write_section(builder, "FREEZE CONFLICTS", conflict_lines)
let issue_lines : Array[String] = []
for issue in report.dependency_issues {
issue_lines.push(issue.to_line())
}
write_section(builder, "DEPENDENCY ISSUES", issue_lines)
let diagnostic_lines : Array[String] = []
for diagnostic in report.plan.diagnostics {
diagnostic_lines.push(diagnostic.to_line())
}
write_section(builder, "DIAGNOSTICS", diagnostic_lines)
builder.to_string()
}
///|
/// Render the slots returned by `safe_slots`, one per line with its length.
pub fn render_slots(slots : Array[Slot]) -> String {
let builder = StringBuilder()
builder.write_string(
"Safe change slots (" + slots.length().to_string() + ")\n",
)
if slots.length() == 0 {
builder.write_string(" (none)\n")
} else {
for slot in slots {
builder.write_string(" ")
builder.write_string(slot.to_line())
builder.write_char('\n')
}
}
builder.to_string()
}