Skip to content

Commit 25ed2a1

Browse files
Python(chore): pytest docs reorganization (#589)
1 parent 2e4c9cf commit 25ed2a1

10 files changed

Lines changed: 957 additions & 826 deletions

File tree

python/docs/examples/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ This section contains interactive Jupyter notebook examples demonstrating how to
66

77
- **[Basic Usage](basic.ipynb)** - Introduction to the Sift Python client, covering basic operations and API usage
88
- **[Data Ingestion](ingestion.ipynb)** - Learn how to ingest telemetry data into Sift using various methods
9-
- **[Pytest Plugin](pytest_plugin.md)** - Turn a pytest run into a Sift TestReport with measurements, nested steps, and pass/fail outcomes
109
- **[Pytest Plugin Quickstart](pytest_plugin_quickstart.md)** - Guided tour of the runnable demo project under `python/examples/pytest_plugin/`
1110

11+
For the conceptual reference on the pytest plugin (fixtures, configuration,
12+
report structure, and pass/fail behavior), see the
13+
[Pytest Plugin guide](../guides/pytest_plugin/index.md).
14+
1215
## Running Examples Locally
1316

1417
To run these examples on your local machine:

python/docs/examples/pytest_plugin.md

Lines changed: 10 additions & 818 deletions
Large diffs are not rendered by default.

python/docs/examples/pytest_plugin_quickstart.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ axes, manual substeps, and gate markers. It also includes a tests directory
88
that uses no Sift APIs at all, to show how the autouse fixtures capture plain
99
pytest tests for free.
1010

11-
For a conceptual reference (fixtures, ini flags, status semantics), see
12-
[Pytest Plugin](pytest_plugin.md).
11+
For a conceptual reference (fixtures, ini flags, status semantics), see the
12+
[Pytest Plugin guide](../guides/pytest_plugin/index.md).
1313

1414
## Project layout
1515

@@ -172,7 +172,7 @@ Flip any of the `sift_*_step` / `sift_parametrize_nesting` flags in
172172

173173
## Next steps
174174

175-
- [Pytest Plugin](pytest_plugin.md): conceptual reference covering fixtures,
176-
ini flags, status semantics, and layout-mapping examples.
175+
- [Pytest Plugin guide](../guides/pytest_plugin/index.md): conceptual reference
176+
covering fixtures, configuration, report structure, and pass/fail behavior.
177177
- The demo's [README](https://github.com/sift-stack/sift/blob/main/python/examples/pytest_plugin/README.md)
178178
on GitHub mirrors this page and is the canonical source.

python/docs/guides/index.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Guides
2+
3+
Conceptual references for the Sift Python client. Guides explain how a feature
4+
works and how to configure it. For runnable, end-to-end walkthroughs see the
5+
[Examples](../examples/index.md) section.
6+
7+
## Available guides
8+
9+
- [Pytest Plugin](pytest_plugin/index.md): turn a pytest run into a `TestReport`
10+
in Sift. Each test becomes a `TestStep`, measurements are recorded as rows, and
11+
failures propagate up through nested substeps to the report.
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Configuration & Defaults
2+
3+
This page is the full reference for everything the plugin exposes: fixtures, CLI
4+
flags, ini options, credential handling, and the markers that control which
5+
tests report.
6+
7+
!!! info "Where the plugin lives"
8+
The plugin lives at `sift_client.pytest_plugin`. It is **not** registered as
9+
a `pytest11` entry point. Projects opt in with a `pytest_plugins` declaration
10+
in their top-level `conftest.py`. Pytest then loads the module as a real
11+
plugin: the fixtures, CLI options, and `pytest_runtest_makereport` hook all
12+
register through standard pytest machinery, so `pytest --trace-config` lists
13+
it and `pytest -p no:sift_client.pytest_plugin` disables it.
14+
15+
## Credentials
16+
17+
Set the connection details in a `.env` next to your tests:
18+
19+
```bash
20+
SIFT_API_KEY="your-api-key"
21+
SIFT_GRPC_URI="..."
22+
SIFT_REST_URI="..."
23+
```
24+
25+
The `SIFT_GRPC_URI` and `SIFT_REST_URI` are the gRPC and REST endpoints for your
26+
Sift organization. You can find these on the Sift Manage page as well as
27+
generate an API key.
28+
29+
The default `sift_client` fixture reads its two URIs from environment first and
30+
falls back to ini keys when the env vars are unset. `SIFT_API_KEY` is
31+
intentionally env-only, so keep it out of source control and supply it through
32+
`pytest-dotenv` (see [API key handling](#api-key-handling) below). The env var
33+
wins when both are set, so secrets injected into a CI environment continue to
34+
override values committed to `pyproject.toml`. There are no CLI flags for
35+
credentials.
36+
37+
| Ini key | Environment variable | Notes |
38+
|---|---|---|
39+
| _(none)_ | `SIFT_API_KEY` | Env-only. Use `.env` + `pytest-dotenv` locally; inject from your secret store in CI. |
40+
| `sift_grpc_uri` | `SIFT_GRPC_URI` | Stable per-org gRPC endpoint; safe to commit. |
41+
| `sift_rest_uri` | `SIFT_REST_URI` | Stable per-org REST endpoint; safe to commit. |
42+
43+
### API key handling
44+
45+
`SIFT_API_KEY` is deliberately read from the process environment only. The
46+
recommended workflow uses the
47+
[`pytest-dotenv`](https://pypi.org/project/pytest-dotenv/) plugin (already a
48+
dependency of `sift-stack-py`), which loads variables from a `.env` file into
49+
`os.environ` before tests run.
50+
51+
1. Add `.env` to `.gitignore`.
52+
2. Drop your key into `.env` at the project root:
53+
54+
```bash title=".env"
55+
SIFT_API_KEY=sk-...your-key...
56+
```
57+
58+
3. In CI, set `SIFT_API_KEY` directly via your provider's secret manager
59+
instead of committing a `.env` file.
60+
61+
`pytest-dotenv` picks the file up automatically; no `pytest_configure` glue is
62+
needed.
63+
64+
!!! warning "FedRAMP / shared environments"
65+
Pass `--sift-log-file=false` (or set the ini key to `"false"`) to skip the
66+
temp file + worker pipeline. Create/update calls then run inline against the
67+
API instead of being deferred through a subprocess.
68+
69+
## Wire the plugin into `conftest.py`
70+
71+
A single `pytest_plugins` declaration in your top-level `conftest.py` is all
72+
that's required. The plugin ships a default `sift_client` fixture that reads
73+
`SIFT_API_KEY`, `SIFT_GRPC_URI`, and `SIFT_REST_URI` from the environment.
74+
75+
```python title="conftest.py"
76+
from dotenv import load_dotenv
77+
78+
load_dotenv()
79+
80+
pytest_plugins = ["sift_client.pytest_plugin"]
81+
```
82+
83+
That's the whole setup. Every test in the session will now create a step on a
84+
single shared `TestReport`.
85+
86+
### Customizing the `SiftClient`
87+
88+
To construct the client differently (custom TLS, timeouts, alternate
89+
credentials, etc.), override the `sift_client` fixture in your conftest. The
90+
plugin's default falls away in favor of your definition.
91+
92+
```python title="conftest.py"
93+
import os
94+
95+
import pytest
96+
from dotenv import load_dotenv
97+
98+
from sift_client import SiftClient, SiftConnectionConfig
99+
100+
load_dotenv()
101+
102+
pytest_plugins = ["sift_client.pytest_plugin"]
103+
104+
105+
@pytest.fixture(scope="session")
106+
def sift_client() -> SiftClient:
107+
return SiftClient(
108+
connection_config=SiftConnectionConfig(
109+
api_key=os.getenv("SIFT_API_KEY"),
110+
grpc_url=os.getenv("SIFT_GRPC_URI"),
111+
rest_url=os.getenv("SIFT_REST_URI"),
112+
use_ssl=False,
113+
)
114+
)
115+
```
116+
117+
## Plugin provided fixtures
118+
119+
| Name | Kind | Scope | Purpose |
120+
|---|---|---|---|
121+
| `report_context` | fixture (autouse) | session | The `ReportContext` backing the run's `TestReport`. Use it to attach metadata or open ad-hoc steps. |
122+
| `step` | fixture (autouse) | function | A `NewStep` created for the current test function. Exposes `measure*`, `substep`, `report_outcome`, `fail_if_measurements_failed`, and `current_step`. |
123+
| `_hierarchy_parents` | internal fixture (autouse) | function | Opens a parent step for each `pytest.Package`, `pytest.Module`, and `pytest.Class` ancestor of the current test. Each layer is gated independently; see [ini options](#ini-options). |
124+
| `_parametrize_parents` | internal fixture (autouse) | function | Opens a parent step for each `@pytest.mark.parametrize` axis (and fixture parametrization), nested inside the hierarchy parents. |
125+
| `client_has_connection` | fixture | session | Calls `sift_client.ping.ping()`; consulted by `report_context` at session start in online mode (the default). Override to skip the ping or use a different reachability signal. |
126+
127+
## CLI options
128+
129+
| Flag | Default | Effect |
130+
|---|---|---|
131+
| `--sift-offline` | off (online) | Skip the session-start ping and don't contact Sift. All create/update calls go to the JSONL log file for later replay via `import-test-result-log`. Missing `SIFT_*` env vars are tolerated; placeholders are filled. |
132+
| `--sift-disabled` | off | Skip Sift entirely. Nothing contacts the API and no log file is written; `step.measure(...)` still evaluates bounds and returns a real pass/fail boolean. Also honored via `SIFT_DISABLED=1`. Supersedes every other flag (disabled wins over offline). |
133+
| `--sift-log-file=<path\|true\|false>` | temp file | Where the JSONL log of create/update calls goes. With a log file set, the plugin spawns an `import-test-result-log --incremental` worker that polls the file and replays entries against Sift while the run is in flight. Pass `false` to disable the file entirely; create/update calls then go straight to the API synchronously during tests. Incompatible with `--sift-offline` since offline mode needs the log file as its sole sink. |
134+
| `--no-sift-git-metadata` | git metadata on | Skip capturing git repo/branch/commit on the report's metadata. |
135+
136+
These can be passed permanently via `addopts`:
137+
138+
```ini title="pytest.ini"
139+
[pytest]
140+
addopts = --sift-offline
141+
```
142+
143+
## Ini options
144+
145+
Set the matching ini key directly (recommended for stable per-project
146+
configuration). Each CLI flag has a corresponding key under
147+
`[tool.pytest.ini_options]` in `pyproject.toml` or `[pytest]` in `pytest.ini`.
148+
CLI flags, when passed, override the ini values.
149+
150+
| Ini key | Type | Equivalent CLI flag |
151+
|---|---|---|
152+
| `sift_log_file` | string (`true` / `false` / `none` / path) | `--sift-log-file=<value>` |
153+
| `sift_git_metadata` | bool (default `true`) | `--no-sift-git-metadata` (sets to `false`) |
154+
| `sift_offline` | bool (default `false`) | `--sift-offline` |
155+
| `sift_disabled` | bool (default `false`) | `--sift-disabled` (also honors `SIFT_DISABLED` env var) |
156+
| `sift_autouse` | bool (default `true`) | _(no CLI flag; controls the marker gate below)_ |
157+
| `sift_package_step` | bool (default `true`) | _(ini-only)_. Opens a parent step for each Python package (directory with `__init__.py`) in the test path. |
158+
| `sift_module_step` | bool (default `true`) | _(ini-only)_. Opens a parent step for each test module (file). |
159+
| `sift_class_step` | bool (default `true`) | _(ini-only)_. Opens a parent step for each test class, including nested classes. |
160+
| `sift_parametrize_nesting` | bool (default `true`) | _(ini-only)_. Clusters parametrized tests under shared parents (`test_x`, `axis=value`) instead of flat leaves (`test_x[value]`). |
161+
162+
```toml title="pyproject.toml"
163+
[tool.pytest.ini_options]
164+
sift_offline = true
165+
sift_git_metadata = false
166+
sift_grpc_uri = "your-org.sift.example:443"
167+
sift_rest_uri = "https://your-org.sift.example"
168+
```
169+
170+
```ini title="pytest.ini"
171+
[pytest]
172+
sift_offline = true
173+
sift_git_metadata = false
174+
sift_grpc_uri = your-org.sift.example:443
175+
sift_rest_uri = https://your-org.sift.example
176+
```
177+
178+
## Controlling which tests produce reports
179+
180+
By default every test in the session produces a Sift step. Two markers and one
181+
ini key let you narrow that to a specific set of tests, which is useful when a
182+
repo holds tests that you don't want included in the Sift test report.
183+
184+
| Setting | Effect |
185+
|---------------------------------------------------------|----------------------------------------------------------------------------------------------|
186+
| `sift_autouse = false` in `pyproject.toml` | Flip the project-wide default off. Tests no longer produce steps unless explicitly opted in. |
187+
| `@pytest.mark.sift_include` on a test, class, or module | Force reporting on for that scope, regardless of the project default. |
188+
| `@pytest.mark.sift_exclude` on a test, class, or module | Force reporting off for that scope, regardless of the project default. |
189+
190+
Closest marker determines setting. `sift_exclude` beats `sift_include` when both apply.
191+
`pytestmark` at the class or module level inherits to every test in scope.
192+
193+
### Bulk-applying a marker to a directory
194+
195+
To opt an entire directory in (or out) without editing each file, hook
196+
`pytest_collection_modifyitems` in the directory's `conftest.py`:
197+
198+
```python title="tests/example/conftest.py"
199+
from pathlib import Path
200+
201+
import pytest
202+
203+
_HERE = Path(__file__).parent
204+
205+
206+
def pytest_collection_modifyitems(config, items):
207+
for item in items:
208+
try:
209+
item.path.relative_to(_HERE)
210+
except ValueError:
211+
continue
212+
item.add_marker(pytest.mark.sift_include)
213+
```
214+
215+
This applies `sift_include` to every test collected under `tests/example/`.
216+
Combine with `sift_autouse = false` in `pyproject.toml` for opting in to
217+
specific directories.
218+
219+
`pytest_collection_modifyitems` receives every item in the session, not just
220+
this directory's, so the `relative_to` filter is what scopes the marker.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Pytest Plugin
2+
3+
The Sift Python client ships a pytest plugin that turns a pytest run into a
4+
`TestReport` in Sift. Each test function becomes a `TestStep`, measurements are presented
5+
as rows under that step, and failures propagate up through nested substeps to
6+
the report itself.
7+
8+
## Quick start
9+
10+
Install the client and pytest:
11+
12+
```bash
13+
pip install sift-stack-py pytest python-dotenv
14+
```
15+
16+
Set your connection details in a `.env` next to your tests:
17+
18+
```bash title=".env"
19+
SIFT_API_KEY="..."
20+
SIFT_GRPC_URI="..."
21+
SIFT_REST_URI="..."
22+
```
23+
24+
Find these on the Sift Manage page, where you can also generate an API key.
25+
26+
Register the plugin with a single `pytest_plugins` declaration in your top-level
27+
`conftest.py`:
28+
29+
```python title="conftest.py"
30+
from dotenv import load_dotenv
31+
32+
load_dotenv()
33+
34+
pytest_plugins = ["sift_client.pytest_plugin"]
35+
```
36+
37+
Write a test. The `step` fixture is `autouse`, so any test becomes a step on the
38+
report. Take it as an argument when you want to record a measurement:
39+
40+
```python title="test_battery.py"
41+
def test_battery_voltage(step):
42+
step.measure(
43+
name="battery_voltage",
44+
value=4.97,
45+
bounds={"min": 4.8, "max": 5.2},
46+
unit="V",
47+
)
48+
step.fail_if_measurements_failed()
49+
```
50+
51+
Run it:
52+
53+
```bash
54+
pytest
55+
```
56+
57+
A `TestReport` shows up in Sift once the session finishes.
58+
59+
!!! tip "Fail at the end, not per measurement"
60+
`step.measure(...)` returns a pass/fail boolean and never raises, so a
61+
failing measurement marks the step failed without aborting the test. Take
62+
every measurement first, then call `step.fail_if_measurements_failed()` once
63+
at the end, so every measurement still lands in the report even when one
64+
fails. It fails the test via `pytest.fail` (no assertion noise in
65+
`error_info`), and unlike asserting on an individual `step.measure(...)` call
66+
it does not short-circuit on the first failure and skip every measurement
67+
after it.
68+
69+
## Sensible defaults
70+
71+
With nothing but the `conftest.py` above, you get:
72+
73+
- **Full step tree.** Every Python package, test module, test class, and
74+
parametrize axis above a test becomes a parent step, so the report mirrors
75+
your test layout.
76+
- **Online mode.** The plugin pings Sift at session start and streams
77+
create/update calls to your tenant during the run.
78+
- **Git metadata.** Repo, branch, and commit are captured on the report
79+
automatically.
80+
81+
Everything is on by default and individually overridable. See
82+
[Configuration & Defaults](configuration.md) for the full audit of every knob,
83+
marker, flag, and fixture.
84+
85+
## Running modes
86+
87+
The plugin runs in one of three modes, picked at invocation.
88+
89+
| Mode | How to select | Contacts Sift | When to use |
90+
|---|---|---|---------------------------------------------------------------|
91+
| **Online** | default (no flag) | Yes, during the run | Default choice |
92+
| **Offline** | `--sift-offline` | No; records to a log file for later replay | Environments without Sift access. |
93+
| **Disabled** | `--sift-disabled` | No | Local dev. Bounds still evaluate and return a real pass/fail. |
94+
95+
Online mode pings Sift once at session start and aborts if Sift is unreachable or the credentials are invalid,
96+
so a misconfigured job fails immediately instead of silently producing no report.
97+
During the run, every create and update is appended to a JSONL log file.
98+
A background worker uploads new entries to Sift incrementally.
99+
If the connection drops mid-test, the test keeps running and the log keeps writing locally.
100+
The remaining entries can be uploaded afterward by running import-test-result-log, which the plugin prints on exit.
101+
102+
See [Running Modes](running_modes.md) for the log-file and replay pipeline,
103+
overriding the connection check, and replaying a saved log.
104+
105+
## Report structure
106+
107+
The report tree mirrors your test layout: packages, modules, classes, and
108+
parametrize axes nest automatically, and you can open arbitrary substeps inside
109+
a test. See [Report Structure](report_structure.md) for the layout-to-tree
110+
mapping, measurement variants, and report metadata.
111+
112+
## Pass/fail outcomes
113+
114+
Every pytest outcome (pass, assertion failure, exception, skip, xfail, hard
115+
exit) maps to a `TestStatus`, and failures roll up to the parent steps and the
116+
report. See [Pass/Fail Behavior](pass_fail_behavior.md).
117+
118+
## Try the runnable demo
119+
120+
The [Pytest Plugin Quickstart](../../examples/pytest_plugin_quickstart.md) walks
121+
through a self-contained demo project that exercises every layer of the step
122+
tree, with instructions to run it with or without a Sift tenant.

0 commit comments

Comments
 (0)