mooncov

Coverage report parsing, merging, filtering, gating, and conversion for MoonBit projects.

coverage
lcov
coveralls
ci
testing
moon add Xpeng/mooncov@0.1.1
Download zip
Author
Version
0.1.1
License
Apache-2.0
Last updated
8 days ago
Downloads
6

Dependencies

README

#mooncov

CI license

mooncov is a pure MoonBit coverage-report toolkit for libraries, command-line tools, and CI pipelines. It parses LCOV, Coveralls JSON, and Cobertura XML into one typed data model, merges reports from parallel jobs, normalizes and filters source paths, checks project or changed-line thresholds, and exports deterministic LCOV, Coveralls JSON, Cobertura XML, Markdown, or full-fidelity JSON.

The reusable library supports MoonBit's wasm, wasm-gc, js, and native backends. The optional file-oriented CLI is native-only because it reads local files.

#Why this exists

moon coverage report can produce useful Coveralls, Cobertura, HTML, terminal, and bisect outputs, but downstream systems often need to combine runs, enforce their own policy, or translate data without depending on a hosted service. mooncov keeps those workflow operations local and deterministic.

Typical uses include:

  • merging unit, integration, and backend-specific coverage;
  • converting MoonBit's Coveralls JSON into LCOV for another CI consumer;
  • excluding generated or vendored sources with portable globs;
  • failing CI when line, branch, or function coverage drops below a threshold;
  • checking only instrumented lines touched by a unified Git diff;
  • posting a compact Markdown table to a pull request;
  • retaining all three coverage dimensions in a stable JSON artifact.

#Feature matrix

CapabilityStatus
LCOV parse and deterministic writeSupported
Coveralls source_files JSON parse and writeSupported
Line, branch, and function modelSupported
Duplicate-safe multi-report mergeSupported
Path normalization and * / ? / ** filtersSupported
Overall and per-file summariesSupported
Line, branch, and function threshold gateSupported
Baseline comparison and regression allowancesSupported
Unified-diff changed-line coverageSupported
Markdown and full-fidelity JSON outputSupported
Cobertura XML parse and writeSupported

#Installation

Install the package from mooncakes.io:

moon add Xpeng/mooncov

Then import the root package in moon.pkg:

import { "Xpeng/mooncov", }

For local development, clone the repository and run:

moon check --target all --deny-warn moon test --target all --deny-warn

The project was developed with moon 0.1.20260703 and moonc v0.10.3+16975d007.

#Quick start: parse and summarize LCOV

///|
test "README: parse and summarize LCOV" {
let input =
#|TN:unit
#|SF:src/lib.mbt
#|FN:1,run
#|FNDA:2,run
#|DA:1,2
#|DA:2,0
#|BRDA:2,0,0,1
#|BRDA:2,0,1,0
#|end_of_record
#|
let report = @mooncov.parse_lcov(input)
let summary = @mooncov.summarize(report)
inspect(summary.lines.covered, content="1")
inspect(summary.lines.total, content="2")
inspect(summary.branches.covered, content="1")
inspect(summary.functions.covered, content="1")
}

parse_lcov accepts TN, SF, FN, FNDA, DA, BRDA, VER, the six standard aggregate tags, and end_of_record. Aggregate totals are recalculated from entries instead of trusted. A missing final end_of_record is accepted; malformed entries raise CoverageError::InvalidLcov with the physical input line.

#Merge reports and enforce policy

///|
test "README: merge and gate reports" {
let first = @mooncov.parse_lcov(
"SF:src/lib.mbt\nDA:1,1\nDA:2,0\nend_of_record\n",
)
let second = @mooncov.parse_lcov(
"SF:src/lib.mbt\nDA:1,2\nDA:2,1\nend_of_record\n",
)
let merged = @mooncov.merge_reports([first, second])
inspect(merged.files[0].lines[0].hits, content="3")
inspect(merged.files[0].lines[1].hits, content="1")
let gate = @mooncov.check_thresholds(
merged,
@mooncov.CoverageThresholds::new(lines=100.0),
)
assert_true(gate.passed)
}

Entries merge by these identities:

  • file: normalized source path;
  • line: source line number;
  • branch: line, block identifier, and branch identifier;
  • function: function name and optional declaration line.

Counts are added. If one branch count is unknown (-/null) and another is known, the known count is retained; two unknown counts remain unknown.

Threshold values are percentages from 0 through 100, inclusive. Equality passes. A dimension with no instrumented entries evaluates to 100%, so a line-only producer does not fail a branch gate merely because it cannot report branches.

#Compare against a baseline

Coverage deltas are percentage points, and regression allowances are inclusive:

///|
test "README: baseline regression policy" {
let baseline = @mooncov.parse_lcov(
"SF:src/a.mbt\nDA:1,1\nDA:2,1\nend_of_record\n",
)
let current = @mooncov.parse_lcov(
"SF:src/a.mbt\nDA:1,1\nDA:2,0\nend_of_record\n",
)
let comparison = @mooncov.compare_reports(baseline, current)
inspect(comparison.lines.percentage_delta, content="-50")
let result = @mooncov.check_regressions(
baseline,
current,
@mooncov.RegressionLimits::new(lines=50.0),
)
assert_true(result.passed)
}

CoverageComparison::files distinguishes added, removed, modified, and unchanged paths. comparison_to_markdown renders the overall and per-file changes for a pull request, while regression_message produces a short CI log result.

#Path normalization and filtering

///|
test "README: normalize and filter paths" {
let report = @mooncov.parse_lcov(
"SF:D:\\repo\\src\\lib.mbt\nDA:1,1\nend_of_record\n" +
"SF:D:\\repo\\generated\\api.mbt\nDA:1,1\nend_of_record\n",
strip_prefix="D:/repo",
)
let selected = @mooncov.select_paths(report, ["src/**/*.mbt"], [
"**/generated/**",
])
inspect(selected.files.length(), content="1")
inspect(selected.files[0].path, content="src/lib.mbt")
}

Normalization converts \ to /, removes repeated separators and . segments, and resolves .. without escaping an absolute root. Matching is case-sensitive and independent of the host operating system:

  • * matches zero or more characters inside one segment;
  • ? matches one non-separator character;
  • ** can cross path separators;
  • excludes are applied after includes and always win;
  • an empty include list accepts all paths.

#Changed-line coverage

///|
test "README: changed-line gate" {
let report = @mooncov.parse_lcov(
"SF:src/lib.mbt\nDA:10,1\nDA:11,0\nend_of_record\n",
)
let diff =
#|--- a/src/lib.mbt
#|+++ b/src/lib.mbt
#|@@ -9,0 +10,2 @@
#|+covered
#|+missed
#|
let ranges = @mooncov.parse_unified_diff(diff)
let result = @mooncov.check_diff_threshold(report, ranges, 50.0)
assert_true(result.passed)
inspect(result.summary.lines.total, content="2")
}

Diff coverage counts only changed lines that the coverage producer instrumented. A unified diff alone cannot tell executable lines from comments or whitespace, so absent line records are not guessed as misses. Paths with no file-level coverage record are returned in DiffSummary::unmatched_files for a caller to handle explicitly.

#Coveralls JSON

The parser accepts the standard source_files array, sparse coverage arrays, and flattened branch arrays. null line slots mean “not instrumented” and do not become misses.

///|
test "README: Coveralls to LCOV" {
let coveralls = "{\"source_files\":[{\"name\":\"src/a.mbt\"," +
"\"coverage\":[1,0,null],\"branches\":[2,0,0,0]}]}"
let report = @mooncov.parse_coveralls(coveralls)
let lcov = @mooncov.to_lcov(report)
assert_true(lcov.contains("DA:1,1"))
assert_true(lcov.contains("DA:2,0"))
assert_true(lcov.contains("BRDA:2,0,0,0"))
}

Coveralls has no standard function-coverage field. Therefore to_coveralls_json intentionally serializes lines and branches only. Use to_json_report when a full-fidelity mooncov artifact is required.

MoonBit 0.1.20260703 on Windows can emit bare \ path separators inside source_files[].name, which is not valid JSON. parse_coveralls recognizes that producer-specific form only after standard JSON decoding fails, doubles otherwise-invalid escapes in source_files[].name, and retries strict decoding once. Valid escapes, including \\, \t, \uXXXX, quotes, and slashes, keep their standard JSON meaning.

#Cobertura XML

MoonBit's moon coverage report -f cobertura output can be consumed directly:

///|
test "README: Cobertura interoperability" {
let xml =
#|<coverage><packages><package><classes>
#| <class filename="src/a.mbt">
#| <lines>
#| <line number="1" hits="2"/>
#| <line number="2" hits="0" branch="true"
#| condition-coverage="50% (1/2)"/>
#| </lines>
#| </class>
#|</classes></package></packages></coverage>
let report = @mooncov.parse_cobertura(xml)
inspect(@mooncov.summarize(report).branches.total, content="2")
let encoded = @mooncov.to_cobertura(report)
assert_true(encoded.contains("branches-valid=\"2\""))
}

Cobertura stores aggregate condition coverage, not LCOV's block and branch identities. The parser therefore creates stable synthetic branch identities; line and branch totals survive a mooncov encode/decode round trip, while the original branch IDs cannot.

#Output APIs

///|
let lcov : String = @mooncov.to_lcov(report)

///|
let coveralls : String = @mooncov.to_coveralls_json(
report,
service_name="github-actions",
service_job_id="build-42",
)

///|
let cobertura : String = @mooncov.to_cobertura(report)

///|
let markdown : String = @mooncov.to_markdown(report)

///|
let json : String = @mooncov.to_json_report(report)

All serializers canonicalize first: normalized files and their entries are deduplicated and sorted before output. Reordering equivalent input records therefore does not change LCOV, Coveralls JSON, Cobertura XML, Markdown, or unified JSON artifacts.

#Command-line example

The CLI is deliberately a thin adapter over the public library:

# Built-in demo; no input file required moon run cmd/main # Reproduce the repository sample moon run cmd/main -- summary lcov examples/sample.info moon run cmd/main -- validate lcov examples/sample.info moon run cmd/main -- summary cobertura examples/sample.xml # Convert any accepted format to canonical LCOV or full JSON moon run cmd/main -- normalize coveralls path/to/coverage.json moon run cmd/main -- json lcov path/to/lcov.info # Exit with code 2 if line coverage is below 80% moon run cmd/main -- gate lcov path/to/lcov.info 80 # Merge two or more inputs of the same format and print LCOV moon run cmd/main -- merge lcov unit.info integration.info # Compare two reports or allow at most a 2-point line-coverage drop moon run cmd/main -- compare lcov baseline.info current.info moon run cmd/main -- regress lcov baseline.info current.info 2

The root module prefers the native backend so moon run cmd/main works without an extra flag. This preference does not restrict the root library: CI still checks and tests it on every stable backend.

#Public API overview

  • Model: CoverageReport, FileCoverage, CoverageLine, CoverageBranch, CoverageFunction;
  • Formats: parse_lcov, to_lcov, parse_coveralls, to_coveralls_json, parse_cobertura, to_cobertura;
  • Composition: merge_reports, canonicalize_report;
  • Paths: normalize_path, glob_match, PathFilter, filter_report, select_paths;
  • Statistics: summarize, summarize_file, summarize_files;
  • Gates: CoverageThresholds, check_thresholds, gate_message;
  • Comparison: compare_reports, RegressionLimits, check_regressions, comparison_to_markdown, regression_message;
  • Diff: parse_unified_diff, summarize_diff, check_diff_threshold;
  • Reports: to_markdown, to_json_report.

The generated pkg.generated.mbti is the authoritative compact interface listing for release 0.1.0.

#Reproducible verification

Run from the repository root:

moon fmt --check moon info --target all moon check --target all --deny-warn --warn-list +73 moon test --target all --deny-warn moon build --target all moon run cmd/main moon run cmd/main -- summary lcov examples/sample.info moon run cmd/main -- summary cobertura examples/sample.xml moon package git diff --exit-code

CI executes the same essential format, interface, check, build, test, example, and package gates.

#Scope and compatibility notes

  • LCOV extension tags other than VER are rejected rather than silently discarded; standard aggregate tags are accepted.
  • LCOV function names may contain commas because only the first comma separates their numeric field.
  • Coveralls numeric coverage values must be finite, integral, and non-negative.
  • Current MoonBit Windows Coveralls output with bare path separators is accepted through a strict-first, invalid-escape-only source_files[].name repair.
  • Cobertura XML supports class lines, method lines, standard XML attribute entities, and condition-coverage; DTD validation is intentionally out of scope.
  • Path matching preserves case; callers targeting case-insensitive filesystems may normalize case before constructing reports.
  • Unified-diff parsing handles standard Git destination headers, optional tab timestamps, destination ranges with omitted counts, new files, and deleted files. Git C-style escape sequences inside quoted path headers are not decoded in 0.1.0.
  • Hit-count addition uses MoonBit Int; inputs should stay within that type's range.

See docs/format-semantics.md for detailed format decisions and docs/recipes.md for CI-oriented recipes.

#Project origin and open-source compliance

This project is an original MoonBit implementation. It uses public format documentation as behavioral references:

No implementation source was copied from LCOV, Coveralls, Codecov, or another coverage library. Repository fixtures are small original examples written for this project. The only runtime dependency is moonbitlang/x for native CLI file and process access; the library code uses moonbitlang/core.

As part of the ecosystem survey, this project also reviewed PingGuoMiaoMiao/mooncove. That package focuses on MoonBit bisect data and HTML/CSV reports. mooncov instead focuses on LCOV text parsing and serialization, multi-report merging, path filtering, coverage statistics, multi-format reports, and a CI-oriented CLI. This comparison only describes the different scopes; mooncov does not copy mooncove implementation code or claim to be the first MoonBit coverage tool.

#Contributing

Issues and pull requests are welcome. Read CONTRIBUTING.md for the validation commands and format-fixture rules. Security issues should be reported according to SECURITY.md.

#License

Copyright 2026 mooncov contributors.

Licensed under the Apache License 2.0.

#
CoverageError

pub(all) suberror CoverageError {
InvalidLineNumber(Int)
InvalidHitCount(Int)
InvalidLcov(Int, String)
InvalidCoveralls(String)
InvalidCobertura(Int, String)
InvalidModel(String)
InvalidThreshold(String, Double)
InvalidDiff(Int, String)
} derive(Eq,
Debug
)

Errors raised while decoding or validating coverage data.

#
ChangedRange

pub(all) struct ChangedRange {
path : String
start_line : Int
end_line : Int
} derive(Eq,
Debug
)

An inclusive new-file line range extracted from a unified diff hunk.

#
CoverageBranch

pub(all) struct CoverageBranch {
line : Int
block : String
branch : String
taken : Int?
} derive(Eq,
Debug
)

A branch arm reported by a coverage producer.

taken = None represents LCOV's - value, meaning that the producer instrumented the branch but did not report a numeric execution count.

#
CoverageBranch::is_covered

fn CoverageBranch::is_covered(self : CoverageBranch) -> Bool

Return whether a branch has a positive execution count.

#
CoverageComparison

pub(all) struct CoverageComparison {
baseline : CoverageSummary
current : CoverageSummary
lines : MetricChange
branches : MetricChange
functions : MetricChange
files : Array[FileComparison]
} derive(
Debug
)

Report-wide metric changes and path-level comparisons.

#
CoverageCount

pub(all) struct CoverageCount {
covered : Int
total : Int
} derive(Eq,
Debug
)

A covered-item count and its instrumented total.

#
CoverageCount::percentage

fn CoverageCount::percentage(self : CoverageCount) -> Double

Return the percentage in the inclusive range 0..100.

A metric with no instrumented items is defined as 100%, matching the practical gate semantics that an absent metric cannot fail a build.

#
CoverageFunction

pub(all) struct CoverageFunction {
name : String
line : Int?
hits : Int
} derive(Eq,
Debug
)

A function entry from a coverage report.

Some formats do not carry a declaration line, so line is optional.

#
CoverageFunction::is_covered

fn CoverageFunction::is_covered(self : CoverageFunction) -> Bool

Return whether a function has a positive execution count.

#
CoverageLine

pub(all) struct CoverageLine {
line : Int
hits : Int
} derive(Eq,
Debug
)

A covered or uncovered source line.

#
CoverageLine::is_covered

fn CoverageLine::is_covered(self : CoverageLine) -> Bool

Return whether a line or count pair represents executed code.

#
CoverageMetric

pub(all) enum CoverageMetric {
Lines
Branches
Functions
} derive(Eq,
Debug
)

Coverage dimensions supported by a threshold gate.

#
CoverageReport

pub(all) struct CoverageReport {
files : Array[FileCoverage]
} derive(Eq,
Debug
)

A format-neutral coverage report.

#
CoverageReport::add_file

fn CoverageReport::add_file(self : CoverageReport, file : FileCoverage) -> Unit

Add a file to a report.

#
CoverageReport::find_file

fn CoverageReport::find_file(self : CoverageReport, path : StringView) -> FileCoverage?

Return the file with the exact normalized path, if present.

#
CoverageReport::new

Create an empty coverage report.

#
CoverageSummary

pub(all) struct CoverageSummary {
lines : CoverageCount
branches : CoverageCount
functions : CoverageCount
} derive(Eq,
Debug
)

Line, branch, and function totals for a report or source file.

#
CoverageThresholds

pub(all) struct CoverageThresholds {
lines : Double?
branches : Double?
functions : Double?
} derive(
Debug
)

Optional minimum percentages for each coverage dimension.

#
CoverageThresholds::new

fn CoverageThresholds::new(lines? : Double, branches? : Double, functions? : Double) -> CoverageThresholds

Construct optional percentage thresholds.

#
DiffGateResult

pub(all) struct DiffGateResult {
passed : Bool
summary : DiffSummary
actual : Double
required : Double
} derive(
Debug
)

The result of a changed-line coverage gate.

#
DiffSummary

pub(all) struct DiffSummary {
lines : CoverageCount
changed_ranges : Int
matched_files : Int
unmatched_files : Array[String]
} derive(Eq,
Debug
)

Instrumented line coverage selected by a collection of changed ranges.

#
FileChangeKind

pub(all) enum FileChangeKind {
Added
Removed
Modified
Unchanged
} derive(Eq,
Debug
)

How a source path changed between two reports.

#
FileComparison

pub(all) struct FileComparison {
path : String
kind : FileChangeKind
before : CoverageSummary
after : CoverageSummary
} derive(Eq,
Debug
)

A per-file summary comparison.

#
FileCoverage

pub(all) struct FileCoverage {
path : String
test_name : String?
lines : Array[CoverageLine]
branches : Array[CoverageBranch]
functions : Array[CoverageFunction]
} derive(Eq,
Debug
)

Coverage data for one normalized source path.

#
FileCoverage::add_branch

fn FileCoverage::add_branch(self : FileCoverage, line : Int, block : String, branch : String, taken : Int?) -> Unit raise CoverageError

Add branch coverage after validating its source line and optional count.

#
FileCoverage::add_function

fn FileCoverage::add_function(self : FileCoverage, name : String, hits : Int, line? : Int) -> Unit raise CoverageError

Add function coverage after validating the optional declaration line and hit count.

#
FileCoverage::add_line

fn FileCoverage::add_line(self : FileCoverage, line : Int, hits : Int) -> Unit raise CoverageError

Add line coverage after validating the line number and hit count.

#
FileCoverage::new

fn FileCoverage::new(path : String, test_name? : String) -> FileCoverage

Create an empty file report.

Example

test {
let file = FileCoverage::new("src/lib.mbt")
inspect(file.path, content="src/lib.mbt")
inspect(file.lines.length(), content="0")
}

#
FileSummary

pub(all) struct FileSummary {
path : String
summary : CoverageSummary
} derive(Eq,
Debug
)

A summary associated with one source path.

#
GateResult

pub(all) struct GateResult {
passed : Bool
summary : CoverageSummary
violations : Array[ThresholdViolation]
} derive(
Debug
)

The complete result of evaluating thresholds.

#
MetricChange

pub(all) struct MetricChange {
before : CoverageCount
after : CoverageCount
percentage_delta : Double
} derive(
Debug
)

Coverage before and after a change, plus the percentage-point delta.

#
PathFilter

pub(all) struct PathFilter {
includes : Array[String]
excludes : Array[String]
} derive(Eq,
Debug
)

Include/exclude rules for normalized source paths.

An empty include list accepts every path. Excludes are evaluated after includes and always win.

#
PathFilter::all

fn PathFilter::all() -> PathFilter

Construct a filter that accepts every path.

#
PathFilter::allows

fn PathFilter::allows(self : PathFilter, path : String) -> Bool

Test whether a source path passes this filter.

#
PathFilter::new

fn PathFilter::new(includes : Array[String], excludes : Array[String]) -> PathFilter

Construct a reusable path filter.

#
RegressionLimits

pub(all) struct RegressionLimits {
lines : Double?
branches : Double?
functions : Double?
} derive(
Debug
)

Allowed percentage-point drops for regression checks.

#
RegressionLimits::new

fn RegressionLimits::new(lines? : Double, branches? : Double, functions? : Double) -> RegressionLimits

Construct optional maximum percentage-point drops.

#
RegressionResult

pub(all) struct RegressionResult {
passed : Bool
comparison : CoverageComparison
violations : Array[RegressionViolation]
} derive(
Debug
)

Result of comparing current coverage to a baseline policy.

#
RegressionViolation

pub(all) struct RegressionViolation {
metric : CoverageMetric
percentage_delta : Double
allowed_drop : Double
} derive(
Debug
)

One metric whose drop exceeded its allowance.

#
ThresholdViolation

pub(all) struct ThresholdViolation {
metric : CoverageMetric
actual : Double
required : Double
} derive(
Debug
)

One failed coverage threshold.

#
canonicalize_report

fn canonicalize_report(report : CoverageReport, strip_prefix? : String) -> CoverageReport raise CoverageError

Deduplicate and deterministically sort one report.

#
check_diff_threshold

fn check_diff_threshold(report : CoverageReport, ranges : ArrayView[ChangedRange], minimum : Double) -> DiffGateResult raise CoverageError

Enforce a minimum percentage on instrumented changed lines.

#
check_regressions

fn check_regressions(baseline : CoverageReport, current : CoverageReport, limits : RegressionLimits) -> RegressionResult raise CoverageError

Fail when a coverage percentage drops by more than an allowed number of percentage points.

Equality passes: a 5-point drop satisfies an allowance of 5 points.

#
check_thresholds

fn check_thresholds(report : CoverageReport, thresholds : CoverageThresholds) -> GateResult raise CoverageError

Evaluate report-wide minimum coverage percentages.

Equality passes: a 75% result satisfies a 75% threshold. Metrics without instrumented items evaluate to 100%; see CoverageCount::percentage.

#
compare_reports

fn compare_reports(baseline_report : CoverageReport, current_report : CoverageReport) -> CoverageComparison raise CoverageError

Compare a current report to a baseline after canonicalization.

Deltas are percentage points, so moving from 75% to 80% is +5, not a relative 6.67% increase. File comparisons include added and removed paths.

#
comparison_to_markdown

fn comparison_to_markdown(comparison : CoverageComparison, title? : String, include_files? : Bool) -> String

Render an overall and optional per-file baseline comparison as Markdown.

#
filter_report

fn filter_report(report : CoverageReport, filter : PathFilter) -> CoverageReport

Select files from a report without mutating or aliasing its arrays.

#
gate_message

fn gate_message(result : GateResult) -> String

Produce a concise human-readable threshold result for CI logs.

#
glob_match

fn glob_match(pattern : String, path : String) -> Bool

Match a normalized path against a small, portable glob dialect.

* matches within one segment, ? matches one non-separator character, and ** can cross separators. Backslashes in both inputs are normalized.

#
merge_reports

fn merge_reports(reports : ArrayView[CoverageReport], strip_prefix? : String) -> CoverageReport raise CoverageError

Merge any number of coverage reports.

Files are matched by normalized path. Duplicate line, branch, and function entries have their hit counts added. If only one side knows a branch count, the known count wins; two unknown counts stay unknown. Conflicting test names are cleared rather than reporting a misleading single producer.

#
normalize_path

fn normalize_path(path : String, strip_prefix? : String) -> String

Normalize separators and dot segments in a coverage source path.

If strip_prefix is supplied, it is normalized first and removed only at a complete path-segment boundary. The function intentionally preserves letter case because case sensitivity belongs to the caller's filesystem.

Example

test {
inspect(
normalize_path(
"D:\\work\\pkg\\.\\src\\..\\src\\lib.mbt",
strip_prefix="D:/work/pkg",
),
content="src/lib.mbt",
)
}

#
parse_cobertura

fn parse_cobertura(input : String, strip_prefix? : String) -> CoverageReport raise CoverageError

Parse Cobertura XML coverage into mooncov's unified model.

Class-level <line> 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 <method> sections become function entries using the first method line and maximum line hit count.

#
parse_coveralls

fn parse_coveralls(input : String, strip_prefix? : String) -> CoverageReport raise CoverageError

Parse Coveralls JSON produced by moon coverage report -f coveralls.

The standard source_files[].coverage array and flattened branches array are supported. null coverage slots are preserved as non-instrumented lines rather than counted as misses. If strict decoding fails, invalid bare Windows separators emitted by MoonBit 0.1.20260703 are repaired only in source_files[].name before one strict retry.

#
parse_lcov

fn parse_lcov(input : String, strip_prefix? : String) -> CoverageReport raise CoverageError

Parse an LCOV tracefile into the format-neutral coverage model.

Aggregate summary tags (LF, LH, FNF, FNH, BRF, and BRH) are accepted but recalculated by mooncov instead of trusted. A final end_of_record is recommended but not required.

Example

test {
let report = parse_lcov(
"TN:unit\nSF:src/lib.mbt\nDA:1,3\nDA:2,0\nend_of_record\n",
)
inspect(report.files.length(), content="1")
inspect(report.files[0].lines[0].hits, content="3")
}

#
parse_unified_diff

fn parse_unified_diff(input : String, strip_prefix? : String) -> Array[ChangedRange] raise CoverageError

Parse new-file ranges from standard unified diff text.

+++ b/path headers select the destination path. Deleted files (+++ /dev/null) and deletion-only hunks (+start,0) produce no ranges. Git's conventional a/ and b/ prefixes are removed automatically.

#
regression_message

fn regression_message(result : RegressionResult) -> String

Produce a concise regression result for CI logs.

#
select_paths

fn select_paths(report : CoverageReport, includes : Array[String], excludes : Array[String]) -> CoverageReport

Convenience wrapper for one-off include and exclude lists.

#
summarize

fn summarize(report : CoverageReport) -> CoverageSummary raise CoverageError

Summarize a whole report after coalescing duplicate paths and entries.

#
summarize_diff

fn summarize_diff(report : CoverageReport, ranges : ArrayView[ChangedRange]) -> DiffSummary raise CoverageError

Calculate changed-line coverage for instrumented lines.

Changed lines absent from the coverage report are not treated as misses, because executable and non-executable source cannot be distinguished from a unified diff alone. Their paths are exposed in unmatched_files when no file-level coverage record exists at all.

#
summarize_file

fn summarize_file(file : FileCoverage) -> CoverageSummary raise CoverageError

Summarize one source file after coalescing duplicate coverage identities.

#
summarize_files

fn summarize_files(report : CoverageReport) -> Array[FileSummary] raise CoverageError

Produce deterministic per-file summaries sorted by normalized path.

#
to_cobertura

fn to_cobertura(report : CoverageReport, source_root? : String) -> String raise CoverageError

Encode a report as deterministic Cobertura XML.

Cobertura carries aggregate branch conditions rather than LCOV branch identities. Mooncov writes each branch as a condition and guarantees that line and branch summary counts survive a mooncov encode/decode round trip.

#
to_coveralls_json

fn to_coveralls_json(report : CoverageReport, service_name? : String, service_job_id? : String) -> String raise CoverageError

Encode the line and branch portions of a report as Coveralls JSON.

Coveralls' source-file schema has no standard function-coverage field, so function entries are intentionally not serialized. Numeric-looking branch identifiers stay numeric; other identifiers are emitted as strings and are accepted by mooncov's decoder.

#
to_json_report

fn to_json_report(report : CoverageReport, indent? : Int) -> String raise CoverageError

Encode the complete unified model and calculated statistics as JSON.

Unlike Coveralls JSON, this mooncov-specific format retains function coverage and test names.

#
to_lcov

fn to_lcov(report : CoverageReport) -> String raise CoverageError

Encode a coverage report as deterministic LCOV text.

Files retain report order while functions, lines, and branches are sorted within each record. Summary fields are calculated from the actual entries.

#
to_markdown

fn to_markdown(report : CoverageReport, title? : String, include_files? : Bool) -> String raise CoverageError

Render report-wide and optional per-file statistics as a Markdown table.