junit_delta

    JUnit XML creation, editing, merging and comparison in MoonBit

    junit
    xml
    reports
    testing
    Download zip
    Version
    0.4.0
    License
    MIT AND Apache-2.0
    Last updated
    3 hours ago
    Downloads
    38

    #junit-delta

    MoonBit JUnit XML 创建、读取、编辑、写回与比较库。支持 base/xunit2 报告模型,以及 wasm、wasm-gc、JavaScript 和 native。

    #功能

    • 创建和修改套件、用例、结果、properties、日志及自定义 XML 扩展。
    • 可变共享视图、深拷贝、惰性统计、套件归并和 xUnit2 重试结果。
    • 文件/字节/流回调、可替换解析器、普通及 pretty 写回。
    • CLI merge、verify、--glob,保留原有比较、重试归并和 CI 门禁。

    通用接口适配 junitparser 5.0.3;对标与边界说明语言、解析器和资源差异。原有不可变 XML 与比较接口继续可用。

    #安装与使用

    moon add tc-adventureking/junit_delta@0.4.0

    应用包的 moon.pkg:

    import { "tc-adventureking/junit_delta/junit", }
    pkgtype(kind: "executable")

    将以下内容保存为 main.mbt,运行 moon run . --target js:

    fn main raise {
    let suite = @junit.TestSuite::new(name=Some("service"))
    suite.add_case(@junit.TestCase::new(name=Some("login"), time=Some(0.12)))
    suite.add_property("build", "12")
    let report = @junit.JUnitXml::new()
    report.add_suite(suite)
    report.update_statistics()
    println(report.element().write_xml(pretty=true))
    }

    #许可

    原创实现使用 MIT,适配文件标注 Apache-2.0,见第三方声明。项目使用 AI 辅助设计与实现。

    InputError

    pub(all) suberror InputError {
    InvalidInput(String)
    Rejected(String, String)
    UnknownField(String)
    } derive(Eq)

    impl Show for InputError

    InputError::equal

    fn InputError::equal(InputError, InputError) -> Bool

    InputError::not_equal

    fn InputError::not_equal(x : InputError, y : InputError) -> Bool

    InputError::output

    fn InputError::output(self : InputError, logger : &Logger) -> Unit

    InputError::to_string

    fn InputError::to_string(self : InputError) -> String

    CliResult

    pub struct CliResult {
    stdout : String
    stderr : String
    status : Int
    }

    Diagnostic

    pub struct Diagnostic {
    source : String
    code : String
    message : String
    } derive(Eq)

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    DiffConfig

    pub struct DiffConfig {
    slowdown_pct : Int
    slowdown_floor_us : Int64
    fail_on : ReadOnlyArray[String]
    }

    DiffEntry

    pub struct DiffEntry {
    id : TestId
    category : String
    before : TestCase?
    after : TestCase?
    baseline_time_us : Int64?
    slowdown : Bool
    known_flaky : Bool
    // private fields
    }

    DiffResult

    pub struct DiffResult {
    entries : ReadOnlyArray[DiffEntry]
    diagnostics : ReadOnlyArray[Diagnostic]
    violations : ReadOnlyArray[String]
    baseline_count : Int
    candidate_count : Int
    history_count : Int
    // private fields
    }

    DiffResult::exit_code

    fn DiffResult::exit_code(self : DiffResult) -> Int

    DiffResult::to_json

    fn DiffResult::to_json(self : DiffResult, max_units? : Int64) ->
    Json
    raise InputError

    DiffResult::to_json_string

    fn DiffResult::to_json_string(self : DiffResult, max_units? : Int64) -> String raise InputError

    DiffResult::to_markdown

    fn DiffResult::to_markdown(self : DiffResult, limit? : Int, max_units? : Int64) -> String raise InputError

    DiffResult::to_text

    fn DiffResult::to_text(self : DiffResult, max_units? : Int64) -> String raise InputError

    DuplicatePolicy

    pub(all) enum DuplicatePolicy {
    Reject
    Attempts
    } derive(Eq)

    DuplicatePolicy::equal

    DuplicatePolicy::not_equal

    fn DuplicatePolicy::not_equal(x : DuplicatePolicy, y : DuplicatePolicy) -> Bool

    JUnitDocument

    pub struct JUnitDocument {
    root : JUnitElement
    before : ReadOnlyArray[JUnitNode]
    after : ReadOnlyArray[JUnitNode]
    declaration : (String, String?, String?)?
    }

    Full report tree, separate from the deliberately lossy comparison Report.

    JUnitDocument::from_xml

    fn JUnitDocument::from_xml(text : String) -> JUnitDocument raise InputError

    Read a complete XML tree. No DTD, entity loading, file or network access.

    JUnitDocument::map_cases

    fn JUnitDocument::map_cases(self : JUnitDocument, transform : (JUnitElement) -> JUnitElement? raise) -> JUnitDocument raise

    Map structural cases in source order. None removes a case. Extension subtrees are opaque; cases directly inside the root testsuites are supported.

    JUnitDocument::map_suites

    fn JUnitDocument::map_suites(self : JUnitDocument, transform : (JUnitElement) -> JUnitElement? raise) -> JUnitDocument raise

    Apply a transformation to top-level suites only. A bare suite is wrapped in testsuites when removed. Nested suites remain editable via with_content.

    JUnitDocument::new

    JUnitDocument::to_report

    fn JUnitDocument::to_report(self : JUnitDocument, source? : String) -> Report raise InputError

    Comparison projection deliberately retains the existing strict 0.2 profile.

    JUnitDocument::to_xml

    fn JUnitDocument::to_xml(self : JUnitDocument, max_units? : Int64) -> String raise InputError

    Preserve authored statistics; call with_statistics explicitly after editing. Namespace and XML validation complete before returning any output.

    JUnitDocument::with_root

    fn JUnitDocument::with_root(self : JUnitDocument, root : JUnitElement) -> JUnitDocument

    JUnitDocument::with_statistics

    fn JUnitDocument::with_statistics(self : JUnitDocument, overwrite_time? : Bool) -> JUnitDocument raise InputError

    Recount structural cases (error > failure > skipped > passed), not result elements. Preserve suite wall time unless overwrite_time is true. Missing case times contribute no information; a partial sum is only known case time.

    JUnitDocument::write_to

    fn JUnitDocument::write_to(self : JUnitDocument, write : (String) -> Unit raise, max_units? : Int64) -> Unit raise

    Deliver validated XML to a caller-owned file, stream or other sink. Sink errors propagate; atomic file replacement belongs to the caller.

    JUnitElement

    pub struct JUnitElement {
    name : String
    attributes : ReadOnlyArray[(String, String)]
    content : ReadOnlyArray[JUnitNode]
    }

    A report element, including producer-specific attributes and children. XML validity is checked when a document is serialized.

    JUnitElement::add_property

    fn JUnitElement::add_property(self : JUnitElement, name : String, value : String) -> JUnitElement

    Append a property without discarding existing or duplicate property entries.

    JUnitElement::append

    fn JUnitElement::append(self : JUnitElement, child : JUnitNode) -> JUnitElement

    JUnitElement::attribute

    fn JUnitElement::attribute(self : JUnitElement, name : String) -> String?

    JUnitElement::elements

    fn JUnitElement::elements(self : JUnitElement, name? : String?) -> Array[JUnitElement]

    Direct child elements, in source order. An omitted name selects all.

    JUnitElement::new

    fn JUnitElement::new(name : String, attributes? : Array[(String, String)], content? : Array[JUnitNode]) -> JUnitElement

    JUnitElement::result

    fn JUnitElement::result(kind : String, message? : String, type_? : String, body? : String) -> JUnitElement raise InputError

    Final or Surefire retry result. Multiple result elements remain separate.

    JUnitElement::suite

    fn JUnitElement::suite(name : String) -> JUnitElement

    JUnitElement::testcase

    fn JUnitElement::testcase(name : String, classname? : String?, time_us? : Int64?) -> JUnitElement raise InputError

    JUnitElement::with_attribute

    fn JUnitElement::with_attribute(self : JUnitElement, name : String, value : String) -> JUnitElement

    Set one attribute, replacing all earlier occurrences of that name.

    JUnitElement::with_content

    fn JUnitElement::with_content(self : JUnitElement, content : Array[JUnitNode]) -> JUnitElement

    JUnitElement::with_output

    fn JUnitElement::with_output(self : JUnitElement, kind : String, text : String) -> JUnitElement raise InputError

    Replace direct system-out or system-err children with one text element.

    JUnitElement::with_results

    fn JUnitElement::with_results(self : JUnitElement, results : Array[JUnitElement]) -> JUnitElement raise InputError

    Replace final result elements, preserving retry markers and other metadata.

    JUnitElement::without_attribute

    fn JUnitElement::without_attribute(self : JUnitElement, name : String) -> JUnitElement

    JUnitNode

    pub(all) enum JUnitNode {
    Element(JUnitElement)
    Text(String)
    CData(String)
    Comment(String)
    Instruction(String, String)
    }

    Ordered XML content. Editing returns new values; no shared mutable children.

    Outcome

    pub(all) enum Outcome {
    Passed
    Failed
    Errored
    Skipped
    } derive(Eq)

    Final outcome is separate from evidence of inconsistent attempts.

    Outcome::equal

    fn Outcome::equal(Outcome, Outcome) -> Bool

    Outcome::label

    fn Outcome::label(self : Outcome) -> String

    Outcome::not_equal

    fn Outcome::not_equal(x : Outcome, y : Outcome) -> Bool

    Report

    pub struct Report {
    cases : ReadOnlyArray[TestCase]
    diagnostics : ReadOnlyArray[Diagnostic]
    // private fields
    }

    impl Eq for Report

    Report::equal

    fn Report::equal(a : Report, b : Report) -> Bool

    Report::not_equal

    fn Report::not_equal(x : Report, y : Report) -> Bool

    Report::to_json

    fn Report::to_json(self : Report, max_units? : Int64) ->
    Json
    raise InputError

    TestCase

    pub struct TestCase {
    id : TestId
    name : String
    classname : String?
    final_results : ReadOnlyArray[String]
    interim_results : ReadOnlyArray[String]
    suite_order : Int
    document_order : Int
    outcome : Outcome
    attempts : Int
    flaky : Bool
    time_us : Int64?
    message : String
    source : String
    // private fields
    }

    impl Eq for TestCase

    TestCase::equal

    fn TestCase::equal(a : TestCase, b : TestCase) -> Bool

    TestCase::is_error

    fn TestCase::is_error(self : TestCase) -> Bool

    TestCase::is_failure

    fn TestCase::is_failure(self : TestCase) -> Bool

    TestCase::is_flaky

    fn TestCase::is_flaky(self : TestCase) -> Bool

    TestCase::is_passed

    fn TestCase::is_passed(self : TestCase) -> Bool

    Raw result flags match junitparser's independent Boolean properties.

    TestCase::is_rerun

    fn TestCase::is_rerun(self : TestCase) -> Bool

    TestCase::is_skipped

    fn TestCase::is_skipped(self : TestCase) -> Bool

    TestCase::not_equal

    fn TestCase::not_equal(x : TestCase, y : TestCase) -> Bool

    TestId

    pub(all) enum TestId {
    Class(String, String)
    SuitePath(ReadOnlyArray[String], String)
    } derive(Eq)

    Tagged identity prevents class names from colliding with fallback suite paths.

    TestId::equal

    fn TestId::equal(TestId, TestId) -> Bool

    TestId::key

    fn TestId::key(self : TestId) -> String

    TestId::not_equal

    fn TestId::not_equal(x : TestId, y : TestId) -> Bool

    TestId::to_json

    classify

    fn classify(before : Outcome?, after : Outcome?) -> String

    Final outcomes drive categories. Flakiness is an independent annotation.

    compare

    fn compare(history : Array[Report], candidate : Report, config? : DiffConfig) -> DiffResult raise InputError

    Inputs are chronological runs; file merging is a separate explicit operation.

    default_config

    fn default_config() -> DiffConfig

    diff_config

    fn diff_config(slowdown_pct? : Int, slowdown_floor_us? : Int64, fail_on? : Array[String]) -> DiffConfig raise InputError

    evaluate_request

    fn evaluate_request(text : String) ->
    Json

    Batch/embedding protocol. Errors are structured and never converted to empty reports.

    merge_documents

    fn merge_documents(documents : Array[JUnitDocument]) -> JUnitDocument raise InputError

    Concatenate report documents without coalescing suites, deduplicating cases or interpreting retries. Conflicting testsuites metadata, namespace bindings or outside-root content is rejected. Resolve conflicts explicitly first. New root aggregate attributes are omitted; use with_statistics to recount.

    merge_reports

    fn merge_reports(reports : Array[Report], duplicates? : DuplicatePolicy) -> Report raise InputError

    Ordered merge. Only explicit Attempts opts into interpreting duplicates as retries.

    parse_document

    fn parse_document(text : String, source? : String) -> Report raise InputError

    Parse the documented unnamespaced JUnit profile; no I/O or external entities.

    parse_duration

    fn parse_duration(value : String) -> Int64? raise InputError

    Decimal seconds, floored to microseconds. Bounds keep comparison arithmetic safe.

    parse_report

    fn parse_report(text : String, source? : String, duplicates? : DuplicatePolicy) -> Report raise InputError

    Normalize one document. Explicit attempts always use original document order.

    run_cli

    fn run_cli(args : Array[String], read : (String) -> String raise, write? : (String, String) -> Unit raise?, glob? : (String) -> Array[String] raise?) -> CliResult

    The callback owns all I/O. This function can be tested on any MoonBit target.

    usage

    fn usage() -> String