pytest-nijam 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_nijam-0.1.0/.github/workflows/ci.yml +41 -0
- pytest_nijam-0.1.0/.github/workflows/publish.yml +113 -0
- pytest_nijam-0.1.0/.gitignore +12 -0
- pytest_nijam-0.1.0/CLAUDE.md +82 -0
- pytest_nijam-0.1.0/LICENSE +21 -0
- pytest_nijam-0.1.0/PKG-INFO +103 -0
- pytest_nijam-0.1.0/README.md +80 -0
- pytest_nijam-0.1.0/pyproject.toml +50 -0
- pytest_nijam-0.1.0/src/nijam_pytest/__init__.py +3 -0
- pytest_nijam-0.1.0/src/nijam_pytest/buffer.py +38 -0
- pytest_nijam-0.1.0/src/nijam_pytest/ci.py +249 -0
- pytest_nijam-0.1.0/src/nijam_pytest/client.py +83 -0
- pytest_nijam-0.1.0/src/nijam_pytest/log.py +27 -0
- pytest_nijam-0.1.0/src/nijam_pytest/models.py +139 -0
- pytest_nijam-0.1.0/src/nijam_pytest/plugin.py +331 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Cancel superseded runs on the same ref.
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: lint + types + build
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
# mypy/ruff run on a modern Python; the package itself targets 3.8+ (ruff's
|
|
21
|
+
# target-version in pyproject enforces 3.8-compatible syntax).
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: '3.12'
|
|
25
|
+
cache: pip
|
|
26
|
+
|
|
27
|
+
- name: Install (with dev extras)
|
|
28
|
+
run: pip install -e '.[dev]'
|
|
29
|
+
|
|
30
|
+
- name: Ruff
|
|
31
|
+
run: ruff check src
|
|
32
|
+
|
|
33
|
+
- name: Mypy
|
|
34
|
+
run: mypy
|
|
35
|
+
|
|
36
|
+
# Catch packaging errors (entry point, metadata) on every PR.
|
|
37
|
+
- name: Build sanity check
|
|
38
|
+
run: |
|
|
39
|
+
pip install build twine
|
|
40
|
+
python -m build
|
|
41
|
+
twine check dist/*
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# One-click release for pytest-nijam:
|
|
4
|
+
# set version (in pyproject + __init__) → verify → guard → build → publish to PyPI
|
|
5
|
+
# via Trusted Publishing (OIDC, no token) → commit + tag (vX.Y.Z) → push.
|
|
6
|
+
# Run it from the Actions tab: "Publish" → "Run workflow", enter the new version.
|
|
7
|
+
#
|
|
8
|
+
# No secret needed. Set up Trusted Publishing once on PyPI BEFORE the first run:
|
|
9
|
+
# PyPI → (project, or "Your projects → Publishing" for a *pending* publisher) →
|
|
10
|
+
# Add a publisher: owner = getnijam, repo = pytest-reporter,
|
|
11
|
+
# workflow = publish.yml, environment = pypi.
|
|
12
|
+
# A pending publisher lets the first run create the project. Also create a GitHub
|
|
13
|
+
# environment named "pypi" (Settings → Environments) — optionally gated by reviewers.
|
|
14
|
+
on:
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
inputs:
|
|
17
|
+
version:
|
|
18
|
+
description: 'New version (PEP 440 — e.g. 0.1.0a2, 0.1.0, 1.0.0)'
|
|
19
|
+
type: string
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: write # push the version-bump commit + tag
|
|
24
|
+
id-token: write # PyPI Trusted Publishing (OIDC)
|
|
25
|
+
|
|
26
|
+
# Never let two publishes overlap.
|
|
27
|
+
concurrency:
|
|
28
|
+
group: publish
|
|
29
|
+
cancel-in-progress: false
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
publish:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
# Must match the environment named in the PyPI trusted-publisher config.
|
|
35
|
+
environment: pypi
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
with:
|
|
39
|
+
fetch-depth: 0 # full history so the bump commit + tag push cleanly
|
|
40
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: '3.12'
|
|
45
|
+
cache: pip
|
|
46
|
+
|
|
47
|
+
# Validate BEFORE bumping/publishing anything.
|
|
48
|
+
- name: Validate (lint + types)
|
|
49
|
+
run: |
|
|
50
|
+
pip install -e '.[dev]'
|
|
51
|
+
ruff check src
|
|
52
|
+
mypy
|
|
53
|
+
|
|
54
|
+
# Write the requested version into pyproject.toml + __init__ (kept in sync).
|
|
55
|
+
- name: Set version
|
|
56
|
+
env:
|
|
57
|
+
VERSION: ${{ inputs.version }}
|
|
58
|
+
run: |
|
|
59
|
+
python - <<'PY'
|
|
60
|
+
import os, re, pathlib
|
|
61
|
+
v = os.environ["VERSION"].strip()
|
|
62
|
+
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+([abrc][0-9]+|rc[0-9]+|\.dev[0-9]+|\.post[0-9]+)?", v):
|
|
63
|
+
raise SystemExit(f"::error::'{v}' is not a PEP 440 version")
|
|
64
|
+
targets = [
|
|
65
|
+
("pyproject.toml", r'(?m)^version = ".*"$', f'version = "{v}"'),
|
|
66
|
+
("src/nijam_pytest/__init__.py", r'(?m)^__version__ = ".*"$', f'__version__ = "{v}"'),
|
|
67
|
+
]
|
|
68
|
+
for path, pat, repl in targets:
|
|
69
|
+
p = pathlib.Path(path)
|
|
70
|
+
s = p.read_text()
|
|
71
|
+
s2, n = re.subn(pat, repl, s, count=1)
|
|
72
|
+
if n != 1:
|
|
73
|
+
raise SystemExit(f"::error::could not set version in {path}")
|
|
74
|
+
p.write_text(s2)
|
|
75
|
+
print(f"version set to {v}")
|
|
76
|
+
PY
|
|
77
|
+
|
|
78
|
+
# Guard: this version must not already be published (PyPI rejects re-uploads).
|
|
79
|
+
- name: Guard — version not already on PyPI
|
|
80
|
+
env:
|
|
81
|
+
VERSION: ${{ inputs.version }}
|
|
82
|
+
run: |
|
|
83
|
+
code=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/pytest-nijam/$VERSION/json")
|
|
84
|
+
if [ "$code" = "200" ]; then
|
|
85
|
+
echo "::error::pytest-nijam $VERSION is already on PyPI — bump again."
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
echo "OK — $VERSION is not on PyPI yet (HTTP $code)."
|
|
89
|
+
|
|
90
|
+
- name: Build
|
|
91
|
+
run: |
|
|
92
|
+
pip install build twine
|
|
93
|
+
python -m build
|
|
94
|
+
twine check dist/*
|
|
95
|
+
|
|
96
|
+
# Trusted Publishing: no password — authenticates via OIDC (id-token: write)
|
|
97
|
+
# against the PyPI publisher you configured for this repo + environment.
|
|
98
|
+
- name: Publish to PyPI
|
|
99
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
100
|
+
|
|
101
|
+
# Record the release in git only AFTER PyPI accepted it (publish is the one
|
|
102
|
+
# irreversible step). Pushes to the branch the workflow was dispatched from.
|
|
103
|
+
- name: Commit + tag the release
|
|
104
|
+
env:
|
|
105
|
+
VERSION: ${{ inputs.version }}
|
|
106
|
+
BRANCH: ${{ github.ref_name }}
|
|
107
|
+
run: |
|
|
108
|
+
git config user.name 'github-actions[bot]'
|
|
109
|
+
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
|
110
|
+
git commit -am "release: v$VERSION"
|
|
111
|
+
git tag "v$VERSION"
|
|
112
|
+
git push origin "HEAD:$BRANCH"
|
|
113
|
+
git push origin "v$VERSION"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# pytest-nijam — Claude instructions
|
|
2
|
+
|
|
3
|
+
pytest plugin for **Nijam**. Implements pytest's hooks to capture a test run and ship it
|
|
4
|
+
to the Nijam API. It is the Python sibling of `@nijam/pw-reporter` and reports into the
|
|
5
|
+
**same** ingestion endpoints (`/v1/runs`, `…/executions`, `…/source`, `PATCH /v1/runs/:id`).
|
|
6
|
+
|
|
7
|
+
**This is a public-facing artifact** — installed via `pip`, read on GitHub, pasted into
|
|
8
|
+
users' `pytest.ini`. Code quality, **zero runtime dependencies**, and a copy-paste-runnable
|
|
9
|
+
README matter more here than anywhere else.
|
|
10
|
+
|
|
11
|
+
License: **MIT** (separate from the BSL platform — must be maximally adoptable).
|
|
12
|
+
|
|
13
|
+
> This is an independent repo (`getnijam/pytest-reporter`), not part of a monorepo. It
|
|
14
|
+
> shares nothing with the other repos except the API's wire format.
|
|
15
|
+
|
|
16
|
+
## The one big difference from pw-reporter
|
|
17
|
+
**pytest has no traces.** There is no artifact upload path at all (no trace / screenshot /
|
|
18
|
+
video). We capture the error log (`longrepr`), the failing line, durations, run stats, and
|
|
19
|
+
(opt-in) the test file source — everything the dashboard needs minus the trace viewer.
|
|
20
|
+
Projects created as `pytest` in the dashboard hide trace UI accordingly.
|
|
21
|
+
|
|
22
|
+
## Stack (locked — ask before changing the public option shape)
|
|
23
|
+
- Python, `>=3.8`. `from __future__ import annotations` everywhere (3.8-safe typing).
|
|
24
|
+
- **Zero runtime dependencies.** `pytest>=7` is the host. HTTP is **stdlib `urllib`** only
|
|
25
|
+
(no `requests`/`httpx`).
|
|
26
|
+
- Build: **hatchling**, src-layout. Auto-loads via the `pytest11` entry point in `pyproject.toml`.
|
|
27
|
+
- Tooling: `mypy --strict` and `ruff` must stay clean. No automated test suite in v0.1
|
|
28
|
+
(smoke-test by `pip install -e .` into a sample suite).
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
```
|
|
32
|
+
src/nijam_pytest/
|
|
33
|
+
plugin.py # pytest hooks (addoption/configure/sessionstart/logreport/sessionfinish)
|
|
34
|
+
client.py # NijamClient — HTTP to the API (urllib, soft-fail)
|
|
35
|
+
ci.py # detect_run_context / relative_file — CI/git metadata (port of pw-reporter ci.ts)
|
|
36
|
+
buffer.py # ExecutionBuffer — collect during run, flush in chunks at session finish
|
|
37
|
+
models.py # payload dataclasses (RunContext, TestExecution, FinalizeRunPayload, …)
|
|
38
|
+
log.py # [nijam]-prefixed warn/info
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Public config surface (design backward from this)
|
|
42
|
+
ini options in `pytest.ini` / `[tool.pytest.ini_options]`, each overridable by env (env wins):
|
|
43
|
+
`nijam_api_key` (`NIJAM_API_KEY`), `nijam_project_id` (`NIJAM_PROJECT_ID`),
|
|
44
|
+
`nijam_api_url` (`NIJAM_API_URL`), `nijam_environment` (`NIJAM_ENVIRONMENT`),
|
|
45
|
+
`nijam_upload_source` (bool, default true), `nijam_auto_complete` (bool, default true,
|
|
46
|
+
`NIJAM_AUTO_COMPLETE`), `nijam_silent` (bool). Missing key/project → warn with the docs
|
|
47
|
+
link + disable; no further work. Don't change these names/shape without asking.
|
|
48
|
+
|
|
49
|
+
## Lifecycle & behavior
|
|
50
|
+
- `pytest_configure` → register one `NijamPlugin`.
|
|
51
|
+
- `pytest_sessionstart` → `detect_run_context` + `POST /v1/runs`, store `run_id`; on
|
|
52
|
+
failure log + disable for this run.
|
|
53
|
+
- `pytest_runtest_logreport` → accumulate per-attempt state keyed by nodeid (setup → call
|
|
54
|
+
→ teardown), emit **one execution per attempt** on the teardown report. Never block on I/O.
|
|
55
|
+
- `pytest_sessionfinish` → drain the buffer, optionally upload sources, then
|
|
56
|
+
`PATCH /v1/runs/:id` to finalize with status + stats (unless `auto_complete` is off).
|
|
57
|
+
- **Field mapping**: `testId`=nodeid, `title`=last `::` segment, `titlePath`=`::`-split minus
|
|
58
|
+
the file, `file`=git-root-relative (else rootdir-relative) of `report.location[0]`,
|
|
59
|
+
`line`=`report.location[1] + 1` (pytest is 0-based, the API is 1-based), `errorMessage`=
|
|
60
|
+
`longreprtext`, `durationMs`=summed phase durations, `id`=client `uuid4`.
|
|
61
|
+
- **Status**: failed > skipped > passed precedence across phases. `xfail` lands as skipped.
|
|
62
|
+
`pytest-rerunfailures` reruns (outcome `rerun`) emit extra attempts with bumped `retry`,
|
|
63
|
+
which is how flaky is derived (a test that passed only after a retry). No reruns ⇒ no flaky.
|
|
64
|
+
- **CI detection** (`ci.py`): per-field `CI var → generic GIT_* → git shell-out → empty`.
|
|
65
|
+
GitHub/GitLab/CircleCI/Bitbucket/generic. Leave `branch` None when unknown (dashboard
|
|
66
|
+
renders "No Branch Info"). Same env-var names as pw-reporter's `ci.ts`.
|
|
67
|
+
- **HTTP**: Bearer `api_key`, 30s timeout, no retries, 402 → "plan limit reached" warning.
|
|
68
|
+
|
|
69
|
+
## Guard rails — do NOT
|
|
70
|
+
- ❌ **Raise from any hook** — wrap every hook body in try/except, `log.warn`, continue.
|
|
71
|
+
The plugin MUST NOT break a user's test run. Ever.
|
|
72
|
+
- ❌ Add runtime dependencies (zero-dep goal) · use `requests`/`httpx` (stdlib `urllib` only).
|
|
73
|
+
- ❌ Block the test path (`logreport`) on the network — only append to the buffer; flush at finish.
|
|
74
|
+
- ❌ Add a trace/artifact upload path — pytest has no traces.
|
|
75
|
+
- ❌ Use Python-3.10-only syntax in runtime code without `from __future__ import annotations`.
|
|
76
|
+
- ❌ Change the public option names/shape, or the API wire format, without asking.
|
|
77
|
+
- ❌ Ternary hell / one-liners that hurt readability — prefer early returns and `if`/`else`.
|
|
78
|
+
|
|
79
|
+
## Build & publish
|
|
80
|
+
- `pip install -e '.[dev]'` · `mypy` · `ruff check src` · smoke-test into a sample suite.
|
|
81
|
+
- Versions: `0.1.0aN` until platform launch, then `0.1.0`; semver after. Bump the alpha on
|
|
82
|
+
each meaningful change. Publish: `python -m build && twine upload dist/*`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nijam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-nijam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pytest plugin for Nijam — captures test runs and ships them to the Nijam API.
|
|
5
|
+
Project-URL: Homepage, https://nijam.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.nijam.dev
|
|
7
|
+
Project-URL: Source, https://github.com/getnijam/pytest-reporter
|
|
8
|
+
Author: Nijam
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ci,nijam,pytest,reporting,test-analytics
|
|
12
|
+
Classifier: Framework :: Pytest
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Requires-Dist: pytest>=7.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pytest-nijam
|
|
25
|
+
|
|
26
|
+
pytest plugin for [Nijam](https://nijam.dev) — captures your test runs and ships them
|
|
27
|
+
to the Nijam dashboard so you can track what failed, why, and where (error log +
|
|
28
|
+
failing line), across CI runs and over time.
|
|
29
|
+
|
|
30
|
+
> pytest has no traces, so — unlike the Playwright reporter — runs won't include a
|
|
31
|
+
> trace viewer. Everything else (failures, error output, the failing line, durations,
|
|
32
|
+
> and your test source) is captured.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pytest-nijam
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The plugin auto-activates once installed (via pytest's `pytest11` entry point).
|
|
41
|
+
|
|
42
|
+
## Configure
|
|
43
|
+
|
|
44
|
+
Add your project ID to `pytest.ini` (or `pyproject.toml`), and provide the ingest API
|
|
45
|
+
key via an environment variable (it's a secret — keep it out of source control):
|
|
46
|
+
|
|
47
|
+
```ini
|
|
48
|
+
# pytest.ini
|
|
49
|
+
[pytest]
|
|
50
|
+
nijam_project_id = 00000000-0000-0000-0000-000000000000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
# pyproject.toml
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
nijam_project_id = "00000000-0000-0000-0000-000000000000"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export NIJAM_API_KEY="nij_sk_…" # from the Nijam dashboard → Secret keys
|
|
61
|
+
pytest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both the project ID and the API key can come from either the ini file or an
|
|
65
|
+
environment variable; **the environment variable wins** when both are set.
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
|
|
69
|
+
| ini option | env var | default | what it does |
|
|
70
|
+
| --------------------- | --------------------- | ------------------------ | ------------------------------------------------------------------- |
|
|
71
|
+
| `nijam_api_key` | `NIJAM_API_KEY` | — | Ingest API key (required). |
|
|
72
|
+
| `nijam_project_id` | `NIJAM_PROJECT_ID` | — | Project UUID (required). |
|
|
73
|
+
| `nijam_api_url` | `NIJAM_API_URL` | `https://api.nijam.dev` | API base URL. |
|
|
74
|
+
| `nijam_environment` | `NIJAM_ENVIRONMENT` | — | Free-form environment tag (e.g. `staging`). |
|
|
75
|
+
| `nijam_upload_source` | — | `true` | Upload each test file's source so the dashboard can show it. |
|
|
76
|
+
| `nijam_auto_complete` | `NIJAM_AUTO_COMPLETE` | `true` | Finalize the run when this process ends. Set `false` for fan-out. |
|
|
77
|
+
| `nijam_silent` | — | `false` | Suppress `[nijam]` log lines. |
|
|
78
|
+
|
|
79
|
+
If `nijam_api_key` or `nijam_project_id` is missing, the plugin disables itself with a
|
|
80
|
+
single warning — your tests run exactly as before.
|
|
81
|
+
|
|
82
|
+
## CI metadata
|
|
83
|
+
|
|
84
|
+
Run context (commit, branch, PR number, CI provider/run URL, commit author) is detected
|
|
85
|
+
automatically from GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines, or generic
|
|
86
|
+
`GIT_*` env vars, falling back to `git` itself. No configuration needed.
|
|
87
|
+
|
|
88
|
+
## Fan-out across CI jobs
|
|
89
|
+
|
|
90
|
+
If you split your suite across several CI jobs (e.g. one job per test path) that all feed
|
|
91
|
+
one Nijam run, set `nijam_auto_complete = false` (or `NIJAM_AUTO_COMPLETE=false`) on each
|
|
92
|
+
job so none of them finalizes early, then complete the run once from a post-matrix step.
|
|
93
|
+
See the [docs](https://docs.nijam.dev/reporter/pytest/).
|
|
94
|
+
|
|
95
|
+
## Guarantees
|
|
96
|
+
|
|
97
|
+
This plugin **never breaks your test run.** Every hook is fail-soft: a network error, a
|
|
98
|
+
bad key, or an unreachable API produces a `[nijam]` warning and nothing more — your tests
|
|
99
|
+
still pass or fail on their own merits.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# pytest-nijam
|
|
2
|
+
|
|
3
|
+
pytest plugin for [Nijam](https://nijam.dev) — captures your test runs and ships them
|
|
4
|
+
to the Nijam dashboard so you can track what failed, why, and where (error log +
|
|
5
|
+
failing line), across CI runs and over time.
|
|
6
|
+
|
|
7
|
+
> pytest has no traces, so — unlike the Playwright reporter — runs won't include a
|
|
8
|
+
> trace viewer. Everything else (failures, error output, the failing line, durations,
|
|
9
|
+
> and your test source) is captured.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install pytest-nijam
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The plugin auto-activates once installed (via pytest's `pytest11` entry point).
|
|
18
|
+
|
|
19
|
+
## Configure
|
|
20
|
+
|
|
21
|
+
Add your project ID to `pytest.ini` (or `pyproject.toml`), and provide the ingest API
|
|
22
|
+
key via an environment variable (it's a secret — keep it out of source control):
|
|
23
|
+
|
|
24
|
+
```ini
|
|
25
|
+
# pytest.ini
|
|
26
|
+
[pytest]
|
|
27
|
+
nijam_project_id = 00000000-0000-0000-0000-000000000000
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
# pyproject.toml
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
nijam_project_id = "00000000-0000-0000-0000-000000000000"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export NIJAM_API_KEY="nij_sk_…" # from the Nijam dashboard → Secret keys
|
|
38
|
+
pytest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Both the project ID and the API key can come from either the ini file or an
|
|
42
|
+
environment variable; **the environment variable wins** when both are set.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
| ini option | env var | default | what it does |
|
|
47
|
+
| --------------------- | --------------------- | ------------------------ | ------------------------------------------------------------------- |
|
|
48
|
+
| `nijam_api_key` | `NIJAM_API_KEY` | — | Ingest API key (required). |
|
|
49
|
+
| `nijam_project_id` | `NIJAM_PROJECT_ID` | — | Project UUID (required). |
|
|
50
|
+
| `nijam_api_url` | `NIJAM_API_URL` | `https://api.nijam.dev` | API base URL. |
|
|
51
|
+
| `nijam_environment` | `NIJAM_ENVIRONMENT` | — | Free-form environment tag (e.g. `staging`). |
|
|
52
|
+
| `nijam_upload_source` | — | `true` | Upload each test file's source so the dashboard can show it. |
|
|
53
|
+
| `nijam_auto_complete` | `NIJAM_AUTO_COMPLETE` | `true` | Finalize the run when this process ends. Set `false` for fan-out. |
|
|
54
|
+
| `nijam_silent` | — | `false` | Suppress `[nijam]` log lines. |
|
|
55
|
+
|
|
56
|
+
If `nijam_api_key` or `nijam_project_id` is missing, the plugin disables itself with a
|
|
57
|
+
single warning — your tests run exactly as before.
|
|
58
|
+
|
|
59
|
+
## CI metadata
|
|
60
|
+
|
|
61
|
+
Run context (commit, branch, PR number, CI provider/run URL, commit author) is detected
|
|
62
|
+
automatically from GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines, or generic
|
|
63
|
+
`GIT_*` env vars, falling back to `git` itself. No configuration needed.
|
|
64
|
+
|
|
65
|
+
## Fan-out across CI jobs
|
|
66
|
+
|
|
67
|
+
If you split your suite across several CI jobs (e.g. one job per test path) that all feed
|
|
68
|
+
one Nijam run, set `nijam_auto_complete = false` (or `NIJAM_AUTO_COMPLETE=false`) on each
|
|
69
|
+
job so none of them finalizes early, then complete the run once from a post-matrix step.
|
|
70
|
+
See the [docs](https://docs.nijam.dev/reporter/pytest/).
|
|
71
|
+
|
|
72
|
+
## Guarantees
|
|
73
|
+
|
|
74
|
+
This plugin **never breaks your test run.** Every hook is fail-soft: a network error, a
|
|
75
|
+
bad key, or an unreachable API produces a `[nijam]` warning and nothing more — your tests
|
|
76
|
+
still pass or fail on their own merits.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-nijam"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "pytest plugin for Nijam — captures test runs and ships them to the Nijam API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{ name = "Nijam" }]
|
|
13
|
+
keywords = ["pytest", "nijam", "test-analytics", "reporting", "ci"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Framework :: Pytest",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
# Zero runtime dependencies. pytest is the host — declared so resolvers know the
|
|
22
|
+
# minimum, but the plugin only ever loads inside a pytest process anyway.
|
|
23
|
+
dependencies = ["pytest>=7.0"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://nijam.dev"
|
|
27
|
+
Documentation = "https://docs.nijam.dev"
|
|
28
|
+
Source = "https://github.com/getnijam/pytest-reporter"
|
|
29
|
+
|
|
30
|
+
# This entry point is what makes pytest auto-discover and load the plugin.
|
|
31
|
+
[project.entry-points.pytest11]
|
|
32
|
+
nijam = "nijam_pytest.plugin"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["mypy>=1.8", "ruff>=0.5"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/nijam_pytest"]
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
strict = true
|
|
42
|
+
files = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
target-version = "py38"
|
|
47
|
+
src = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Batches test executions and flushes them in chunks.
|
|
2
|
+
|
|
3
|
+
Unlike the Playwright reporter (which fire-and-forgets over the network on a worker
|
|
4
|
+
thread), this buffer only *appends* during the test run — it never blocks the test
|
|
5
|
+
path on I/O. The accumulated executions are flushed in `FLUSH_SIZE` chunks at
|
|
6
|
+
`drain()` time (session finish), which keeps the hot path allocation-only and avoids
|
|
7
|
+
threading complexity. pytest sessions are short-lived, so a single end-of-run flush
|
|
8
|
+
is acceptable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
from . import log
|
|
16
|
+
from .models import TestExecution
|
|
17
|
+
|
|
18
|
+
FLUSH_SIZE = 50
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExecutionBuffer:
|
|
22
|
+
def __init__(self, flush_fn: Callable[[list[TestExecution]], None]) -> None:
|
|
23
|
+
self._flush_fn = flush_fn
|
|
24
|
+
self._items: list[TestExecution] = []
|
|
25
|
+
|
|
26
|
+
def add(self, item: TestExecution) -> None:
|
|
27
|
+
self._items.append(item)
|
|
28
|
+
|
|
29
|
+
def drain(self) -> None:
|
|
30
|
+
"""Send everything collected, in FLUSH_SIZE chunks. A failed chunk drops that
|
|
31
|
+
batch with a warning (the flush fn soft-fails) and we continue with the rest."""
|
|
32
|
+
items, self._items = self._items, []
|
|
33
|
+
for start in range(0, len(items), FLUSH_SIZE):
|
|
34
|
+
batch = items[start : start + FLUSH_SIZE]
|
|
35
|
+
try:
|
|
36
|
+
self._flush_fn(batch)
|
|
37
|
+
except Exception as err:
|
|
38
|
+
log.warn(f"dropped a batch of {len(batch)} executions: {err}")
|