///|
priv struct CoberturaMethod {
name : String
mut line : Int?
mut hits : Int
}
///|
fn cobertura_required_attribute(
tag : XmlTag,
name : String,
) -> String raise CoverageError {
match tag.attributes.get(name) {
Some(value) => value
None =>
raise InvalidCobertura(
tag.input_line,
"<\{tag.name}> is missing attribute \{name}",
)
}
}
///|
fn cobertura_int_text(
text : String,
input_line : Int,
field : String,
) -> Int raise CoverageError {
@strconv.from_str(text) catch {
_ => raise InvalidCobertura(input_line, "\{field} must be a MoonBit Int")
}
}
///|
fn cobertura_integer_attribute(
tag : XmlTag,
name : String,
) -> Int raise CoverageError {
cobertura_int_text(
cobertura_required_attribute(tag, name),
tag.input_line,
"\{tag.name}.\{name}",
)
}
///|
fn parse_condition_coverage(
value : String,
input_line : Int,
) -> (Int, Int) raise CoverageError {
guard value.split_once("(") is Some((_, after_open)) else {
raise InvalidCobertura(
input_line, "condition-coverage must contain (covered/total)",
)
}
guard after_open.split_once(")") is Some((counts, _)) else {
raise InvalidCobertura(
input_line, "condition-coverage is missing closing parenthesis",
)
}
guard counts.split_once("/") is Some((covered_view, total_view)) else {
raise InvalidCobertura(
input_line, "condition-coverage must contain covered/total",
)
}
let covered_text = "\{covered_view.trim()}"
let total_text = "\{total_view.trim()}"
let covered = cobertura_int_text(
covered_text, input_line, "condition covered count",
)
let total = cobertura_int_text(
total_text, input_line, "condition total count",
)
if covered < 0 || total < 0 || covered > total {
raise InvalidCobertura(
input_line, "condition counts must satisfy 0 <= covered <= total",
)
}
(covered, total)
}
///|
fn add_cobertura_branches(
file : FileCoverage,
tag : XmlTag,
line : Int,
) -> Unit raise CoverageError {
match tag.attributes.get("branch") {
Some("true") =>
match tag.attributes.get("condition-coverage") {
Some(value) => {
let (covered, total) = parse_condition_coverage(value, tag.input_line)
for branch in 0..
raise InvalidCobertura(
tag.input_line,
"branch line is missing condition-coverage",
)
}
Some("false") | None => ()
Some(value) =>
raise InvalidCobertura(
tag.input_line,
"line.branch must be true or false, got \{value}",
)
}
}
///|
fn update_cobertura_method(
method_entry : CoberturaMethod,
line : Int,
hits : Int,
) -> Unit {
match method_entry.line {
Some(previous) => if line < previous { method_entry.line = Some(line) }
None => method_entry.line = Some(line)
}
if hits > method_entry.hits {
method_entry.hits = hits
}
}
///|
fn finish_cobertura_method(
file : FileCoverage,
method_entry : CoberturaMethod,
) -> Unit {
file.functions.push({
name: method_entry.name,
line: method_entry.line,
hits: method_entry.hits,
})
}
///|
/// Parse Cobertura XML coverage into mooncov's unified model.
///
/// Class-level `` elements preserve line hits. Standard
/// `condition-coverage="P% (covered/total)"` attributes are represented as
/// synthetic branch identities because Cobertura does not expose LCOV block
/// and branch identifiers. Optional `` sections become function
/// entries using the first method line and maximum line hit count.
pub fn parse_cobertura(
input : String,
strip_prefix? : String = "",
) -> CoverageReport raise CoverageError {
let report = CoverageReport::new()
let mut current_file : FileCoverage? = None
let mut current_method : CoberturaMethod? = None
for tag in scan_xml_tags(input) {
match (tag.closing, tag.name) {
(false, "class") => {
if current_file is Some(_) {
raise InvalidCobertura(tag.input_line, "nested is invalid")
}
let filename = cobertura_required_attribute(tag, "filename")
current_file = Some(
FileCoverage::new(normalize_path(filename, strip_prefix~)),
)
if tag.self_closing {
guard current_file is Some(file) else {
raise InvalidCobertura(tag.input_line, "internal class state error")
}
report.files.push(file)
current_file = None
}
}
(true, "class") => {
if current_method is Some(_) {
raise InvalidCobertura(tag.input_line, "class closed before method")
}
guard current_file is Some(file) else {
raise InvalidCobertura(
tag.input_line,
"closing class has no open class",
)
}
report.files.push(file)
current_file = None
}
(false, "method") => {
guard current_file is Some(_) else {
raise InvalidCobertura(
tag.input_line,
"method appears outside a class",
)
}
if current_method is Some(_) {
raise InvalidCobertura(tag.input_line, "nested method is invalid")
}
current_method = Some({
name: cobertura_required_attribute(tag, "name"),
line: None,
hits: 0,
})
if tag.self_closing {
guard (current_file, current_method)
is (Some(file), Some(method_entry)) else {
raise InvalidCobertura(
tag.input_line,
"internal method state error",
)
}
finish_cobertura_method(file, method_entry)
current_method = None
}
}
(true, "method") => {
guard (current_file, current_method) is (Some(file), Some(method_entry)) else {
raise InvalidCobertura(
tag.input_line,
"closing method has no open method",
)
}
finish_cobertura_method(file, method_entry)
current_method = None
}
(false, "line") => {
guard current_file is Some(file) else {
raise InvalidCobertura(tag.input_line, "line appears outside a class")
}
let line = cobertura_integer_attribute(tag, "number")
let hits = cobertura_integer_attribute(tag, "hits")
if line <= 0 {
raise InvalidCobertura(tag.input_line, "line.number must be positive")
}
if hits < 0 {
raise InvalidCobertura(
tag.input_line,
"line.hits must not be negative",
)
}
match current_method {
Some(method_entry) =>
update_cobertura_method(method_entry, line, hits)
None => {
file.lines.push({ line, hits })
add_cobertura_branches(file, tag, line)
}
}
}
_ => ()
}
}
if current_method is Some(_) {
raise InvalidCobertura(1, "document ended inside a method")
}
if current_file is Some(_) {
raise InvalidCobertura(1, "document ended inside a class")
}
canonicalize_report(report)
}