///|
/// Human-readable failure report, in the style of cucumber-js. `moon test`
/// prints this when `run_or_fail` raises, so it names each failed feature,
/// scenario and step:
///
/// ```text
/// 1 failed, 1 total
///
/// 1) Feature: Add / Scenario: wrong sum # features/add.feature:3
/// ✔ Given the numbers 1 and 2
/// ✖ Then the result is 4
/// Expected 3 to equal 4
/// - And it is logged
/// ```
///
/// Only `RunFailed` uses color, when its `color` field is true.
pub impl Show for MoonspecError with fn output(self, logger) {
let lines : Array[String] = []
error_lines(self, lines, "", false)
logger.write_string(lines.join("\n"))
}
///|
pub extend MoonspecError with Show::{to_string, output}
///|
/// Indent of step lines inside a scenario block, aligned after "1) ".
let item_indent = " "
///|
/// Extra indent of the detail lines under a step.
let detail_indent = " "
///|
fn error_lines(
err : MoonspecError,
lines : Array[String],
indent : String,
color : Bool,
) -> Unit {
match err {
UndefinedStep(..) | PendingStep(..) | StepFailed(..) =>
step_error_lines(err, lines, indent, color)
ParseFailed(..) => parse_lines(err, lines, indent, color)
ScenarioFailed(..) => scenario_lines(err, lines, indent, color)
RunFailed(summary~, errors~, color~) => {
lines.push(summary)
for i, error in errors {
lines.push("")
error_lines(error, lines, "\{i + 1}) ", color)
}
}
}
}
///|
/// Print a scenario block. `prefix` goes in front of the header line only.
fn scenario_lines(
err : MoonspecError,
lines : Array[String],
prefix : String,
color : Bool,
) -> Unit {
guard err is ScenarioFailed(scenario~, feature~, location~, steps~, errors~) else {
return
}
let where_ = match location {
Some(loc) => " # " + loc
None => ""
}
lines.push(prefix + "Feature: \{feature} / Scenario: \{scenario}" + where_)
if steps.is_empty() {
for error in errors {
error_lines(error, lines, item_indent, color)
}
return
}
for step in steps {
match step {
Passed(keyword~, step~) =>
lines.push(
item_indent + paint(step_text("✔", keyword, step), Green, color),
)
Skipped(keyword~, step~) =>
lines.push(
item_indent + paint(step_text("-", keyword, step), Cyan, color),
)
Problem(error) => error_lines(error, lines, item_indent, color)
}
}
}
///|
fn parse_lines(
err : MoonspecError,
lines : Array[String],
prefix : String,
color : Bool,
) -> Unit {
guard err is ParseFailed(uri~, line~, message~) else { return }
let where_ = match line {
Some(n) => uri + ":" + n.to_string()
None => uri
}
lines.push(prefix + "Parse error: " + where_)
let detail = (if prefix == "" { "" } else { item_indent }) + detail_indent
push_painted(lines, detail, message, Red, color)
}
///|
fn step_error_lines(
err : MoonspecError,
lines : Array[String],
indent : String,
color : Bool,
) -> Unit {
let detail = indent + detail_indent
match err {
UndefinedStep(step~, keyword~, snippet~, suggestions~) => {
lines.push(indent + paint(step_text("?", keyword, step), Yellow, color))
push_painted(
lines,
detail,
"Undefined step. Implement it with:",
Yellow,
color,
)
push_painted(lines, detail + " ", snippet, Yellow, color)
if suggestions.length() > 0 {
push_painted(lines, detail, "Did you mean:", Yellow, color)
for suggestion in suggestions {
push_painted(lines, detail + " ", suggestion, Yellow, color)
}
}
}
PendingStep(step~, keyword~, message~) => {
lines.push(indent + paint(step_text("?", keyword, step), Yellow, color))
push_painted(lines, detail, "Pending: " + message, Yellow, color)
}
StepFailed(step~, keyword~, message~) => {
lines.push(indent + paint(step_text("✖", keyword, step), Red, color))
push_painted(lines, detail, message, Red, color)
}
_ => error_lines(err, lines, indent, color)
}
}
///|
fn step_text(marker : String, keyword : String, step : String) -> String {
marker + " " + keyword.trim(chars=" ").to_owned() + " " + step
}
///|
/// Push each line of `text` with `indent` in front of it. Each line is painted
/// on its own, so line-based readers never see a color span a line break.
fn push_painted(
lines : Array[String],
indent : String,
text : String,
c : Color,
color : Bool,
) -> Unit {
for line in text.split("\n") {
lines.push(indent + paint(line.to_owned(), c, color))
}
}