Metadata-Version: 2.4
Name: m00nreport-pytest
Version: 1.0.0
Summary: M00N Report reporter for pytest - real-time test reporting to m00nreport.com or your self-hosted instance
Author: M00N Report
License: MIT
Project-URL: Homepage, https://m00nreport.com
Project-URL: Documentation, https://m00nreport.com/documentation/reporters/pytest
Keywords: pytest,reporting,testing,m00nreport,playwright
Classifier: Framework :: Pytest
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pytest>=7.0
Provides-Extra: playwright
Requires-Dist: pytest-playwright>=0.5; extra == "playwright"
Provides-Extra: dev
Requires-Dist: pytest-rerunfailures>=12.0; extra == "dev"
Requires-Dist: pytest-playwright>=0.5; extra == "dev"
Dynamic: license-file

# m00nreport-pytest

M00N Report reporter for [pytest](https://pytest.org) - real-time test reporting to
[m00nreport.com](https://m00nreport.com) or your self-hosted instance.

## Installation

```bash
pip install m00nreport-pytest
```

That's it. The package registers itself as a pytest plugin via the standard `pytest11`
entry point, so pytest discovers and loads it automatically on every run once it is
installed - there is no `conftest.py` wiring, `-p` flag, or config block to add.

## Quick start

Set two environment variables and run pytest normally:

```bash
export M00N_SERVER_URL=https://m00nreport.com   # or your self-hosted URL
export M00N_API_KEY=m00n_xxxxxxxxxxxxx           # your project API key

pytest
```

On Windows PowerShell:

```powershell
$env:M00N_SERVER_URL = "https://m00nreport.com"
$env:M00N_API_KEY = "m00n_xxxxxxxxxxxxx"
pytest
```

If either variable is missing, the reporter stays inactive and pytest behaves exactly as
it would without it installed - see [Resilience](#resilience).

### Get your API key

1. Log in to [M00N Report](https://m00nreport.com)
2. Open your project settings
3. Generate or copy the project API key

## Configuration

Every option can be set three ways. Precedence, highest wins:
**CLI flag > `pytest.ini` key > environment variable > default.**

| CLI flag | ini key | Env var | Default | Description |
|---|---|---|---|---|
| `--m00n-server-url` | `m00n_server_url` | `M00N_SERVER_URL` | required | M00N Report server URL |
| `--m00n-api-key` | `m00n_api_key` | `M00N_API_KEY` | required | Project API key (`m00n_...`) |
| `--m00n-launch` | `m00n_launch` | `M00N_LAUNCH` | `Run <date>` | Title for this run |
| `--m00n-tags` | `m00n_tags` | `M00N_TAGS` | `[]` | Comma-separated tags |
| `--m00n-attributes` | `m00n_attributes` | `M00N_ATTRIBUTES` | `{}` | JSON object of custom run attributes |
| `--m00n-debug` | `m00n_debug` | `M00N_DEBUG` | `false` | Prints a `[m00nreport]` line to stdout for every HTTP retry attempt and any final exhaustion (`run/start`, `test/complete`, `run/end`, attachment uploads), plus the `attach()`-outside-an-active-test edge case; silent when off |
| `--m00n-disable` | - | - | `false` | Force-disable the reporter for this invocation, even if a server url and api key are set. CLI-only - no ini or env equivalent by design. |

The reporter activates only once both `server_url` and `api_key` resolve to a value. If
exactly one of them is set, pytest prints a single
`[m00nreport] serverUrl and apiKey are required. Reporter disabled.` warning to stderr
and continues (this usually means a typo in an env var name); if neither is set, the
reporter is silently inactive. A malformed `M00N_ATTRIBUTES` JSON string is ignored
(the reporter falls back to `{}`) rather than crashing the run.

`pytest.ini` example:

```ini
[pytest]
m00n_server_url = https://m00nreport.com
m00n_api_key = m00n_xxxxxxxxxxxxx
m00n_launch = Nightly Regression
m00n_tags = smoke, api
```

Or entirely from the CLI:

```bash
pytest --m00n-server-url https://m00nreport.com --m00n-api-key m00n_xxxxxxxxxxxxx --m00n-tags smoke,api
```

## Manual steps

Wrap any block of test code in `step(...)` to record it as a named, timed step. Steps
stream to the dashboard live and nest naturally:

```python
from m00n_reporter import step

def test_login():
    with step("open login page"):
        ...
    with step("submit credentials"):
        with step("fill form"):
            ...
        with step("click submit"):
            ...
```

A step whose body raises is recorded `failed` with the exception captured; a step that
exits normally is recorded `passed`. Any step still open when the test finishes is
recorded `skipped`.

## Attachments

```python
from m00n_reporter import attach

def test_checkout():
    attach("screenshot.png")                     # file path - name/type inferred
    attach(b"raw bytes", name="notes.txt")        # bytes payload - name is required
    attach(trace_path, name="trace.zip", content_type="application/zip")
```

`attach(path_or_bytes, name=None, content_type=None)` accepts either a file path
(`str` / `os.PathLike`) or a `bytes` / `bytearray` payload. Byte payloads must supply
`name`; file paths default `name` to the basename and guess `content_type` from it.
Files under 10 MB upload as buffered multipart; larger files stream instead. Anything
over 200 MB is skipped with a single warning. Uploads run on a background thread pool
(up to 15 concurrent) and `attach()` returns immediately - it never blocks the test.
pytest waits up to 5 minutes for pending uploads to finish before exiting, so
attachments from fast-finishing tests are not silently dropped. Calling `attach()`
outside of an active test, or when the reporter is disabled, is a silent no-op.

## Case linking and tags

```python
import pytest

@pytest.mark.m00n(case_id=42, tags=["regression", "checkout"])
def test_completes_checkout():
    ...
```

`case_id` links the test attempt to an existing M00N Report test case; `tags` adds
per-test tags on top of any run-level tags from configuration.

## Retries

The reporter supports [pytest-rerunfailures](https://pypi.org/project/pytest-rerunfailures/)
out of the box - no extra wiring required:

```bash
pip install pytest-rerunfailures
pytest --reruns 3
```

Each attempt is reported as a distinct entry with a fresh `testId` and an incrementing
zero-based `retry` index (`0`, `1`, `2`, ...), so the dashboard shows the full attempt
history of a flaky test rather than only its final outcome.

## CI auto-detection

The reporter inspects the environment and auto-populates run attributes (`branch`,
`commit`, `pipeline`, `build_number`, `build_url`, `trigger`, `triggered_by`,
`ci_job_url` (GitLab) where available) for these providers, checked in this order
(first match wins):

1. GitHub Actions
2. GitLab CI
3. Jenkins
4. Bitbucket Pipelines
5. Azure DevOps
6. CircleCI
7. Travis CI

Detected attributes are merged with, and overridden by, anything you pass via
`--m00n-attributes` / `m00n_attributes` / `M00N_ATTRIBUTES`.

## Resilience

The reporter follows the same "never fail the run" rule as every M00N Report
integration:

- Every pytest hook is wrapped so a bug or a dead server degrades to a single
  `[m00nreport] ...` warning on stderr - it never turns a green run red, and never
  raises a pytest `INTERNALERROR`.
- `run/start`, `test/complete`, `run/end`, and attachment uploads retry transient 5xx
  responses up to 3 attempts (1 initial + 2 retries) with exponential backoff
  (`min(1.0 * 2^(attempt-1), 5.0)` seconds, capped at 10s for streamed uploads).
  Permanent error codes (invalid API key, project not found, run attachment limit
  exceeded) are never retried.
- A circuit breaker disables the reporter for the rest of the run after 5 consecutive
  `test/complete` calls report a service failure, so a sustained outage does not keep
  retrying every remaining test.

Two deliberate improvements over the JS Playwright reporter:

1. **Smarter health probe.** The startup probe checks `/api/ingest/health` first
   (falling back to `/healthz`) and requires a `200` response with a JSON body of
   `{"ok": true}` - not just any `200`. The probe is advisory only: it never disables
   the reporter when `run/start` itself already succeeded, so a proxy or firewall rule
   that blocks the health endpoint but not the real API cannot take reporting offline.
2. **No dropped `test/start` calls.** Fire-and-forget calls (`test/start` and step
   batches) run through the same retrying background worker as the awaited calls, so a
   single transient 5xx can no longer permanently drop a test's start record while its
   completion still lands.

## Parallel execution (pytest-xdist)

Parallel runs with [pytest-xdist](https://pypi.org/project/pytest-xdist/) (`-n auto`,
`-n 4`, distributed workers) are supported and report as a single run - no extra wiring:

```bash
pip install pytest-xdist
pytest -n 4
```

How it works (master/worker coordination, the same model Allure and ReportPortal use):

- The **controller** (master) process starts the run exactly once and shares its run id
  with every worker before the worker boots, then closes the run once at the end. Its
  final status comes from the controller-aggregated result, so a failure on any worker
  fails the whole run.
- Each **worker** inherits that shared run id, skips its own `run/start`/health probe,
  and reports its slice of tests (`test/start`, steps, `test/complete`, attachments)
  against the shared run. Workers never send `run/end`.

The result is one launch on the dashboard with every worker's tests, retries (retry
`0`, `1`, ... survive across workers), skips, steps, and attachments merged into it -
identical to a serial run, just faster.

Caveat: because the controller does not perform test collection under xdist (the workers
do), the run's up-front `total` test count is omitted for parallel runs; the count fills
in from the tests as they report. Serial (non-xdist) runs still send `total` as before.

## Known limitations

- **Teardown-phase failures are not reported.** If a test's own body passes but a
  fixture's teardown/finalizer raises afterward, that failure is not surfaced to M00N
  Report - the test keeps the status it already reported at `call` time. This is a
  known limitation of this release, not a bug in your fixtures.

## Conformance

This reporter targets the M00N Report ingest v2 contract (public OpenAPI document) and
is verified against the M00N Report reporter-conformance suite: both the normal and
fault-injection (injected transient 5xx) baselines pass, all 11 protocol invariants
green on the normal run and all 12 (the additional `RETRIES_ON_FAILURE` check) green
on the fault-injection run. Unlike the JS Playwright reporter (1.0.10), whose
`test/start` call is fire-and-forget with no retry, this reporter's fire-and-forget
calls run through a retrying background worker and `test/complete` waits for its
matching `test/start` to land before sending, so the fault-injection baseline stays
fully green instead of losing `test/start` records to a single transient 5xx.

## License

MIT License. See [LICENSE](LICENSE).
