Skip to main content

junit-time-diff

junit-time-diff compares test execution times from JUnit XML reports and highlights meaningful slowdowns, speedups, new tests, and removed tests.

It works especially well with pytest's built-in --junit-xml output and can compare either single runs or averages computed from multiple XML files.

Two Overall Comparisons

Every report contains two overall timing comparisons, because they answer different questions:

  • All tests: the time of the whole baseline run against the time of the whole current run. This tells you whether the test suite as a whole became faster or slower, which is what matters for CI wall clock time. It moves whenever tests are added or removed, even if no existing test changed.
  • Common tests: the same comparison restricted to the tests present in both runs. Since existing tests normally keep testing the same thing, this is the number that tells you whether the code itself became faster or slower. Adding or removing tests does not affect it.

The verdict and the exit code are based on the common tests comparison, so that a new slow test does not report a false regression, and deleting a slow test does not hide a real one.

Installation

pip install junit-time-diff

For local development from this repository:

pip install -e .[test]

This installs the junit-time-diff command.

Quick Start

Single Run Comparison

pytest --junit-xml=baseline.xml

# make changes

pytest --junit-xml=current.xml

junit-time-diff baseline.xml current.xml

Averaged Comparison Across Multiple Runs

for i in {1..5}; do pytest --junit-xml=baseline$i.xml; done

# make changes

for i in {1..5}; do pytest --junit-xml=current$i.xml; done

junit-time-diff "baseline*.xml" "current*.xml"

Quoting the glob pattern is recommended so the tool receives the pattern and expands it consistently.

CLI Usage

junit-time-diff --help
usage: junit-time-diff [-h] [--version] [--threshold THRESHOLD]
                       [--min-diff MIN_DIFF]
                       [--overall-threshold OVERALL_THRESHOLD]
                       baseline current

Arguments:

  • baseline: Baseline JUnit XML file or glob pattern such as baseline.xml or baseline*.xml
  • current: Current JUnit XML file or glob pattern such as current.xml or current*.xml

Options:

  • --threshold: Ratio by which a single test must change to be listed, default 1.10. A test is reported as slower at 1.10x its baseline duration and as faster at 1/1.10x
  • --min-diff: Minimum absolute duration change in seconds for a single test to be listed, default 0.01. Both this and --threshold must be exceeded, so tiny fast tests never create noise
  • --overall-threshold: Percentage by which the total time of the tests common to both runs may grow before the command exits with code 1, default 5

Exit Codes

  • 0: no significant regression. Either no test got slower, or the tests common to both runs stayed within --overall-threshold
  • 1: the tests common to both runs are more than --overall-threshold percent slower, or the reports could not be read

A run with no tests in common exits with 0 and prints a warning, since there is nothing to compare.

Example Output

====================================================================================================
TEST TIMING COMPARISON REPORT
====================================================================================================

SUMMARY
----------------------------------------------------------------------------------------------------
Baseline: 9 tests, 3.51s total
Current:  9 tests, 3.69s total
Common tests: 8, New tests: 1, Removed tests: 1

OVERALL TIMING
----------------------------------------------------------------------------------------------------
Scope                                                          Before      After       Diff   Change
----------------------------------------------------------------------------------------------------
All tests (9 -> 9)                                              3.51s      3.69s     +0.18s    +5.1%
Common tests (8)                                                3.51s      3.69s     +0.18s    +5.0%
The all-tests change is the common-tests change (+0.176s) plus new tests (+0.004s) minus removed tests (0.000s).

SLOWER TESTS (2 tests)
----------------------------------------------------------------------------------------------------
Test                                                           Before      After       Diff   Change
----------------------------------------------------------------------------------------------------
tests.test_complex::test_complex_four                          1.108s     1.422s    +0.314s   +28.3%
tests.test_complex::test_complex_one                           1.527s     1.694s    +0.167s   +10.9%

====================================================================================================
VERDICT
----------------------------------------------------------------------------------------------------
⚠ WARNING: Tests common to both runs are slower: 3.51s -> 3.69s (+5.0%)!
  2 tests got slower (per-test threshold: 10%)
✓ 1 test got faster!
ℹ The whole test suite changed by +5.1%, new and removed tests included
====================================================================================================

The full report also lists faster tests, new tests, and removed tests, each sorted by duration.

Typical Workflows

Compare Python Versions

python3.11 -m pytest --junit-xml=py311.xml
python3.12 -m pytest --junit-xml=py312.xml

junit-time-diff py311.xml py312.xml

Compare a Specific Test Subset

pytest tests/test_api.py --junit-xml=baseline_api.xml

# make changes

pytest tests/test_api.py --junit-xml=current_api.xml

junit-time-diff baseline_api.xml current_api.xml

Use a Stricter Threshold

junit-time-diff baseline.xml current.xml --threshold 1.05 --overall-threshold 2

Fail a CI Job on a Regression

# Exits with 1 when the tests present in both runs are more than 5% slower.
junit-time-diff "baseline*.xml" "current*.xml"

Tips

  • Run several repetitions and compare averages to reduce noise.
  • Compare results on the same machine when possible.
  • Keep the selected test set consistent between baseline and current runs.
  • Ignore tiny changes unless they are part of a repeated pattern.
  • Read the common-tests comparison to judge the code, and the all-tests comparison to judge the CI time.

Development

Build the package:

python3 -m build

Run tests:

pytest

Metadata

Release files for junit-time-diff 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for junit-time-diff 0.2.0
File Size Uploaded
junit_time_diff-0.2.0.tar.gz 12.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for junit-time-diff 0.2.0
File Interpreter ABI Platform
junit_time_diff-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 22.5 kB

Release files / junit_time_diff-0.2.0.tar.gz

Download URL junit_time_diff-0.2.0.tar.gz
Size 12.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c462b4fc7901432048785942fbc9546c31cbe38ee63fa5dee175c2cbbb44dd87
BLAKE2b-256 checksum
How to use checksums
f2480587b8b15cda6b624330b4efd79a948f358385690bc9fe0f4012d88d927f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release files / junit_time_diff-0.2.0-py3-none-any.whl

Download URL junit_time_diff-0.2.0-py3-none-any.whl
Size 10.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
67d1ef19e78404aed78bf5e08e4ea3cee96366f367082f46c87d891a1ba09e2a
BLAKE2b-256 checksum
How to use checksums
1e3af3655e493a1bb95712293bb40ffe4e2462efe446913af7125d80a064b030
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page