<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pankaj Kumar Nayak</title>
    <description>The latest articles on DEV Community by Pankaj Kumar Nayak (@pankajnayak1994).</description>
    <link>https://dev.to/pankajnayak1994</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4168881%2Fdc36772d-1b4a-4e0d-916b-a00ed5e8cbbc.png</url>
      <title>DEV Community: Pankaj Kumar Nayak</title>
      <link>https://dev.to/pankajnayak1994</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pankajnayak1994"/>
    <language>en</language>
    <item>
      <title>Stop Digging Through CI Logs: One Test Report for Python, Java, JS, .NET, Go and Robot Framework</title>
      <dc:creator>Pankaj Kumar Nayak</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:08:59 +0000</pubDate>
      <link>https://dev.to/pankajnayak1994/stop-digging-through-ci-logs-one-test-report-for-python-java-js-net-go-and-robot-framework-m6g</link>
      <guid>https://dev.to/pankajnayak1994/stop-digging-through-ci-logs-one-test-report-for-python-java-js-net-go-and-robot-framework-m6g</guid>
      <description>&lt;p&gt;It is 9:40 in the morning. The nightly build is red. The CI log is 14,000 lines long, and somewhere in it are the four failures you care about. Before you can fix anything, you have to answer the same questions every team asks after every red build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What broke since yesterday?&lt;/strong&gt; Not "what is failing", but what is &lt;em&gt;newly&lt;/em&gt; failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it one problem or ten?&lt;/strong&gt; Forty tests failing with the same &lt;code&gt;ConnectionError&lt;/code&gt; is one broken Redis, not forty bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is this test flaky, or did we really break it?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can the person who isn't a Python developer read the result?&lt;/strong&gt; The QA lead, the release manager, the Java team whose service you call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most test runners answer none of these out of the box. They print pass/fail to a terminal and leave the rest to you.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/pankajnayak1994/pyrept" rel="noopener noreferrer"&gt;pyrept&lt;/a&gt;&lt;/strong&gt; to answer all four, for every test framework a real company runs, in one consistent report. This article is a practical, engineering-focused tour: what it produces, how it works inside, and copy-paste recipes for Python, Java, JavaScript/TypeScript, .NET, Go, Robot Framework, Ruby, Rust and shell tests, plus GitHub Actions, GitLab, Jenkins and Azure DevOps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try it before reading further:&lt;/strong&gt; here is the &lt;a href="https://pankajnayak1994.github.io/pyrept/demo/report.html" rel="noopener noreferrer"&gt;live demo report&lt;/a&gt; and the &lt;a href="https://pankajnayak1994.github.io/pyrept/demo/history.html" rel="noopener noreferrer"&gt;demo history page&lt;/a&gt;. Open them on your phone, they work there too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fazkwwwnk9uq0oajfmma2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fazkwwwnk9uq0oajfmma2.png" alt="pyrept HTML test report showing the run verdict, a 92% pass-rate ring, the outcome bar, counts, a comparison with the previous run and common failure causes" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation :
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pyrept
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open &lt;code&gt;report.html&lt;/code&gt;. That's it for pytest. For everything else:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You have&lt;/th&gt;
&lt;th&gt;You run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pytest / pytest-bdd / Playwright / Selenium (Python)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pytest --pyrept&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unittest (standard library only)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python -m pyrept.unittest_runner discover -s tests&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nose2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nose2 --html-report&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;behave (Cucumber BDD)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;behave -f pyrept.behave_formatter:PyreptFormatter -o /dev/null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java, JS/TS, .NET, Go, Ruby, Rust, PHP, Robot Framework, TAP, Allure, Cucumber&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from &amp;lt;format&amp;gt; &amp;lt;files&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uses: pankajnayak1994/pyrept@v1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every path produces the same four outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report.html&lt;/code&gt;&lt;/strong&gt;: one self-contained page. No CDN, no external fonts, no server. Open it offline, attach it to a CI run, email it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report.json&lt;/code&gt;&lt;/strong&gt;: the same data, machine-readable, for dashboards, bots and the next run's comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;junit.xml&lt;/code&gt;&lt;/strong&gt; (optional): so GitLab, Jenkins, Azure DevOps and friends show results in their native test tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Markdown summary&lt;/strong&gt; (optional): for GitHub job summaries, pull request comments and chat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only runtime dependency is &lt;code&gt;jinja2&lt;/code&gt;. Python 3.8 to 3.14 are tested in CI, plus the 3.15 pre-releases.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 1: The technical spec
&lt;/h2&gt;

&lt;p&gt;Before the recipes, it helps to know the model pyrept builds, because every feature falls out of it.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.1 The pipeline
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  pytest plugin ───────┐
  unittest runner ─────┤
  nose2 plugin ────────┤                         ┌──&amp;gt; report.html  (self-contained)
  behave formatter ────┼──&amp;gt; ReportCollector ─────┼──&amp;gt; report.json  (machine-readable)
  Python API ──────────┤      │                  ├──&amp;gt; junit.xml    (CI test tabs)
                       │      ├─ baseline diff   ├──&amp;gt; summary.md   (job summary / PR)
  pyrept convert ──────┘      ├─ failure groups  └──&amp;gt; Slack / Teams / webhook
   (JUnit, TRX, NUnit,        └─ quality gates
    xUnit, Robot, TAP,
    Allure, Cucumber,
    Playwright, ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every integration, native or imported, feeds one &lt;code&gt;ReportCollector&lt;/code&gt;. Everything downstream (HTML, JSON, JUnit, Markdown, comparison, gates, notifications) is written once and behaves identically no matter where the tests came from. That's the whole design: &lt;strong&gt;normalise early, render once.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  1.2 Four outcomes, mapped the same way everywhere
&lt;/h3&gt;

&lt;p&gt;Every test ends as exactly one of &lt;code&gt;passed&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt; or &lt;code&gt;skipped&lt;/code&gt;. The mapping is deliberate and consistent across all frameworks and importers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Reported as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Assertion failure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unexpected exception, setup/teardown failure, collection or import error, undefined BDD step, hook error, timeout, crash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skipped test&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;skipped&lt;/code&gt;, with the reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected failure (&lt;code&gt;xfail&lt;/code&gt;, &lt;code&gt;@expectedFailure&lt;/code&gt;, TAP &lt;code&gt;# TODO&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;skipped&lt;/code&gt;, with "Expected failure"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unexpected success (strict &lt;code&gt;xfail&lt;/code&gt; that passed, &lt;code&gt;@expectedFailure&lt;/code&gt; that passed)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failing subtest&lt;/td&gt;
&lt;td&gt;its own &lt;code&gt;failed&lt;/code&gt; / &lt;code&gt;error&lt;/code&gt; entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passed after a rerun&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;passed&lt;/code&gt;, with &lt;code&gt;retries&lt;/code&gt; and &lt;code&gt;flaky&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pass rate = passed ÷ (total − skipped).&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering note: why skipped tests don't lower the pass rate.&lt;/strong&gt; If you skip 200 tests because a sandbox is down, your code didn't get 200 tests worse. Counting skips as failures trains teams to delete skip markers instead of fixing environments. Skips are shown prominently, they just don't pollute the quality signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering note: why &lt;code&gt;failed&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; are different.&lt;/strong&gt; A &lt;code&gt;failed&lt;/code&gt; test means your code produced the wrong answer. An &lt;code&gt;error&lt;/code&gt; usually means the test couldn't even run: a fixture blew up, an import failed, a dependency was down. When 30 errors appear at once, you look at infrastructure first. When 3 failures appear, you look at the diff. Keeping them separate saves real triage time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  1.3 The JSON contract
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;report.json&lt;/code&gt; is the stable interface between pyrept and everything else (dashboards, bots, the next run). Abbreviated:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_report_title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Checkout suite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"passed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"skipped"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"percentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;92.0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026/10/07 11:58:46 UTC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;13.29&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Python"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.13.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Framework"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pytest 9.0 + Playwright"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#1842"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slowest_tests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/test_checkout.py::test_pay_with_card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"failure_groups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"redis.exceptions.ConnectionError: Error 111 connecting to cache:6379. Connection refused."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                      &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tests/test_api.py::test_rate_limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/test_api.py::test_session_cache"&lt;/span&gt;&lt;span class="p"&gt;]}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"comparison"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"percentage_delta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"new_failures"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tests/test_checkout.py::test_pay_with_card"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fixed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"still_failing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new_tests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"removed_tests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slower"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ratio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.85&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/test_checkout.py::test_pay_with_card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pays with a saved Visa card."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"traceback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AssertionError: ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/test_checkout.py:42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"smoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;]}}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;test_results&lt;/code&gt; lists errors first, then failures, skips and passes. &lt;code&gt;comparison&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; when no baseline was given. Because the format is plain JSON, a pyrept report from one job is also a valid &lt;em&gt;input&lt;/em&gt; to another: that's how sharded runs and multi-language monorepos get merged (Part 3).&lt;/p&gt;
&lt;h3&gt;
  
  
  1.4 Regression diff: comparing with the previous run
&lt;/h3&gt;

&lt;p&gt;Give any integration an earlier &lt;code&gt;report.json&lt;/code&gt; as a &lt;em&gt;baseline&lt;/em&gt;, and every test gets a change label:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Failed/errored now, but passed, was skipped, or didn't exist in the baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fixed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Failed/errored in the baseline, passes now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Still failing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Failed/errored in both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not in the baseline, passing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Removed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In the baseline, not in this run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slower&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;At least &lt;strong&gt;1.5×&lt;/strong&gt; as slow &lt;strong&gt;and&lt;/strong&gt; at least &lt;strong&gt;0.5 s&lt;/strong&gt; slower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nt472y5xpvr9jd2gn1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nt472y5xpvr9jd2gn1x.png" alt="pyrept comparison panel: new failures, fixed, still failing, new tests, removed tests and slower tests" width="799" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering note: why "slower" needs two thresholds.&lt;/strong&gt; A ratio alone flags a 2 ms test that became 5 ms (2.5×, irrelevant). An absolute delta alone flags a 60 s integration test that jittered to 60.6 s. Requiring &lt;em&gt;both&lt;/em&gt; 1.5× and +0.5 s catches real regressions and ignores noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering note: the baseline can be the file you're about to overwrite.&lt;/strong&gt; pyrept reads the baseline before writing, so &lt;code&gt;--pyrept-json=report.json --pyrept-baseline=report.json&lt;/code&gt; always compares with the previous run. A missing or corrupt baseline never fails the run; it logs a warning and writes the report without a comparison. That's exactly what you want on the first CI run of a new branch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tests are matched by name, so compare reports that come from the same source.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.5 Common failure causes
&lt;/h3&gt;

&lt;p&gt;Failures that share an error message are grouped. In the demo, two API tests fail with the same &lt;code&gt;redis.exceptions.ConnectionError&lt;/code&gt;; the report shows one group of 2, not two separate mysteries. On a bad day this turns "38 failures" into "1 broken Redis + 2 real bugs", which is a very different stand-up.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.6 What gets captured per test
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Description (docstrings, BDD scenario text), tags (pytest markers, NUnit categories, xUnit traits, Robot tags, Allure labels), duration and &lt;code&gt;file:line&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For failures: captured stdout, stderr and logs, the traceback, and its first line as the message.&lt;/li&gt;
&lt;li&gt;For browser tests: a full-page &lt;strong&gt;screenshot&lt;/strong&gt; taken while the browser is still open, the &lt;strong&gt;browser console&lt;/strong&gt; output and page errors (Playwright), and links to Playwright &lt;strong&gt;traces&lt;/strong&gt; and &lt;strong&gt;videos&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Retries and flakiness (pytest-rerunfailures, Surefire reruns, Playwright retries, Allure history).&lt;/li&gt;
&lt;li&gt;Environment: Python, platform, framework, plus anything you add.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  1.7 Security and robustness details
&lt;/h3&gt;

&lt;p&gt;These are the boring things that make a report tool safe to run on untrusted CI output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything is HTML-escaped&lt;/strong&gt;, including tracebacks that contain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;-like text, and attachment data from imported files. &lt;code&gt;javascript:&lt;/code&gt; links in attachment paths are never rendered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images up to 5 MB are embedded&lt;/strong&gt; as data URIs, so the report stays a single file. Larger files stay linked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JUnit output strips characters XML forbids&lt;/strong&gt; (such as ANSI colour codes), so CI parsers never reject it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Markdown summary stays far below GitHub's 1 MiB job-summary limit&lt;/strong&gt;, however big the run. It shows the first 20 failures with trimmed errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook URLs are treated as secrets&lt;/strong&gt;: never logged, and a failed notification never fails your build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes are predictable&lt;/strong&gt;: &lt;code&gt;0&lt;/code&gt; OK, &lt;code&gt;1&lt;/code&gt; a gate failed, &lt;code&gt;2&lt;/code&gt; bad input (missing file, wrong format).&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Part 2: Python recipes
&lt;/h2&gt;
&lt;h3&gt;
  
  
  2.1 pytest in 30 seconds
&lt;/h3&gt;

&lt;p&gt;Here's a tiny shop test suite I'll use for the rest of the Python section:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/test_cart.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.smoke&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add_item&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adding an item updates the cart count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_apply_coupon&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;AUTUMN15 takes 15% off.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mf"&gt;85.0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;85.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_legacy_tax_rules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Known broken since the tax refactor (JIRA-123).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.31&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment sandbox down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_refund&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The plugin is installed with the package but &lt;strong&gt;stays off until you ask for a report&lt;/strong&gt;, so it never changes a normal &lt;code&gt;pytest&lt;/code&gt; run. Docstrings become descriptions, markers become tags, skip reasons are kept.&lt;/p&gt;

&lt;p&gt;All the options:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--pyrept-html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.html &lt;span class="nt"&gt;--pyrept-json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.json
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt; &lt;span class="nt"&gt;--pyrept-title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Nightly regression"&lt;/span&gt;
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt; &lt;span class="nt"&gt;--pyrept-junit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/junit.xml        &lt;span class="c"&gt;# also JUnit XML&lt;/span&gt;
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt; &lt;span class="nt"&gt;--pyrept-baseline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.json   &lt;span class="c"&gt;# compare with the previous run&lt;/span&gt;
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt; &lt;span class="nt"&gt;--pyrept-markdown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/summary.md    &lt;span class="c"&gt;# Markdown summary&lt;/span&gt;
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt; &lt;span class="nt"&gt;--pyrept-github-summary&lt;/span&gt;                 &lt;span class="c"&gt;# GitHub job summary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or set it once in &lt;code&gt;pytest.ini&lt;/code&gt; / &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[pytest]&lt;/span&gt;
&lt;span class="py"&gt;pyrept&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;pyrept_html&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;reports/report.html&lt;/span&gt;
&lt;span class="py"&gt;pyrept_json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;reports/report.json&lt;/span&gt;
&lt;span class="py"&gt;pyrept_junit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;reports/junit.xml&lt;/span&gt;
&lt;span class="py"&gt;pyrept_baseline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;reports/report.json&lt;/span&gt;
&lt;span class="py"&gt;pyrept_github_summary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering note: path resolution.&lt;/strong&gt; Paths on the command line are relative to where you run pytest. Paths in the ini file are relative to the &lt;strong&gt;ini file&lt;/strong&gt;, so the report always lands in the same place no matter which subdirectory a developer runs &lt;code&gt;pytest&lt;/code&gt; from. Small thing, saves a lot of "where did my report go?".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  2.2 The regression workflow, for real
&lt;/h3&gt;

&lt;p&gt;I ran the suite above twice. On the second run, a coupon calculation broke and the Redis cache was down. pytest's own output tells you 4 things failed. pyrept adds one line that tells you what matters:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;------ pyrept: 3 new failures, 0 fixed, 1 still failing (vs report.json) -------
FAILED tests/test_cart.py::test_apply_coupon - assert 90.0 == 85.0
FAILED tests/test_cart.py::test_legacy_tax_rules - assert 0.3 == 0.31
ERROR tests/test_api.py::test_rate_limit - ConnectionError: Error 111 connect...
ERROR tests/test_api.py::test_session_cache - ConnectionError: Error 111 conn...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;test_legacy_tax_rules&lt;/code&gt; was already broken yesterday. The other three are new, and two of them share one root cause. That's the triage done before you've opened a single traceback.&lt;/p&gt;
&lt;h3&gt;
  
  
  2.3 Quality gates for legacy suites: the ratchet
&lt;/h3&gt;

&lt;p&gt;Every mature codebase has a few known-broken tests. Teams usually respond by either ignoring the red build (bad) or deleting the tests (worse). pyrept gives you a third option: &lt;strong&gt;fail only on new failures, with a floor.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--pyrept-json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.json &lt;span class="nt"&gt;--pyrept-baseline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.json &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--pyrept-ignore-known-failures&lt;/span&gt; &lt;span class="nt"&gt;--pyrept-fail-under&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With the known-broken tax test only:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- pyrept: 1 known failure ignored: all of them were already failing in the baseline -
1 failed, 4 passed, 1 skipped in 0.04s
$ echo $?
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The moment a &lt;em&gt;previously passing&lt;/em&gt; test breaks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- pyrept: 3 new failures, so --pyrept-ignore-known-failures does not apply ---
$ echo $?
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;fail-under&lt;/code&gt; floor stops known failures from quietly piling up. Over time you raise it: that's the ratchet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering note: gates never hide other failures.&lt;/strong&gt; If pytest-cov's &lt;code&gt;--cov-fail-under&lt;/code&gt; (or any other plugin) failed the run, pyrept keeps it failed. Collection errors (a test module that doesn't even import) are never ignored. A gate that can accidentally turn a broken build green is worse than no gate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  2.4 Parallel, retries, subtests, BDD
&lt;/h3&gt;

&lt;p&gt;These just work with the same flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pytest-xdist&lt;/strong&gt; (&lt;code&gt;-n auto&lt;/code&gt;): workers send results to the controller, which writes one report. Descriptions, tags and screenshots survive the trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest-rerunfailures&lt;/strong&gt;: reruns aren't counted as extra tests; the final attempt is recorded with &lt;code&gt;retries&lt;/code&gt;, and marked &lt;code&gt;flaky&lt;/code&gt; if it passed after a retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subtests&lt;/strong&gt; (built into pytest 9+, or &lt;code&gt;pytest-subtests&lt;/code&gt;): each failing subtest gets its own entry, like &lt;code&gt;test_x (i=1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest-bdd&lt;/strong&gt;: each scenario shows the feature, the scenario and every step with its status:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feature: Checkout
Scenario: Pay with card
  Given a cart with 2 items  [passed]
  When I pay with a declined card  [failed]
  Then I see the confirmation page  [skipped]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2.5 Playwright and Selenium (Python)
&lt;/h3&gt;

&lt;p&gt;UI test failures without a screenshot are guesswork. When a test using a pytest-playwright &lt;code&gt;page&lt;/code&gt; fixture, or a Selenium &lt;code&gt;driver&lt;/code&gt; / &lt;code&gt;selenium&lt;/code&gt; / &lt;code&gt;browser&lt;/code&gt; fixture, fails, pyrept attaches a full-page screenshot (taken &lt;em&gt;before&lt;/em&gt; the browser closes), the browser console output and uncaught page errors, and links to the trace and video:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"pyrept[playwright]"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt; &lt;span class="nt"&gt;--tracing&lt;/span&gt; retain-on-failure &lt;span class="nt"&gt;--video&lt;/span&gt; retain-on-failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open traces at &lt;a href="https://trace.playwright.dev" rel="noopener noreferrer"&gt;trace.playwright.dev&lt;/a&gt;. Links are relative to the report, so upload &lt;code&gt;reports/&lt;/code&gt; and &lt;code&gt;test-results/&lt;/code&gt; together as one CI artifact.&lt;/p&gt;
&lt;h3&gt;
  
  
  2.6 Extend it from &lt;code&gt;conftest.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Three hooks let you add environment rows, attachments or anything else without forking anything:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyrept&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_attachment&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_pyrept_environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BUILD_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_SHA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_pyrept_attachments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Attach the API server's log to every failing test
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;make_attachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;:])]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_pyrept_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_report_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (nightly)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;pytest_pyrept_attachments&lt;/code&gt; runs where the test runs, including xdist workers, so it returns plain data.&lt;/p&gt;
&lt;h3&gt;
  
  
  2.7 unittest, nose2, behave
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;unittest&lt;/strong&gt;, with zero extra dependencies. It accepts all the usual &lt;code&gt;python -m unittest&lt;/code&gt; arguments:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pyrept.unittest_runner discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--pyrept-junit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/junit.xml &lt;span class="nt"&gt;--pyrept-baseline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or from code:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyrept.unittest_runner&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PyreptTestRunner&lt;/span&gt;

&lt;span class="n"&gt;suite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultTestLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PyreptTestRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/report.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unit tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wasSuccessful&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;nose2&lt;/strong&gt; (&lt;code&gt;pip install "pyrept[nose2]"&lt;/code&gt;), in &lt;code&gt;nose2.cfg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[unittest]&lt;/span&gt;
&lt;span class="py"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;pyrept.html_report&lt;/span&gt;

&lt;span class="nn"&gt;[html-report]&lt;/span&gt;
&lt;span class="py"&gt;always-on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;span class="py"&gt;html-report-path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;test_results/report.html&lt;/span&gt;
&lt;span class="py"&gt;json-report-path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;test_results/report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;behave&lt;/strong&gt; (Cucumber BDD for Python):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"pyrept[behave]"&lt;/span&gt;
behave &lt;span class="nt"&gt;-f&lt;/span&gt; pyrept.behave_formatter:PyreptFormatter &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-f&lt;/span&gt; pretty &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nv"&gt;pyrept_html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.html &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nv"&gt;pyrept_json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports/report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha worth knowing:&lt;/strong&gt; behave pairs each &lt;code&gt;-o&lt;/code&gt; with the &lt;code&gt;-f&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; it. Put &lt;code&gt;-f pyrept... -o /dev/null&lt;/code&gt; first; if you write &lt;code&gt;-f pretty -f pyrept -o /dev/null&lt;/code&gt;, your console output disappears into &lt;code&gt;/dev/null&lt;/code&gt;. (Windows: &lt;code&gt;-o NUL&lt;/code&gt;.)&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Part 3: Every other stack
&lt;/h2&gt;

&lt;p&gt;Here's the part I'm most proud of. Your QA team writes Robot Framework, the backend is Java, the frontend is TypeScript, the payment service is .NET, and the CLI is Go. They all produce &lt;em&gt;some&lt;/em&gt; report file. &lt;code&gt;pyrept convert&lt;/code&gt; turns each of them into the same HTML/JSON/JUnit/Markdown output, and &lt;strong&gt;the project you convert from doesn't need to be a Python project.&lt;/strong&gt; You only need Python on the CI runner, which every hosted runner already has.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.1 The format matrix
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Produce a report with&lt;/th&gt;
&lt;th&gt;Convert with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Java&lt;/strong&gt;: Maven Surefire / Failsafe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mvn test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit target/surefire-reports/*.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Java / Kotlin&lt;/strong&gt;: Gradle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./gradlew test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit build/test-results/test/*.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;JavaScript / TypeScript&lt;/strong&gt;: Jest&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jest --ci --reporters=default --reporters=jest-junit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit junit.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cypress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cypress run --reporter junit --reporter-options "mochaFile=results/junit-[hash].xml"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit results/*.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Playwright Test&lt;/strong&gt; (JS/TS)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx playwright test --reporter=json &amp;gt; results.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from playwright results.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; test runner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node --test --test-reporter=tap &amp;gt; results.tap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from tap results.tap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;.NET&lt;/strong&gt; (MSTest, NUnit, xUnit via VSTest)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dotnet test --logger trx --results-directory TestResults&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from trx TestResults/*.trx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;NUnit&lt;/strong&gt; 3 / 2 native XML&lt;/td&gt;
&lt;td&gt;NUnit console&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from nunit TestResult.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;xUnit.net&lt;/strong&gt; v2 XML&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-xml results.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from xunit results.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gotestsum --junitfile go-junit.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit go-junit.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cargo nextest run --profile ci&lt;/code&gt; (with JUnit enabled in &lt;code&gt;.config/nextest.toml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit target/nextest/ci/junit.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;PHP&lt;/strong&gt;: PHPUnit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;phpunit --log-junit junit.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit junit.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Ruby&lt;/strong&gt;: RSpec&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rspec --format RspecJunitFormatter --out rspec.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from junit rspec.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Robot Framework&lt;/strong&gt; 3.x to 7.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;robot --outputdir results tests/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from robot results/output.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Shell&lt;/strong&gt;: bats, Perl &lt;code&gt;prove&lt;/code&gt;, &lt;code&gt;pg_prove&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bats --tap tests/ &amp;gt; results.tap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from tap results.tap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Cucumber&lt;/strong&gt; (JVM, JS, Ruby, godog)&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;json&lt;/code&gt; formatter (e.g. &lt;code&gt;--plugin json:target/cucumber.json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from cucumber target/cucumber.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Allure&lt;/strong&gt; (any language)&lt;/td&gt;
&lt;td&gt;an &lt;code&gt;allure-results&lt;/code&gt; directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from allure allure-results&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pytest-json-report&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pytest --json-report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pyrept convert --from pytest-json .report.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each converter does more than read pass/fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JUnit XML&lt;/strong&gt;: properties become tags, Surefire &lt;code&gt;flakyFailure&lt;/code&gt;/&lt;code&gt;rerunFailure&lt;/code&gt; become retries, &lt;code&gt;[[ATTACHMENT|...]]&lt;/code&gt; screenshots are embedded, system-out/err are attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TRX&lt;/strong&gt;: data-driven rows become separate tests, result files are linked or embedded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robot Framework&lt;/strong&gt;: long names like &lt;code&gt;Suite.Sub suite.Test&lt;/code&gt;, tags, documentation, durations for both the Robot 7 &lt;code&gt;elapsed&lt;/code&gt; attribute and older start/end timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TAP 12 to 14&lt;/strong&gt;: plans, &lt;code&gt;SKIP&lt;/code&gt; / &lt;code&gt;TODO&lt;/code&gt; directives, YAML diagnostics, and &lt;code&gt;Bail out!&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allure&lt;/strong&gt;: retries folded into one test with history, labels as tags, attachments kept.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mapping is consistent across all of them: crashes, timeouts and broken tests become &lt;code&gt;error&lt;/code&gt;; ignored, inconclusive and TODO tests become &lt;code&gt;skipped&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.2 One report for a polyglot release
&lt;/h3&gt;

&lt;p&gt;Here's a real run I did while writing this article: a Java Surefire file, a Jest JUnit file, a .NET TRX file, a Robot Framework &lt;code&gt;output.xml&lt;/code&gt; and a Node TAP file, merged into one release report.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; junit  target/surefire-reports/&lt;span class="k"&gt;*&lt;/span&gt;.xml &lt;span class="nt"&gt;--json&lt;/span&gt; parts/java.json   &lt;span class="nt"&gt;--html&lt;/span&gt; parts/java.html
pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; junit  web/junit.xml                 &lt;span class="nt"&gt;--json&lt;/span&gt; parts/web.json    &lt;span class="nt"&gt;--html&lt;/span&gt; parts/web.html
pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; trx    TestResults/&lt;span class="k"&gt;*&lt;/span&gt;.trx             &lt;span class="nt"&gt;--json&lt;/span&gt; parts/dotnet.json &lt;span class="nt"&gt;--html&lt;/span&gt; parts/dotnet.html
pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; robot  results/output.xml            &lt;span class="nt"&gt;--json&lt;/span&gt; parts/robot.json  &lt;span class="nt"&gt;--html&lt;/span&gt; parts/robot.html
pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; tap    cli/results.tap               &lt;span class="nt"&gt;--json&lt;/span&gt; parts/cli.json    &lt;span class="nt"&gt;--html&lt;/span&gt; parts/cli.html

&lt;span class="c"&gt;# Merge every stack into one report (pyrept JSON is itself an input format)&lt;/span&gt;
pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; pyrept parts/&lt;span class="k"&gt;*&lt;/span&gt;.json &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Release 2026.10 - all stacks"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
               &lt;span class="nt"&gt;--html&lt;/span&gt; all/report.html &lt;span class="nt"&gt;--json&lt;/span&gt; all/report.json &lt;span class="nt"&gt;--junit&lt;/span&gt; all/junit.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pyrept: 23 tests (10 passed, 6 failed, 2 errors, 5 skipped)
pyrept HTML report: all/report.html
pyrept JSON report: all/report.json
pyrept JUnit report: all/junit.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your release manager gets one link. Each test keeps its &lt;code&gt;framework&lt;/code&gt; metadata (&lt;code&gt;junit&lt;/code&gt;, &lt;code&gt;trx&lt;/code&gt;, &lt;code&gt;robot&lt;/code&gt;, &lt;code&gt;tap&lt;/code&gt;), so you can still tell where it came from.&lt;/p&gt;

&lt;p&gt;The same trick merges &lt;strong&gt;sharded CI jobs&lt;/strong&gt;: run 8 shards in parallel, have each upload its &lt;code&gt;junit.xml&lt;/code&gt; or &lt;code&gt;report.json&lt;/code&gt;, then merge in a final job:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; junit shard-&lt;span class="k"&gt;*&lt;/span&gt;/junit.xml &lt;span class="nt"&gt;--html&lt;/span&gt; reports/report.html &lt;span class="nt"&gt;--json&lt;/span&gt; reports/report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3.3 Gates and baselines work for every stack
&lt;/h3&gt;

&lt;p&gt;Everything from Part 2 applies to converted reports:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Robot Framework suite: fail the job only on tests that passed last time,&lt;/span&gt;
&lt;span class="c"&gt;# and never let the pass rate fall below 95%&lt;/span&gt;
pyrept convert &lt;span class="nt"&gt;--from&lt;/span&gt; robot output.xml &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--baseline&lt;/span&gt; reports/report.json &lt;span class="nt"&gt;--json&lt;/span&gt; reports/report.json &lt;span class="nt"&gt;--junit&lt;/span&gt; reports/junit.xml &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--fail-on-failure&lt;/span&gt; &lt;span class="nt"&gt;--ignore-known-failures&lt;/span&gt; &lt;span class="nt"&gt;--fail-under&lt;/span&gt; 95
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So a Java team gets "3 new failures, 1 fixed" from their Surefire XML exactly like a Python team does from pytest.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 4: CI recipes
&lt;/h2&gt;
&lt;h3&gt;
  
  
  4.1 GitHub Actions: job summary and a sticky PR comment
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://github.com/marketplace/actions/pyrept" rel="noopener noreferrer"&gt;pyrept GitHub Action&lt;/a&gt;&lt;/strong&gt; puts the result on the run's summary page and keeps &lt;strong&gt;one&lt;/strong&gt; pull request comment up to date (it edits its own comment instead of spamming a new one per push).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;          &lt;span class="c1"&gt;# for the PR comment&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

      &lt;span class="c1"&gt;# Keep the previous report so every run is compared with the one before it&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache/restore@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/report.json&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pyrept-${{ github.ref_name }}-${{ github.run_id }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pyrept-${{ github.ref_name }}-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt pyrept&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;pytest --pyrept-html=reports/report.html --pyrept-json=reports/report.json&lt;/span&gt;
          &lt;span class="s"&gt;--pyrept-baseline=reports/report.json&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;       &lt;span class="c1"&gt;# let the action decide whether the job fails&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pankajnayak1994/pyrept@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/report.json&lt;/span&gt;
          &lt;span class="na"&gt;fail-on-failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;ignore-known-failures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;fail-under&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache/save@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/report.json&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pyrept-${{ github.ref_name }}-${{ github.run_id }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The PR comment looks like this (it's plain GitHub Markdown, trimmed here):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### ❌ Test Report: 2 failed, 2 errors&lt;/span&gt;

| Total | Passed | Failed | Errors | Skipped | Pass rate | Duration |
|---:|---:|---:|---:|---:|---:|---:|
| 6 | 1 | 2 | 2 | 1 | 20.0% | 0.02s |

&lt;span class="gs"&gt;**Compared with previous run**&lt;/span&gt;: pass rate 80.0% then, -60.00 pts now

3 new failures · 0 fixed · 1 still failing

&lt;span class="gu"&gt;#### Common failure causes&lt;/span&gt;
| Tests | Error |
|---:|---|
| 2 | &lt;span class="sb"&gt;`ConnectionError: Error 111 connecting to cache:6379. Connection refused.`&lt;/span&gt; |

&lt;span class="gu"&gt;#### Failures&lt;/span&gt;
▸ tests/test_api.py::test_rate_limit (error, new failure)
▸ tests/test_cart.py::test_apply_coupon (failed, new failure)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Reviewers see "3 new failures, one shared root cause" right in the PR, without opening the Actions tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-Python repos&lt;/strong&gt; use &lt;code&gt;convert-from&lt;/code&gt;, which runs the conversion inside the action:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mvn -B test&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pankajnayak1994/pyrept@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;convert-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;junit&lt;/span&gt;
          &lt;span class="na"&gt;convert-inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;target/surefire-reports/*.xml&lt;/span&gt;
          &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/report.json&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Payments service&lt;/span&gt;
          &lt;span class="na"&gt;fail-on-failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The action also exposes outputs (&lt;code&gt;total&lt;/code&gt;, &lt;code&gt;passed&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;errors&lt;/code&gt;, &lt;code&gt;skipped&lt;/code&gt;, &lt;code&gt;pass-rate&lt;/code&gt;, &lt;code&gt;new-failures&lt;/code&gt;) for later steps, for example to post a badge or block a deploy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering note: forks.&lt;/strong&gt; Pull requests from forks get a read-only token, so they can't comment. The action treats the comment as best-effort: it never fails your job because of a permission it was never going to have. The job summary still works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  4.2 GitLab CI
&lt;/h3&gt;

&lt;p&gt;GitLab reads JUnit natively and shows it in the merge request widget; the HTML goes along as an artifact:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.13&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt pyrept&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pytest --pyrept-html=reports/report.html --pyrept-json=reports/report.json --pyrept-junit=reports/junit.xml&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;reports/&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;junit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/junit.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4.3 Jenkins
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'pytest --pyrept-html=reports/report.html --pyrept-json=reports/report.json --pyrept-junit=reports/junit.xml'&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;junit&lt;/span&gt; &lt;span class="s1"&gt;'reports/junit.xml'&lt;/span&gt;
      &lt;span class="n"&gt;archiveArtifacts&lt;/span&gt; &lt;span class="nl"&gt;artifacts:&lt;/span&gt; &lt;span class="s1"&gt;'reports/**'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;allowEmptyArchive:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4.4 Azure DevOps
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;pip install pyrept&lt;/span&gt;
    &lt;span class="s"&gt;dotnet test --logger trx --results-directory TestResults&lt;/span&gt;
    &lt;span class="s"&gt;pyrept convert --from trx TestResults/*.trx --html reports/report.html --json reports/report.json --junit reports/junit.xml&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test and report&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishTestResults@2&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;testResultsFormat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JUnit&lt;/span&gt;
    &lt;span class="na"&gt;testResultsFiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/junit.xml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports&lt;/span&gt;
  &lt;span class="na"&gt;artifact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-report&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4.5 Deep links
&lt;/h3&gt;

&lt;p&gt;The HTML report keeps filters in the URL, so you can link straight to what matters from a chat message or a ticket:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;report.html#problems&lt;/code&gt;: failures and errors only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;report.html#new-failures&lt;/code&gt;: only what broke since the baseline&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#failed&lt;/code&gt;, &lt;code&gt;#error&lt;/code&gt;, &lt;code&gt;#skipped&lt;/code&gt;, &lt;code&gt;#passed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Part 5: History and flaky tests
&lt;/h2&gt;

&lt;p&gt;A single report tells you about one run. &lt;code&gt;pyrept history&lt;/code&gt; reads many JSON reports and tells you about your test suite:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyrept &lt;span class="nb"&gt;history history&lt;/span&gt;/ &lt;span class="nt"&gt;--html&lt;/span&gt; reports/history.html &lt;span class="nt"&gt;--json&lt;/span&gt; reports/history.json &lt;span class="nt"&gt;--limit&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pyrept: 30 runs, 412 tests, 3 flaky, 4 failing now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfmjpl4q8kbk4u7wbfqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfmjpl4q8kbk4u7wbfqa.png" alt="pyrept history page with pass rate over time, outcomes per run and run durations" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You get pass-rate trends, outcomes and duration per run, &lt;strong&gt;flaky tests&lt;/strong&gt;, &lt;strong&gt;failure streaks&lt;/strong&gt; ("failing for 5 runs, since …"), the most-failing tests and the slowest tests, each with a run-by-run outcome strip.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering note: a precise definition of "flaky".&lt;/strong&gt; The naive definition calls a test flaky if it ever failed and ever passed. That's too eager: a test that broke on Tuesday and was fixed on Wednesday is a &lt;em&gt;regression&lt;/em&gt;, not flakiness. pyrept counts a test as flaky when it fails in &lt;strong&gt;two or more separate stretches&lt;/strong&gt; with passing runs in between, or when it passed only after a retry. The list stays short and actionable instead of shaming every test that was ever fixed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To build history in GitHub Actions, keep one report per run in a cached directory:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache/restore@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;history&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pyrept-history-${{ github.run_id }}&lt;/span&gt;
    &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pyrept-history-&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;pytest --pyrept-json=reports/report.json || true&lt;/span&gt;
    &lt;span class="s"&gt;mkdir -p history &amp;amp;&amp;amp; cp reports/report.json "history/run-${{ github.run_number }}.json"&lt;/span&gt;
    &lt;span class="s"&gt;ls -t history/*.json | tail -n +31 | xargs -r rm   # keep the newest 30&lt;/span&gt;
    &lt;span class="s"&gt;pyrept history history --html reports/history.html&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache/save@v4&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;history&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pyrept-history-${{ github.run_id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Runs are ordered by their timestamp (not file name), and files that aren't pyrept reports are skipped with a message, so a stray file in the cache never breaks the job.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 6: Notifications
&lt;/h2&gt;

&lt;p&gt;Set environment variables in CI. Nothing else changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PYREPT_SLACK_WEBHOOK_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slack incoming webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PYREPT_TEAMS_WEBHOOK_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Microsoft Teams workflow webhook (adaptive card)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PYREPT_WEBHOOK_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any endpoint; receives the summary as JSON (counts, comparison, failing tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PYREPT_NOTIFY_ON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;always&lt;/code&gt; (default), &lt;code&gt;failure&lt;/code&gt; or &lt;code&gt;new-failures&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PYREPT_REPORT_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Link to the published HTML report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Nightly: 2 failed, 1 error of 412 tests (pass rate 99.27%, -0.49 pts) · 2 new failures, 0 fixed · Open the report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;PYREPT_NOTIFY_ON=new-failures&lt;/code&gt; is the one I recommend for team channels: silence while known issues are being worked on, a ping the moment something new breaks. That's how you keep a channel from becoming noise everyone mutes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 7: The Python API, for anything that isn't a test framework
&lt;/h2&gt;

&lt;p&gt;Not every check is a unit test. Load tests, contract checks, data-quality validations, smoke scripts, migration verifications: if it has a name and a pass/fail, it fits. Here's a performance check reported with the same HTML, JSON and JUnit output as everything else:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyrept&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ReportCollector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_attachment&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReportCollector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nightly smoke (k6 + custom checks)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1842&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-west-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET /health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smoke&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST /checkout p95 &amp;lt; 800ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95 was 1240ms (threshold 800ms)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;performance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
           &lt;span class="n"&gt;attachments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;make_attachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k6 summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http_req_duration p(95)=1.24s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET /search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skipped: search cluster in maintenance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/report.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;junit_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports/junit.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# {'total': 3, 'passed': 1, 'failed': 1, 'error': 0, 'skipped': 1, 'percentage': 50.0}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;write()&lt;/code&gt; also takes &lt;code&gt;baseline_path&lt;/code&gt;, &lt;code&gt;markdown_path&lt;/code&gt;, &lt;code&gt;github_summary&lt;/code&gt; and &lt;code&gt;report_url&lt;/code&gt;, so custom checks get regression diffs and PR summaries for free.&lt;/p&gt;

&lt;p&gt;Data engineers: the same pattern works for dbt tests, Great Expectations results or SQL assertions. Platform engineers: wrap your Terraform policy checks or Kubernetes smoke probes. If you can loop over results, you can produce a report your whole org already knows how to read.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 8: The HTML report, up close
&lt;/h2&gt;

&lt;p&gt;A few details that come from using reports during real incidents:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1u7zfb6rxj2trkvngie6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1u7zfb6rxj2trkvngie6.png" alt="pyrept report in dark mode" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verdict first.&lt;/strong&gt; A big PASSED / FAILED header with the pass-rate ring and an outcome bar. You know the answer before you scroll.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problems first.&lt;/strong&gt; Failures and errors are listed before passes; the comparison panel and failure groups sit above the test list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test map.&lt;/strong&gt; One square per test, coloured by outcome. Click any square to jump to it. On a 2,000-test suite, clusters of red show you which module is on fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search across names, descriptions and errors&lt;/strong&gt;, filters with counts, sort by status, name or duration, and per-test duration bars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard navigation&lt;/strong&gt;: &lt;code&gt;j&lt;/code&gt; / &lt;code&gt;k&lt;/code&gt; to move between tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copyable tracebacks&lt;/strong&gt; and zoomable screenshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt; that follows your OS, a print-friendly layout, and it works on a phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branding&lt;/strong&gt;: &lt;code&gt;PYREPT_LOGO=branding/logo.svg&lt;/code&gt; and &lt;code&gt;PYREPT_ACCENT_COLOR="#0f766e"&lt;/code&gt; make it look like &lt;em&gt;your&lt;/em&gt; company's report. &lt;code&gt;PYREPT_LOGO=none&lt;/code&gt; removes the logo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And again: it's &lt;strong&gt;one file&lt;/strong&gt;. No server, no JavaScript CDN, nothing to host. It opens from an email attachment, from a CI artifact zip, or from an air-gapped network.&lt;/p&gt;


&lt;h2&gt;
  
  
  How does it compare?
&lt;/h2&gt;

&lt;p&gt;Honest positioning, because good tools exist already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pytest-html&lt;/strong&gt; is great if you only use pytest and want a simple results table. pyrept adds the cross-framework model, baseline diffs, failure grouping, history/flakiness, gates, Markdown/PR output and importers for other languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allure&lt;/strong&gt; is powerful and beautiful, but it needs its own report generator step and a results directory per framework adapter. pyrept produces the finished single-file HTML directly from the test run, and it can &lt;em&gt;import&lt;/em&gt; Allure results if you already have them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your CI server's test tab&lt;/strong&gt; (GitLab, Jenkins, Azure) is useful, and pyrept feeds it with JUnit. But those tabs are tied to one CI system and can't be emailed, archived or opened offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;pyrept isn't trying to replace your test framework or your CI. It's the layer between them that turns raw results into a decision.&lt;/p&gt;


&lt;h2&gt;
  
  
  Engineering checklist: rolling it out in a team
&lt;/h2&gt;

&lt;p&gt;What I'd do on a real team, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Week 1:&lt;/strong&gt; add &lt;code&gt;--pyrept&lt;/code&gt; (or &lt;code&gt;pyrept convert&lt;/code&gt;) and upload &lt;code&gt;reports/&lt;/code&gt; as a CI artifact. Zero risk, instant value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 1:&lt;/strong&gt; add the GitHub Action (or JUnit for GitLab/Jenkins/Azure) so results show up where reviewers already look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2:&lt;/strong&gt; cache &lt;code&gt;report.json&lt;/code&gt; and pass it as &lt;code&gt;--pyrept-baseline&lt;/code&gt;. Now every run says what's &lt;em&gt;new&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2:&lt;/strong&gt; turn on &lt;code&gt;fail-on-failure&lt;/code&gt; + &lt;code&gt;ignore-known-failures&lt;/code&gt; with a &lt;code&gt;fail-under&lt;/code&gt; floor at today's pass rate. Raise the floor every sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3:&lt;/strong&gt; start a history cache and review the flaky list in retro. Fix or quarantine the top 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3:&lt;/strong&gt; set &lt;code&gt;PYREPT_NOTIFY_ON=new-failures&lt;/code&gt; for the team channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Later:&lt;/strong&gt; merge the other teams' Java/JS/.NET/Robot results into one release report.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pyrept
pytest &lt;span class="nt"&gt;--pyrept&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/pankajnayak1994/pyrept" rel="noopener noreferrer"&gt;github.com/pankajnayak1994/pyrept&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/pyrept/" rel="noopener noreferrer"&gt;pypi.org/project/pyrept&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;GitHub Action:&lt;/strong&gt; &lt;a href="https://github.com/marketplace/actions/pyrept" rel="noopener noreferrer"&gt;marketplace/actions/pyrept&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://pankajnayak1994.github.io/pyrept/" rel="noopener noreferrer"&gt;pankajnayak1994.github.io/pyrept&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Live demo report:&lt;/strong&gt; &lt;a href="https://pankajnayak1994.github.io/pyrept/demo/report.html" rel="noopener noreferrer"&gt;demo/report.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT-licensed and tested on Python 3.8 through 3.14. If it saves you one morning of log-scrolling, a star on GitHub helps other engineers find it. Issues, ideas and pull requests are very welcome, especially new importers: if your stack writes a report format pyrept can't read yet, open an issue with a sample file.&lt;/p&gt;

&lt;p&gt;I'd love to hear in the comments: &lt;strong&gt;what's the first question you ask after a red build?&lt;/strong&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pankajnayak1994" rel="noopener noreferrer"&gt;
        pankajnayak1994
      &lt;/a&gt; / &lt;a href="https://github.com/pankajnayak1994/pyrept" rel="noopener noreferrer"&gt;
        pyrept
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      This Library Generate a HTML and JSON report for your Nose Unit Tests
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;pyrept: one HTML, JSON and JUnit test report for every test framework&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/pankajnayak1994/pyrept/master/docs/images/logo.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fpankajnayak1994%2Fpyrept%2Fmaster%2Fdocs%2Fimages%2Flogo.jpg" alt="pyrept: a Python snake with HTML, JSON and JUnit reports" width="440"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pankajnayak1994/pyrept/actions/workflows/codecov.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/pankajnayak1994/pyrept/actions/workflows/codecov.yml/badge.svg" alt="Tests"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/pankajnayak1994/pyrept" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4bc256e1745b8c29cf80ea3aa00d30603eec6a1936b30fb0cefcc6c7d70b74ee/68747470733a2f2f636f6465636f762e696f2f67682f70616e6b616a6e6179616b313939342f7079726570742f67726170682f62616467652e7376673f746f6b656e3d4d304b5455414f4f3456" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://pypi.org/project/pyrept/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e790c8a570c5d4595c560b385d1c0aacf8ffa9bfdb00a1c249dcf4a5165a44be/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f7079726570743f63616368655365636f6e64733d33363030" alt="PyPI"&gt;&lt;/a&gt;
&lt;a href="https://pypi.org/project/pyrept/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6e2377ec5729ff37ffd224a7492214e4c63e04d2ce0de3db1df0e679c3d72622/68747470733a2f2f696d672e736869656c64732e696f2f707970692f707976657273696f6e732f7079726570743f63616368655365636f6e64733d33363030" alt="Python versions"&gt;&lt;/a&gt;
&lt;a href="https://pepy.tech/project/pyrept" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/573cc4d21298c81f81e0b8cc0ad035eef227ef2e20a6cff35ad75d4e01b00899/68747470733a2f2f7374617469632e706570792e746563682f62616467652f7079726570742f6d6f6e7468" alt="Downloads"&gt;&lt;/a&gt;
&lt;a href="https://github.com/pankajnayak1994/pyrept/blob/master/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/27ce60dc83475913ff8b53ba613796d0d01d972a94fac05bcd6c8cb684300acb/68747470733a2f2f696d672e736869656c64732e696f2f707970692f6c2f707972657074" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://pankajnayak1994.github.io/pyrept/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0362c9f68fc11bb8861d70dece9f7d3d954d1c0db8a53420a9b93820e6d833ad/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d6c69766525323064656d6f2d346634366535" alt="Docs"&gt;&lt;/a&gt;
&lt;a href="https://github.com/marketplace?type=actions&amp;amp;query=pyrept" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f010ca600e66a048add61e8bd6cf408a4917a8aa2e7eebbb41976e70d74b8bea/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61726b6574706c6163652d7079726570742d3265613434663f6c6f676f3d676974687562" alt="GitHub Marketplace"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/pankajnayak1994/pyrept#choose-your-framework" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d405582c27813a835d6186c739b0c9c86b40ee31691d43c0478670a211e11748/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6672616d65776f726b732d707974657374253230253743253230756e6974746573742532302537432532306e6f7365322532302537432532306265686176652532302537432532307079746573742d2d626464253230253743253230506c617977726967687425323025374325323053656c656e69756d2d304139454443" alt="Frameworks"&gt;&lt;/a&gt;
&lt;a href="https://github.com/pankajnayak1994/pyrept#junit-xml-output" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/aa86f0087742ac0a923e0027b16f252d61316e2ffcbdbbe3a28fb7aa38d7cfd3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6f7574707574732d48544d4c2532302537432532304a534f4e2532302537432532304a556e6974253230584d4c2532302537432532304d61726b646f776e2d453334463236" alt="Outputs"&gt;&lt;/a&gt;
&lt;a href="https://github.com/pankajnayak1994/pyrept#converting-other-reports" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5d8bd2ef7f3f34226919490ebbde865c7fa7955348f14c6a313d141b6affda23/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f696d706f7274732d4a556e69742532302537432532305452582532302537432532304e556e6974253230253743253230526f626f74253230253743253230544150253230253743253230416c6c757265253230253743253230437563756d626572253230253743253230506c61797772696768742d364135414344" alt="Imports"&gt;&lt;/a&gt;
&lt;a href="https://github.com/pankajnayak1994/pyrept#using-pyrept-in-ci" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/99a308c837ea33c7c9ae89ae5e94104addc927cb6dacb97c272e3884f5d14129/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f43492d476974487562253230416374696f6e732532302537432532304769744c61622532302537432532304a656e6b696e73253230253743253230417a7572652532304465764f70732d353535" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;pyrept turns a test run into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report.html&lt;/code&gt;&lt;/strong&gt;: one self-contained page you can open offline, attach to a CI run or email. It has no CDN or external assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report.json&lt;/code&gt;&lt;/strong&gt;: the same data in machine-readable form, for dashboards, bots and pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;junit.xml&lt;/code&gt;&lt;/strong&gt; (optional): JUnit XML, so GitLab, Jenkins, Azure DevOps and other CI servers can show results in their test tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Markdown summary&lt;/strong&gt; (optional): for GitHub job summaries, pull request comments and chat.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It works the same way with pytest, unittest, nose2, behave, pytest-bdd and Playwright or Selenium browser tests. It also converts JUnit XML, .NET TRX, NUnit, xUnit.net, Robot Framework, TAP, Allure, Cucumber JSON, Playwright Test JSON and pytest-json-report files, so teams using Java, JavaScript, Go, .NET or Ruby get the same report.&lt;/p&gt;
&lt;p&gt;Every report can compare itself with the previous run and highlight &lt;strong&gt;new&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pankajnayak1994/pyrept" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;Developed and maintained by &lt;strong&gt;Pankaj Kumar Nayak&lt;/strong&gt; (&lt;a href="https://github.com/pankajnayak1994" rel="noopener noreferrer"&gt;@pankajnayak1994&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
