///|
/// Parse errors for the stable MoonFormData conformance baseline format.
pub(all) enum BaselineError {
InvalidBaselineHeader(String)
MissingBaselineSuite
InvalidBaselineLine(String)
InvalidBaselineExpectation(String)
InvalidBaselineActual(String)
InvalidBaselineStatus(String)
InvalidBaselineEscape(String)
} derive(Debug, Eq)
///|
/// Serializable behavior snapshot for one conformance case.
pub(all) struct ConformanceBaselineEntry {
name : String
expectation : ConformanceExpectation
actual : ConformanceActual
passed : Bool
issue_codes : Array[String]
} derive(Debug, Eq)
///|
/// Versioned, text-serializable snapshot of one conformance suite.
pub(all) struct ConformanceBaseline {
suite_name : String
entries : Array[ConformanceBaselineEntry]
} derive(Debug, Eq)
///|
/// Impact assigned to a behavior change relative to a stored baseline.
pub(all) enum BaselineChangeImpact {
BaselineCompatible
BaselineReview
BaselineRegression
} derive(Debug, Eq)
///|
/// Kind of behavior change detected between a baseline and a current suite.
pub(all) enum BaselineChangeKind {
BaselineSuiteRenamed
BaselineCaseAdded
BaselineCaseRemoved
BaselineExpectationChanged
BaselineActualChanged
BaselinePassStatusChanged
BaselineIssueCodesChanged
} derive(Debug, Eq)
///|
/// One deterministic conformance baseline difference.
pub(all) struct BaselineChange {
impact : BaselineChangeImpact
kind : BaselineChangeKind
case_name : String
before : String
after : String
} derive(Debug, Eq)
///|
/// Complete baseline comparison for CI regression checks.
pub(all) struct BaselineDiff {
suite_name : String
changes : Array[BaselineChange]
} derive(Debug, Eq)
///|
/// Capture the observable result of a conformance suite.
pub fn conformance_baseline(
suite : ConformanceSuiteResult,
) -> ConformanceBaseline {
let entries = Array::new()
let mut i = 0
while i < suite.cases.length() {
let result = suite.cases[i]
entries.push({
name: result.name,
expectation: result.expectation,
actual: result.actual,
passed: result.passed,
issue_codes: baseline_clone_strings(result.issue_codes),
})
i = i + 1
}
{ suite_name: suite.name, entries }
}
///|
/// Parse a baseline previously produced by `ConformanceBaseline::to_text`.
pub fn parse_conformance_baseline(
source : String,
) -> Result[ConformanceBaseline, BaselineError] {
let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
if lines.length() == 0 || lines[0] != "moonformdata-conformance-baseline-v1" {
return Err(
InvalidBaselineHeader(
if lines.length() == 0 {
""
} else {
lines[0].to_owned()
},
),
)
}
if lines.length() < 2 || !lines[1].has_prefix("suite\t") {
return Err(MissingBaselineSuite)
}
let suite_line = lines[1].to_owned()
let suite_name = match baseline_unescape(slice_from(suite_line, 6)) {
Ok(value) => value
Err(err) => return Err(err)
}
let entries = Array::new()
let mut i = 2
while i < lines.length() {
if lines[i] != "" {
match baseline_parse_entry(lines[i].to_owned()) {
Ok(entry) => entries.push(entry)
Err(err) => return Err(err)
}
}
i = i + 1
}
Ok({ suite_name, entries })
}
///|
/// Compare a stored baseline with a newly executed conformance suite.
pub fn compare_conformance_baseline(
previous : ConformanceBaseline,
current : ConformanceSuiteResult,
) -> BaselineDiff {
let changes = Array::new()
if previous.suite_name != current.name {
changes.push({
impact: BaselineReview,
kind: BaselineSuiteRenamed,
case_name: "",
before: previous.suite_name,
after: current.name,
})
}
let mut i = 0
while i < previous.entries.length() {
let old_entry = previous.entries[i]
match baseline_find_result(current.cases, old_entry.name) {
Some(result) => baseline_compare_entry(old_entry, result, changes)
None =>
changes.push({
impact: BaselineRegression,
kind: BaselineCaseRemoved,
case_name: old_entry.name,
before: baseline_entry_state(old_entry),
after: "missing",
})
}
i = i + 1
}
let mut j = 0
while j < current.cases.length() {
let result = current.cases[j]
if baseline_find_entry(previous.entries, result.name) is None {
changes.push({
impact: BaselineCompatible,
kind: BaselineCaseAdded,
case_name: result.name,
before: "missing",
after: baseline_result_state(result),
})
}
j = j + 1
}
{ suite_name: current.name, changes }
}
///|
pub fn BaselineError::message(self : BaselineError) -> String {
match self {
InvalidBaselineHeader(value) =>
"invalid conformance baseline header: " + value
MissingBaselineSuite => "conformance baseline is missing its suite line"
InvalidBaselineLine(value) => "invalid conformance baseline line: " + value
InvalidBaselineExpectation(value) =>
"invalid baseline expectation: " + value
InvalidBaselineActual(value) => "invalid baseline actual outcome: " + value
InvalidBaselineStatus(value) => "invalid baseline pass status: " + value
InvalidBaselineEscape(value) => "invalid baseline percent escape: " + value
}
}
///|
/// Serialize a baseline using a stable, line-oriented, diff-friendly format.
pub fn ConformanceBaseline::to_text(self : ConformanceBaseline) -> String {
let lines = [
"moonformdata-conformance-baseline-v1",
"suite\t" + baseline_escape(self.suite_name),
]
let mut i = 0
while i < self.entries.length() {
lines.push(baseline_serialize_entry(self.entries[i]))
i = i + 1
}
lines.join("\n") + "\n"
}
///|
pub fn BaselineChangeImpact::label(self : BaselineChangeImpact) -> String {
match self {
BaselineCompatible => "compatible"
BaselineReview => "review"
BaselineRegression => "regression"
}
}
///|
pub fn BaselineChangeKind::label(self : BaselineChangeKind) -> String {
match self {
BaselineSuiteRenamed => "suite-renamed"
BaselineCaseAdded => "case-added"
BaselineCaseRemoved => "case-removed"
BaselineExpectationChanged => "expectation-changed"
BaselineActualChanged => "actual-changed"
BaselinePassStatusChanged => "pass-status-changed"
BaselineIssueCodesChanged => "issue-codes-changed"
}
}
///|
pub fn BaselineChange::to_line(self : BaselineChange) -> String {
self.impact.label() +
"|" +
self.kind.label() +
"|" +
self.case_name +
"|before=" +
self.before +
"|after=" +
self.after
}
///|
pub fn BaselineDiff::is_unchanged(self : BaselineDiff) -> Bool {
self.changes.length() == 0
}
///|
pub fn BaselineDiff::regression_count(self : BaselineDiff) -> Int {
self.count_impact(BaselineRegression)
}
///|
pub fn BaselineDiff::review_count(self : BaselineDiff) -> Int {
self.count_impact(BaselineReview)
}
///|
pub fn BaselineDiff::compatible_count(self : BaselineDiff) -> Int {
self.count_impact(BaselineCompatible)
}
///|
pub fn BaselineDiff::has_regressions(self : BaselineDiff) -> Bool {
self.regression_count() > 0
}
///|
pub fn BaselineDiff::summary(self : BaselineDiff) -> String {
"baseline-diff=" +
self.suite_name +
", regressions=" +
self.regression_count().to_string() +
", review=" +
self.review_count().to_string() +
", compatible=" +
self.compatible_count().to_string()
}
///|
pub fn BaselineDiff::to_lines(self : BaselineDiff) -> Array[String] {
let lines = [self.summary()]
let mut i = 0
while i < self.changes.length() {
lines.push(self.changes[i].to_line())
i = i + 1
}
lines
}
///|
/// Render a Markdown behavior-diff report for CI artifacts and code review.
pub fn BaselineDiff::to_markdown(self : BaselineDiff) -> String {
let lines = [
"# Upload conformance baseline diff",
"",
self.summary(),
"",
"| Impact | Change | Case | Before | After |",
"| --- | --- | --- | --- | --- |",
]
if self.changes.length() == 0 {
lines.push("| compatible | unchanged | - | - | - |")
} else {
let mut i = 0
while i < self.changes.length() {
let change = self.changes[i]
lines.push(
"| " +
change.impact.label() +
" | " +
change.kind.label() +
" | " +
baseline_markdown_cell(change.case_name) +
" | " +
baseline_markdown_cell(change.before) +
" | " +
baseline_markdown_cell(change.after) +
" |",
)
i = i + 1
}
}
lines.join("\n")
}
///|
fn BaselineDiff::count_impact(
self : BaselineDiff,
impact : BaselineChangeImpact,
) -> Int {
let mut count = 0
let mut i = 0
while i < self.changes.length() {
if self.changes[i].impact == impact {
count = count + 1
}
i = i + 1
}
count
}
///|
fn baseline_compare_entry(
previous : ConformanceBaselineEntry,
current : ConformanceCaseResult,
changes : Array[BaselineChange],
) -> Unit {
if previous.expectation != current.expectation {
changes.push({
impact: BaselineReview,
kind: BaselineExpectationChanged,
case_name: previous.name,
before: previous.expectation.label(),
after: current.expectation.label(),
})
}
if previous.actual != current.actual {
changes.push({
impact: BaselineRegression,
kind: BaselineActualChanged,
case_name: previous.name,
before: previous.actual.label(),
after: current.actual.label(),
})
}
if previous.passed != current.passed {
changes.push({
impact: if previous.passed {
BaselineRegression
} else {
BaselineCompatible
},
kind: BaselinePassStatusChanged,
case_name: previous.name,
before: baseline_pass_label(previous.passed),
after: baseline_pass_label(current.passed),
})
}
if !baseline_string_arrays_equal(previous.issue_codes, current.issue_codes) {
changes.push({
impact: BaselineReview,
kind: BaselineIssueCodesChanged,
case_name: previous.name,
before: previous.issue_codes.join(","),
after: current.issue_codes.join(","),
})
}
}
///|
fn baseline_serialize_entry(entry : ConformanceBaselineEntry) -> String {
let escaped_codes = Array::new()
let mut i = 0
while i < entry.issue_codes.length() {
escaped_codes.push(baseline_escape(entry.issue_codes[i]))
i = i + 1
}
"case\t" +
baseline_escape(entry.name) +
"\t" +
entry.expectation.label() +
"\t" +
entry.actual.label() +
"\t" +
baseline_pass_label(entry.passed) +
"\t" +
escaped_codes.join(",")
}
///|
fn baseline_parse_entry(
line : String,
) -> Result[ConformanceBaselineEntry, BaselineError] {
let fields = line.split("\t").collect()
if fields.length() != 6 || fields[0] != "case" {
return Err(InvalidBaselineLine(line))
}
let name = match baseline_unescape(fields[1].to_owned()) {
Ok(value) => value
Err(err) => return Err(err)
}
let expectation = match baseline_parse_expectation(fields[2].to_owned()) {
Ok(value) => value
Err(err) => return Err(err)
}
let actual = match baseline_parse_actual(fields[3].to_owned()) {
Ok(value) => value
Err(err) => return Err(err)
}
let passed = match fields[4] {
"pass" => true
"fail" => false
value => return Err(InvalidBaselineStatus(value.to_owned()))
}
let issue_codes = Array::new()
if fields[5] != "" {
let encoded_codes = fields[5].split(",").collect()
let mut i = 0
while i < encoded_codes.length() {
match baseline_unescape(encoded_codes[i].to_owned()) {
Ok(value) => issue_codes.push(value)
Err(err) => return Err(err)
}
i = i + 1
}
}
Ok({ name, expectation, actual, passed, issue_codes })
}
///|
fn baseline_parse_expectation(
value : String,
) -> Result[ConformanceExpectation, BaselineError] {
match value {
"accepted" => Ok(ExpectAccepted)
"rejected" => Ok(ExpectRejected)
"parse-error" => Ok(ExpectParseError)
_ => Err(InvalidBaselineExpectation(value))
}
}
///|
fn baseline_parse_actual(
value : String,
) -> Result[ConformanceActual, BaselineError] {
match value {
"accepted" => Ok(ActualAccepted)
"rejected" => Ok(ActualRejected)
"parse-error" => Ok(ActualParseError)
_ => Err(InvalidBaselineActual(value))
}
}
///|
fn baseline_entry_state(entry : ConformanceBaselineEntry) -> String {
"expected=" +
entry.expectation.label() +
",actual=" +
entry.actual.label() +
",status=" +
baseline_pass_label(entry.passed)
}
///|
fn baseline_result_state(result : ConformanceCaseResult) -> String {
"expected=" +
result.expectation.label() +
",actual=" +
result.actual.label() +
",status=" +
baseline_pass_label(result.passed)
}
///|
fn baseline_pass_label(passed : Bool) -> String {
if passed {
"pass"
} else {
"fail"
}
}
///|
fn baseline_find_entry(
entries : Array[ConformanceBaselineEntry],
name : String,
) -> ConformanceBaselineEntry? {
let mut i = 0
while i < entries.length() {
if entries[i].name == name {
return Some(entries[i])
}
i = i + 1
}
None
}
///|
fn baseline_find_result(
results : Array[ConformanceCaseResult],
name : String,
) -> ConformanceCaseResult? {
let mut i = 0
while i < results.length() {
if results[i].name == name {
return Some(results[i])
}
i = i + 1
}
None
}
///|
fn baseline_string_arrays_equal(a : Array[String], b : Array[String]) -> Bool {
if a.length() != b.length() {
return false
}
let mut i = 0
while i < a.length() {
if a[i] != b[i] {
return false
}
i = i + 1
}
true
}
///|
fn baseline_clone_strings(values : Array[String]) -> Array[String] {
let copy = Array::new()
let mut i = 0
while i < values.length() {
copy.push(values[i])
i = i + 1
}
copy
}
///|
fn baseline_escape(value : String) -> String {
let out = StringBuilder::new()
let mut i = 0
while i < value.length() {
let code = value.code_unit_at(i)
if code == 37 {
out.write_string("%25")
} else if code == 9 {
out.write_string("%09")
} else if code == 10 {
out.write_string("%0A")
} else if code == 13 {
out.write_string("%0D")
} else if code == 44 {
out.write_string("%2C")
} else {
out.write_char(code.unsafe_to_char())
}
i = i + 1
}
out.to_string()
}
///|
fn baseline_unescape(value : String) -> Result[String, BaselineError] {
let out = StringBuilder::new()
let mut i = 0
while i < value.length() {
let code = value.code_unit_at(i)
if code == 37 {
if i + 2 >= value.length() {
return Err(InvalidBaselineEscape(value))
}
let hi = baseline_hex_value(value.code_unit_at(i + 1))
let lo = baseline_hex_value(value.code_unit_at(i + 2))
if hi < 0 || lo < 0 {
return Err(InvalidBaselineEscape(value))
}
out.write_char((hi * 16 + lo).unsafe_to_char())
i = i + 3
} else {
out.write_char(code.unsafe_to_char())
i = i + 1
}
}
Ok(out.to_string())
}
///|
fn baseline_hex_value(code : UInt16) -> Int {
if code >= 48 && code <= 57 {
code.to_int() - 48
} else if code >= 65 && code <= 70 {
code.to_int() - 55
} else if code >= 97 && code <= 102 {
code.to_int() - 87
} else {
-1
}
}
///|
fn baseline_markdown_cell(value : String) -> String {
let out = StringBuilder::new()
let mut i = 0
while i < value.length() {
let code = value.code_unit_at(i)
if code == 124 {
out.write_string("\\|")
} else if code == 10 || code == 13 {
out.write_char(' ')
} else {
out.write_char(code.unsafe_to_char())
}
i = i + 1
}
out.to_string()
}