Skip to content

Commit 4522498

Browse files
committed
clean up handling of offline mode and implement a noop version
1 parent 8820e7f commit 4522498

9 files changed

Lines changed: 673 additions & 249 deletions

File tree

python/docs/examples/pytest_plugin.md

Lines changed: 74 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -88,27 +88,26 @@ def sift_client() -> SiftClient:
8888
| `report_context` | fixture (autouse) | session | The `ReportContext` backing the run's `TestReport`. Use it to attach metadata or open ad-hoc steps. |
8989
| `step` | fixture (autouse) | function | A `NewStep` created for the current test function. Exposes `measure*`, `substep`, `report_outcome`, and `current_step`. |
9090
| `module_substep` | fixture (autouse) | module | One step per test file with each function nested as a substep. |
91-
| `client_has_connection` | fixture | session | Calls `sift_client.ping.ping()`; consulted only when `--sift-test-results-check-connection` is set. |
91+
| `client_has_connection` | fixture | session | Pings Sift via `sift_client.ping.ping()`. Consulted at session start when running in online mode. |
9292

9393
### CLI options
9494

9595
| Flag | Default | Effect |
9696
|---|---|---|
97-
| `--sift-test-results-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. |
98-
| `--no-sift-test-results-git-metadata` | git metadata on | Skip capturing git repo/branch/commit on the report's metadata. |
99-
| `--sift-test-results-check-connection` | off | Make `report_context`, `step`, and `module_substep` no-op (yield `None`) when `client_has_connection` is `False`. Lets the same suite run locally without a Sift backend. |
97+
| `--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`. |
98+
| `--sift-log-file=<path>` | temp file | Path to write the JSONL log file. In online mode this is a write-through backup; in offline mode it is the sole sink. When unset, a temp file is created and its path emitted via a `logger.info` line at session start. |
99+
| `--no-sift-log-file` | off | Disable the JSONL log file (online mode only). Create/update calls run inline against the API instead of being deferred through the import worker. Incompatible with `--sift-offline`. |
100+
| `--sift-no-git-metadata` | git metadata on | Skip capturing git repo/branch/commit on the report's metadata. |
100101

101102
These can be set permanently in `pytest.ini`:
102103

103104
```ini title="pytest.ini"
104105
[pytest]
105-
addopts = --sift-test-results-check-connection
106+
addopts = --sift-offline
106107
```
107108

108109
!!! warning "FedRAMP / shared environments"
109-
Pass `--sift-test-results-log-file=false` to skip the temp file + worker
110-
pipeline. Create/update calls then run inline against the API instead of
111-
being deferred through a subprocess.
110+
Pass `--no-sift-log-file` to skip the temp file + worker pipeline. Create/update calls then run inline against the API instead of being deferred through a subprocess.
112111

113112
### Report metadata captured automatically
114113

@@ -119,7 +118,7 @@ Every report the plugin creates includes:
119118
- `system_operator`: `getpass.getuser()`.
120119
- `start_time` / `end_time`: set on session enter/exit.
121120
- `status`: starts at `IN_PROGRESS`, finalized to `PASSED` or `FAILED` on session exit (failure if any step failed or an exception escaped the session).
122-
- `metadata.git_repo`, `metadata.git_branch`, `metadata.git_commit`: captured via `git remote get-url origin` / `git rev-parse --abbrev-ref HEAD` / `git describe --always --dirty --exclude '*'`. Suppressed by `--no-sift-test-results-git-metadata` or when not in a git repo.
121+
- `metadata.git_repo`, `metadata.git_branch`, `metadata.git_commit`: captured via `git remote get-url origin` / `git rev-parse --abbrev-ref HEAD` / `git describe --always --dirty --exclude '*'`. Suppressed by `--sift-no-git-metadata` or when not in a git repo.
123122

124123
Example invocations:
125124

@@ -548,94 +547,75 @@ The `unit` argument is a free-form string label (e.g. `"V"`, `"C"`, `"psi"`).
548547
pytest
549548

550549
# Pin the log file so you can replay it later if the import worker dies
551-
pytest --sift-test-results-log-file=./sift-results.jsonl
550+
pytest --sift-log-file=./sift-results.jsonl
552551
```
553552

554-
See [Running offline](#running-offline) for the same suite running with or
555-
without a reachable Sift server.
553+
See [Running offline](#running-offline) for capturing a run locally and
554+
replaying it later, and [Disabling the plugin](#disabling-the-plugin) for
555+
keeping test code working when Sift isn't part of the pipeline.
556556

557-
## Running offline
557+
## Modes
558558

559-
The plugin supports two offline workflows, depending on whether you want a
560-
Sift report at all when the test environment can't reach Sift. The first
561-
turns the plugin into a no-op when the server is unreachable. The second
562-
keeps the plugin running normally and writes every create/update to a local
563-
JSONL file that you upload from a connected machine afterward.
559+
The plugin has two runtime modes. Bad configuration (missing
560+
`SIFT_API_KEY` / `SIFT_GRPC_URI` / `SIFT_REST_URI`) is a hard error in both
561+
modes — the failure surfaces with the missing variable named.
564562

565-
| Pattern | Flag | Runtime behavior | Follow-up |
566-
|---|---|---|---|
567-
| Skip when offline | `--sift-test-results-check-connection` | Fixtures yield `None`, no log file, no report. Pytest still reports pass/fail. | None. |
568-
| Capture locally, upload later | `--sift-test-results-log-file=<path>` | Plugin writes every create/update to the JSONL file. | `import-test-result-log <path>` from a connected machine. |
563+
| Mode | Flag | Session-start ping | Transport | Log file |
564+
|---|---|---|---|---|
565+
| Online (default) | | Required (hard fail) | Live gRPC | Optional write-through backup |
566+
| Offline | `--sift-offline` | Skipped | None | Required (temp file by default) |
569567

570-
Pattern 1 suits laptop dev and CI without Sift secrets. Pattern 2 suits
571-
field tests, vehicles on remote sites, and air-gapped labs.
568+
If you want to keep test code working without the plugin loaded at all,
569+
wire in [`sift_client.pytest_plugin_noop`](#disabling-the-plugin) instead.
572570

573-
### Pattern 1: skip when offline
571+
## Running offline
574572

575-
`--sift-test-results-check-connection` makes the plugin ping Sift once at
576-
session start through the `client_has_connection` fixture (which by default
577-
calls `sift_client.ping.ping()`). On a failed ping, `report_context`,
578-
`step`, and `module_substep` yield `None` for the rest of the session.
579-
Pytest still runs the tests and still reports pass/fail.
573+
`--sift-offline` runs the suite without contacting Sift. Every create/update
574+
goes to a JSONL log file that you replay against Sift later with
575+
`import-test-result-log`. Useful for field tests, vehicles on remote sites,
576+
air-gapped labs, or CI that doesn't have Sift credentials.
580577

581578
```bash
582-
pytest --sift-test-results-check-connection
579+
pytest --sift-offline --sift-log-file=./run.jsonl
583580
```
584581

585582
```ini title="pytest.ini"
586583
[pytest]
587-
addopts = --sift-test-results-check-connection
584+
addopts = --sift-offline
588585
```
589586

590-
#### Handling `None` in tests
591-
592-
Calls on `step` raise `AttributeError` when it's `None`, so tests that take
593-
`step` as a parameter need a guard. The cleanest fix is to shadow the
594-
plugin's `step` fixture in your conftest and turn the `None` case into an
595-
automatic skip.
596-
597-
```python title="conftest.py"
598-
import pytest
599-
600-
pytest_plugins = ["sift_client.pytest_plugin"]
601-
602-
603-
@pytest.fixture(autouse=True)
604-
def step(step):
605-
if step is None:
606-
pytest.skip("Sift unavailable")
607-
yield step
608-
```
587+
What happens during the run:
609588

610-
The `step` parameter on the override resolves to the plugin's fixture, not
611-
to the override itself. `autouse=True` is required so the skip applies to
612-
tests that don't request `step` directly. The same shadowing trick works
613-
for `module_substep` and `report_context`.
589+
- The plugin does not ping Sift at session start.
590+
- Every report, step, and measurement create/update is written to the log
591+
file. Responses are simulated locally and keyed by UUIDs that the replay
592+
later maps to real IDs.
593+
- Tests run against a real `step` fixture, so `step.measure(...)`, substeps,
594+
parametrize, fixtures, and `module_substep` behave exactly as they do
595+
online. No test-code changes are required.
596+
- The plugin does not spawn the import worker subprocess; the log file is
597+
the sole sink.
614598

615-
For one-off tests that don't share a conftest, an inline guard works just
616-
as well:
599+
Once you have connectivity, replay the file:
617600

618-
```python
619-
def test_battery_voltage(step):
620-
if step is None:
621-
pytest.skip("Sift unavailable")
622-
step.measure(name="battery_voltage", value=4.97, bounds={"min": 4.8, "max": 5.2})
601+
```bash
602+
import-test-result-log ./run.jsonl
623603
```
624604

625-
If you'd rather have tests pass through silently than skip them, wrap the
626-
calls in a helper that no-ops on `None`:
605+
The replay creates the report, steps, and measurements against Sift in one
606+
batch. See [Replaying a saved log file](#replaying-a-saved-log-file) for
607+
details on cleanup and the incremental flag.
627608

628-
```python
629-
def safe_measure(step, **kwargs):
630-
if step is None:
631-
return True
632-
return step.measure(**kwargs)
633-
```
609+
!!! warning "Pin the log path"
610+
Without `--sift-log-file=<path>`, the plugin writes to a
611+
`tempfile.NamedTemporaryFile` and surfaces the path via a `logger.info`
612+
line at session start. Always pin a known path when you intend to replay
613+
the file later.
634614

635-
#### Overriding the connection check
615+
### Overriding the connection check
636616

637617
The default `client_has_connection` fixture calls `sift_client.ping.ping()`.
638-
Override it in your conftest if pinging is the wrong signal for your
618+
Override it in your conftest when pinging is the wrong signal for your
639619
environment, for example a token cache that's only warm when authenticated:
640620

641621
```python title="conftest.py"
@@ -649,50 +629,35 @@ def client_has_connection(sift_client) -> bool:
649629
return Path("~/.sift-token-cache").expanduser().is_file()
650630
```
651631

652-
The plugin only consults this fixture when `--sift-test-results-check-connection`
653-
is set, so an unused override has no effect on a normal run.
654-
655-
### Pattern 2: capture locally, upload later
632+
The plugin only consults this fixture in online mode (the default), so the
633+
override has no effect when running with `--sift-offline`. Returning `False`
634+
or raising from the fixture aborts the session with `pytest.UsageError`.
656635

657-
This pattern keeps the plugin running normally even when Sift is
658-
unreachable. The plugin writes to the log file, the worker dies on connect,
659-
and the file is left on disk for you to upload later. Pin the log file path
660-
so you can find it afterward, and don't pass
661-
`--sift-test-results-check-connection`, which would suppress the logging
662-
this pattern relies on.
636+
## Disabling the plugin
663637

664-
```bash
665-
pytest --sift-test-results-log-file=./run.jsonl
666-
```
638+
Two ways to turn the plugin off:
667639

668-
What happens during the run:
669-
670-
- Every report, step, and measurement create/update is written to
671-
`run.jsonl`. The plugin doesn't contact the Sift API for any of these
672-
calls; they return simulated responses keyed by UUIDs that the replay
673-
later maps to real IDs.
674-
- The `import-test-result-log --incremental` worker subprocess starts and
675-
exits early when it can't reach Sift. The session does not fail when the
676-
worker exits before the run ends.
677-
- Tests run against a real `step` fixture, so `step.measure(...)`,
678-
substeps, parametrize, fixtures, and `module_substep` behave exactly as
679-
they do online. No conftest changes are needed.
640+
- For a one-off run: `pytest -p no:sift_client.pytest_plugin`. Tests that
641+
request `step`, `report_context`, or `module_substep` will fail at fixture
642+
resolution.
643+
- To keep test code working without Sift configuration:
644+
swap to the sibling no-op plugin. It ships the same fixture names; calls
645+
to `step.measure(...)` evaluate bounds locally and return the real
646+
pass/fail boolean, but nothing is sent to Sift and no log file is written.
680647

681-
Once you have connectivity, replay the file:
648+
```python title="conftest.py"
649+
import os
682650

683-
```bash
684-
import-test-result-log ./run.jsonl
651+
pytest_plugins = [
652+
"sift_client.pytest_plugin"
653+
if os.getenv("SIFT_ENABLED")
654+
else "sift_client.pytest_plugin_noop"
655+
]
685656
```
686657

687-
The replay creates the report, steps, and measurements against Sift in one
688-
batch. See [Replaying a saved log file](#replaying-a-saved-log-file) for
689-
details on cleanup and the incremental flag.
690-
691-
!!! warning "Pin the log path for Pattern 2"
692-
Without `--sift-test-results-log-file=<path>`, the plugin writes to a
693-
`tempfile.NamedTemporaryFile` and only surfaces the path via a
694-
`logger.info` line. Always pin a known path when you intend to replay
695-
the file later.
658+
The no-op plugin doesn't read `SIFT_API_KEY` / `SIFT_GRPC_URI` /
659+
`SIFT_REST_URI` and never constructs a `SiftClient`, so it's safe to load in
660+
environments that aren't allowed to talk to Sift at all.
696661

697662
## Replaying a saved log file
698663

python/lib/sift_client/_tests/conftest.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@
88
from sift_client import SiftClient, SiftConnectionConfig
99
from sift_client.util.util import AsyncAPIs
1010

11+
pytest_plugins = ["sift_client.pytest_plugin"]
12+
13+
14+
def pytest_configure(config: pytest.Config) -> None:
15+
"""Pick a sensible plugin mode based on whether integration tests are being run.
16+
17+
Without ``-m integration`` (the default), force offline mode so the autouse
18+
plugin fixtures construct cleanly without contacting Sift. Integration runs
19+
that target a real backend stay in online mode with the log file disabled.
20+
"""
21+
is_integration_run = "integration" in (config.option.markexpr or "")
22+
have_real_backend = all(
23+
os.getenv(name) for name in ("SIFT_API_KEY", "SIFT_GRPC_URI", "SIFT_REST_URI")
24+
)
25+
if is_integration_run and have_real_backend:
26+
config.option.no_sift_log_file = True
27+
else:
28+
config.option.sift_offline = True
29+
1130

1231
@pytest.fixture(scope="session")
1332
def sift_client() -> SiftClient:
@@ -78,6 +97,3 @@ def ci_pytest_tag(sift_client):
7897
return tag
7998

8099

81-
def pytest_configure(config: pytest.Config) -> None:
82-
"""Enable the Sift connection-check mode for the fixtures used in this test suite since we run w/ mock client in non-integration tests."""
83-
config.option.sift_test_results_check_connection = True

0 commit comments

Comments
 (0)