/ plugins / report format

Testhide Report Format v1

The canonical contract every official Testhide reporter emits — a JUnit-extended XML (junittests.xml) with a stable failure id, a resolution, captured output, attachments and suite metadata. The build agent parses it identically for pytest, unittest, .NET and JS, so your data always lands correctly.

The one rule

Always emit the JUnit dialect: <testsuites> → <testsuite> → <testcase>. That's the only shape that carries the rich fields (fail_id, test_resolution, properties, system-out). Native NUnit / xUnit XML is a degraded fallback that drops them — which is exactly why you install the plugin.

Schema version: 1 · Status: stable. Plugins emit testhide_schema_version=1 as a suite property; a missing version is treated as 1 for back-compat.
1

Document & suite

One <testsuite> per run, wrapped in <testsuites>. Counts should equal the sum of the testcases; the suite timestamp must be ISO-8601 UTC (trailing Z).

Suite attributes

AttributeRequiredNotes
nameoptionalRunner / suite name (e.g. pytest, dotnet). Free text.
timestamprequiredISO-8601 UTC with trailing Z. The agent derives per-test start times from this + each time.
hostnameoptionalAuto-populated by the plugin.
tests / failures / errors / skipped / timerecommendedSum of children. The backend recomputes from the testcases, so a mismatch is a warning, not fatal.

Suite <properties>

PropertyRequiredNotes
testhide_schema_versionrequiredValue 1 in v1 plugins.
ip_address, hostnameautoPopulated by the plugin; identify the runner.
buildoptionalReserved — shown as the build number.
branchoptionalReserved — the branch.
any otheroptionalFree metadata: every name/value pair is captured verbatim.
2

The <testcase>

One per test. A passing test has no outcome child; every other state has exactly one of <failure> / <error> / <skipped>.

Attributes

AttributeRequiredNotes
classnamerequiredDotted path (package.module.Class); strip the source file extension.
namerequiredTest function/case name. Parametrization is stripped from the identity used for fail_id (the human name may keep it).
timerequiredSeconds, . decimal, invariant culture. setup + call + teardown.
fail_idrequired*md5("module.class.function.ExceptionType(message)"). Non-empty on failure/error, empty otherwise. Stable across runs → the backend dedups failures and links Jira on it.
test_resolutionrequiredClosed set (below). Parser default if absent: Unresolved.
file, linerecommendedEnables stacktrace ↔ changed-file code-impact scoring.

Outcome children

StateElementNotes
PassednoneNo outcome child; fail_id="".
Failed<failure message="…">One-line message; full cleaned traceback in <![CDATA[…]]>.
Errored<error message="…">Collection / import / teardown error (not an assertion).
Skipped / xfail<skipped type="…" message="…">For an expected failure that failed (xfail), prefer <failure> + test_resolution="Known Issue" so it isn't a hard failure.

Per-test <properties> & output

PropertyNotes
docstrHuman-readable intent (the docstring). Fed to the text embedder.
attachmentRepeatable. File path or URL — images, logs, JSON, binaries. The agent downloads & runs image/binary/config parsers (+ CLIP on images). Value must be non-empty.
infoFree-form JSON or text context.
jira (or issue)Linked ticket(s). Either name is accepted.
<system-out>Execution log / steps / HTTP traces. Sanitized & truncated to 512 KB. Wrap in <![CDATA[…]]>.

test_resolution — closed set

PassedSkippedCollection ErrorTeardown Error Known IssueNeed to reopenResolved in branch Verified at BranchUnresolved

Default mapping: pass→Passed, skip→Skipped, import/collection failure→Collection Error, teardown failure→Teardown Error, xfail→Known Issue. Jira enrichment may override to Known Issue / Need to reopen / Resolved in branch / Verified at Branch based on the linked ticket's status.

3

A complete report

Every field, every outcome — passed, failed (attachments + info + captured output), collection error, skipped, and a known-issue/xfail. Point your job's report_paths at this file.

<?xml version="1.0" encoding="utf-8"?>
<testsuites>
  <testsuite name="pytest" timestamp="2026-05-29T10:00:00.000Z" hostname="build-node-07"
             tests="5" failures="2" errors="1" skipped="1" time="0.470">
    <properties>
      <property name="testhide_schema_version" value="1"/>
      <property name="ip_address" value="10.0.0.7"/>
      <property name="hostname"   value="build-node-07"/>
      <property name="build"      value="1042"/>
      <property name="branch"     value="main"/>
    </properties>

    <!-- PASSED — no outcome child, empty fail_id -->
    <testcase classname="shop.auth.LoginTests" name="test_login_ok"
              file="tests/test_login.py" line="12" time="0.100"
              fail_id="" test_resolution="Passed">
      <properties>
        <property name="docstr" value="User can log in with valid credentials."/>
      </properties>
    </testcase>

    <!-- FAILED — <failure> + attachments + info JSON + captured output -->
    <testcase classname="shop.checkout.CheckoutTests" name="test_total_with_tax"
              file="tests/test_checkout.py" line="48" time="0.200"
              fail_id="3f8a1c2b9d4e5f60718293a4b5c6d7e8" test_resolution="Unresolved">
      <failure message="AssertionError: expected total 110.00, got 100.00"><![CDATA[
Traceback (most recent call last):
  File "tests/test_checkout.py", line 52, in test_total_with_tax
    assert order.total == Decimal("110.00")
AssertionError: expected total 110.00, got 100.00
]]></failure>
      <properties>
        <property name="docstr"     value="Order total must include 10% tax."/>
        <property name="attachment" value="https://artifacts.example.com/1042/checkout_fail.png"/>
        <property name="attachment" value="/var/run/testhide/1042/checkout.har"/>
        <property name="info"       value="{&quot;retries&quot;: 1, &quot;env&quot;: &quot;staging&quot;}"/>
      </properties>
      <system-out><![CDATA[POST /cart/checkout -> 200
[assert] total == 110.00  FAILED (got 100.00)]]></system-out>
    </testcase>

    <!-- ERRORED — import / collection failure -->
    <testcase classname="shop.reports.ImportSuite" name="test_imports"
              file="tests/test_reports.py" line="1" time="0.050"
              fail_id="aa11bb22cc33dd44ee55ff6677889900" test_resolution="Collection Error">
      <error message="ModuleNotFoundError: No module named 'pandas'"><![CDATA[
ModuleNotFoundError: No module named 'pandas'
]]></error>
    </testcase>

    <!-- SKIPPED -->
    <testcase classname="shop.payment.PaymentTests" name="test_refund"
              file="tests/test_payment.py" line="55" time="0.000"
              fail_id="" test_resolution="Skipped">
      <skipped type="pytest.skip" message="refund API disabled in staging"/>
    </testcase>

    <!-- XFAIL / KNOWN ISSUE — a failure linked to a ticket, not a hard regression -->
    <testcase classname="shop.payment.PaymentTests" name="test_gateway_timeout"
              file="tests/test_payment.py" line="70" time="0.120"
              fail_id="bb22cc33dd44ee55ff6677889900aa11" test_resolution="Known Issue">
      <failure message="TimeoutError: gateway did not respond in 30s"><![CDATA[
TimeoutError: gateway did not respond in 30s
]]></failure>
      <properties>
        <property name="jira" value="PAY-417 Known Issue [gateway timeout under load]"/>
      </properties>
    </testcase>

  </testsuite>
</testsuites>
4

Behaviour & guarantees

Every official plugin shares these.

Timing

The agent synthesizes per-test timestamps from the suite timestamp + each time — emit an accurate suite timestamp and per-test time; per-test timestamps aren't needed.

Parallel-safe

Each worker writes a temp chunk under .{report}_temp/, atomically merged on finish — safe with pytest-xdist, sharded dotnet test, and Jest workers.

Quarantine

Deselect flaky tests by node-id from --quarantine-file, TESTHIDE_QUARANTINE_FILE, or .testhide_quarantine_file. One id per line; blanks and # comments ignored.

Jira-aware

Optional: given creds, each plugin looks up the issue by fail_id and enriches the failure message + resolution — works offline, no backend round-trip.

Encoding & safety

UTF-8, valid XML 1.0 only (control chars stripped, emoji/CJK/Cyrillic kept); tracebacks & system-out in CDATA; no DTD/DOCTYPE (XXE-hardened).

Versioning

This is v1. Additive property names don't bump the version (unknown names are ignored safely); breaking changes ship a new version + spec.

Ready to wire it up? Browse the plugins ↗