sift_client.pytest_plugin
¶
Sift pytest plugin: records each test as a step in a Sift test report.
Load it from a project's conftest.py::
pytest_plugins = ["sift_client.pytest_plugin"]
This module holds only the plugin's public surface: the catchable warnings,
the session-state globals a conftest may read, the fixtures a project can
request or override, and pytest's hook entry points. The implementation
(settings registry, step stacks, report construction, terminal formatting)
lives under sift_client._internal.pytest_plugin.
| CLASS | DESCRIPTION |
|---|---|
NewStep |
Context manager to create a new step in a test report. See usage example in init.py. |
ReportContext |
Context manager for a new TestReport. See usage example in init.py. |
SiftPytestPluginWarning |
Base warning for issues raised by the Sift pytest plugin. |
SiftPytestStepDrainWarning |
A parent step's |
| FUNCTION | DESCRIPTION |
|---|---|
abort |
Stop the pytest session and record the report and open parent steps as |
client_has_connection |
Verify the |
report_context |
Lazy session-scoped Sift ReportContext. |
sift_client |
Default |
step |
Create an outer step for the function when the Sift gate is on. |
| ATTRIBUTE | DESCRIPTION |
|---|---|
REPORT_CONTEXT |
TYPE:
|
SIFT_AUDIT_LOG_STASH_KEY |
|
SIFT_REPORT_ID_STASH_KEY |
|
SIFT_REPORT_URL_STASH_KEY |
|
SIFT_SESSION_DIR_STASH_KEY |
|
NewStep
¶
NewStep(
report_context: ReportContext,
name: str,
description: str | None = None,
assertion_as_fail_not_error: bool = True,
metadata: dict[str, str | float | bool] | None = None,
*,
parent: TestStep | None | object = _USE_STACK_TOP,
push: bool = True,
origin: str = "step",
source_path: str | None = None,
)
Bases: AbstractContextManager
Context manager to create a new step in a test report. See usage example in init.py.
Initialize a new step context.
| PARAMETER | DESCRIPTION |
|---|---|
report_context
|
The report context to create the step in.
TYPE:
|
name
|
The name of the step.
TYPE:
|
description
|
The description of the step.
TYPE:
|
assertion_as_fail_not_error
|
Mark steps with assertion errors as failed instead of error+traceback (some users want assertions to work as simple failures especially when using pytest).
TYPE:
|
metadata
|
[Optional] Structured key/value metadata to attach to the step.
TYPE:
|
parent
|
Parent step to nest under; see :meth:
TYPE:
|
push
|
Whether the step joins the step stack; see :meth:
TYPE:
|
origin
|
Audit-log label for where the step came from (hierarchy, parametrize, test, substep); does not affect behavior.
TYPE:
|
source_path
|
Audit-log label: the pytest nodeid this step was created for; does not affect behavior.
TYPE:
|
| METHOD | DESCRIPTION |
|---|---|
measure |
Measure a value and return the result. |
measure_all |
Ensure that all values in a list are within bounds and return the result. Records measurements for all values outside the bounds. |
measure_avg |
Calculate the average of a list of values, measure the average against given bounds, and return the result. |
pytest_fail_if_step_failed |
Fail the running pytest test if this step or any descendant failed. |
report_outcome |
Report an outcome from some action or measurement. Creates a substep that is pass/fail with the optional reason as the description. |
substep |
Alias to return a new step context manager from the current step. The ReportContext will manage nesting of steps. |
| ATTRIBUTE | DESCRIPTION |
|---|---|
assertion_as_fail_not_error |
TYPE:
|
client |
TYPE:
|
current_step |
TYPE:
|
measurements_passed |
True if every measurement recorded directly on this step has passed.
TYPE:
|
report_context |
TYPE:
|
assertion_as_fail_not_error
class-attribute
instance-attribute
¶
current_step
class-attribute
instance-attribute
¶
current_step: TestStep | None = (
self.report_context.create_step(
name,
description,
metadata=metadata,
parent=parent,
push=push,
)
)
measurements_passed
property
¶
True if every measurement recorded directly on this step has passed.
Counts only step.measure, step.measure_avg, and
step.measure_all calls on this NewStep instance; substep and
report_outcome failures are not folded in. For the end-of-test
failure that mirrors the report, use pytest_fail_if_step_failed(),
which also covers failed substeps.
measure
¶
measure(
*,
name: str,
value: float | str | bool | int,
bounds: dict[str, float]
| NumericBounds
| str
| None = None,
timestamp: datetime | None = None,
unit: str | None = None,
description: str | None = None,
metadata: dict[str, str | float | bool] | None = None,
channel_names: list[str] | list[Channel] | None = None,
) -> bool
Measure a value and return the result.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The name of the measurement.
TYPE:
|
value
|
The value of the measurement.
TYPE:
|
bounds
|
[Optional] The bounds to compare the value to.
TYPE:
|
timestamp
|
[Optional] The timestamp of the measurement. Defaults to the current time.
TYPE:
|
unit
|
[Optional] The unit of the measurement.
TYPE:
|
description
|
[Optional] Notes about the measurement. Server caps at 2000 characters; longer strings are truncated with a warning.
TYPE:
|
metadata
|
[Optional] Structured key/value metadata to attach to the measurement.
For metadata shared across measurements, prefer the
TYPE:
|
channel_names
|
[Optional] Sift channel names or
TYPE:
|
returns: The result of the measurement.
measure_all
¶
measure_all(
*,
name: str,
values: list[float | int] | NDArray[float64] | Series,
bounds: dict[str, float] | NumericBounds,
timestamp: datetime | None = None,
unit: str | None = None,
description: str | None = None,
metadata: dict[str, str | float | bool] | None = None,
channel_names: list[str] | list[Channel] | None = None,
) -> bool
Ensure that all values in a list are within bounds and return the result. Records measurements for all values outside the bounds.
Note: Measurements will only be recorded for values outside the bounds. To record measurements for all values, just call measure for each value.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The name of the measurement.
TYPE:
|
values
|
The list of values to measure the average of.
TYPE:
|
bounds
|
The bounds to compare the value to.
TYPE:
|
timestamp
|
[Optional] The timestamp of the measurement. Defaults to the current time.
TYPE:
|
unit
|
[Optional] The unit of the measurement.
TYPE:
|
description
|
[Optional] Notes attached to each out-of-bounds measurement. Server caps at 2000 characters; longer strings are truncated with a warning.
TYPE:
|
metadata
|
[Optional] Structured key/value metadata for each out-of-bounds measurement.
TYPE:
|
channel_names
|
[Optional] Sift channel names or
TYPE:
|
returns: The true if all values are within the bounds, false otherwise.
measure_avg
¶
measure_avg(
*,
name: str,
values: list[float | int] | NDArray[float64] | Series,
bounds: dict[str, float] | NumericBounds,
timestamp: datetime | None = None,
unit: str | None = None,
description: str | None = None,
metadata: dict[str, str | float | bool] | None = None,
channel_names: list[str] | list[Channel] | None = None,
) -> bool
Calculate the average of a list of values, measure the average against given bounds, and return the result.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The name of the measurement.
TYPE:
|
values
|
The list of values to measure the average of.
TYPE:
|
bounds
|
The bounds to compare the value to.
TYPE:
|
timestamp
|
[Optional] The timestamp of the measurement. Defaults to the current time.
TYPE:
|
unit
|
[Optional] The unit of the measurement.
TYPE:
|
description
|
[Optional] Notes about the measurement. Server caps at 2000 characters; longer strings are truncated with a warning.
TYPE:
|
metadata
|
[Optional] Structured key/value metadata to attach to the measurement.
TYPE:
|
channel_names
|
[Optional] Sift channel names or
TYPE:
|
returns: The true if the average of the values is within the bounds, false otherwise.
pytest_fail_if_step_failed
¶
Fail the running pytest test if this step or any descendant failed.
Covers every signal that resolves the step to FAILED in the report:
out-of-bounds measurements recorded directly on the step, failed
substeps, and report_outcome failures. Call it once at the end of a
test so the pytest verdict matches the report instead of passing green
while the report shows a failure.
It fails via pytest.fail(pytrace=False) so the step resolves to
FAILED without an assertion traceback in error_info. No-op when the
step and all of its descendants passed. Call after the work is done so
every measurement and substep is recorded before the failure fires.
The failure message names each out-of-bounds measurement and each
failed substep. message is used as the header line.
report_outcome
¶
Report an outcome from some action or measurement. Creates a substep that is pass/fail with the optional reason as the description.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The name of the substep.
TYPE:
|
result
|
True if the action or measurement passed, False otherwise.
TYPE:
|
reason
|
[Optional] The context to include in the description of the substep.
TYPE:
|
returns: The given result so the function can be used in line.
ReportContext
¶
ReportContext(
client: SiftClient,
name: str,
test_system_name: str | None = None,
system_operator: str | None = None,
test_case: str | None = None,
serial_number: str | None = None,
part_number: str | None = None,
log_file: str | Path | bool | None = None,
include_git_metadata: bool = False,
replay_log_file: bool = True,
metadata: dict[str, str | float | bool] | None = None,
audit_log: str | Path | None = None,
)
Bases: AbstractContextManager
Context manager for a new TestReport. See usage example in init.py.
Initialize a new report context.
| PARAMETER | DESCRIPTION |
|---|---|
client
|
The Sift client to use to create the report.
TYPE:
|
name
|
The name of the report.
TYPE:
|
test_system_name
|
The name of the test system. Will default to the hostname if not provided.
TYPE:
|
system_operator
|
The operator of the test system. Will default to the current user if not provided.
TYPE:
|
test_case
|
The name of the test case. Will default to the basename of the file containing the test if not provided.
TYPE:
|
serial_number
|
Optional serial_number stored on the report. Unset when None.
TYPE:
|
part_number
|
Optional part_number stored on the report. Unset when None.
TYPE:
|
log_file
|
If True, create a temp log file. If a path, use that path. If False/None, no log file is written and create/update calls the API.
TYPE:
|
include_git_metadata
|
If True, include git metadata in the report.
TYPE:
|
metadata
|
Structured key/value metadata to attach to the report. Merged
on top of git metadata when
TYPE:
|
replay_log_file
|
When True (the default) and
TYPE:
|
audit_log
|
When set, the path of a DEBUG audit log. The replay worker
is spawned with
TYPE:
|
| METHOD | DESCRIPTION |
|---|---|
create_step |
Create a new step in the report context. |
exit_step |
Exit a step and update the report context. |
get_next_step_path |
Preview the path the next step under |
mark_step_failed_after_close |
Mark a step's parent as failed after the step has already been popped from the stack. |
new_step |
Alias to return a new step context manager from this report context. Use create_step for actually creating a TestStep in the current context. |
note_close |
Record a just-closed step's |
propagate_step_result |
Propagate this step's final status to the parent step. |
record_measurement |
Retain a recorded measurement for end-of-run summaries. |
record_step_outcome |
Report a failure to the report context. |
| ATTRIBUTE | DESCRIPTION |
|---|---|
any_failures |
TYPE:
|
audit_log |
TYPE:
|
child_counts |
TYPE:
|
client |
TYPE:
|
created_measurements |
TYPE:
|
created_steps |
TYPE:
|
is_simulated |
True when this context's report came from the simulate path.
TYPE:
|
log_file |
TYPE:
|
measurement_counts |
Tally of recorded measurements keyed by
TYPE:
|
open_step_results |
TYPE:
|
parent_end_times |
TYPE:
|
replay_incomplete |
TYPE:
|
replay_log_file |
|
report |
TYPE:
|
session_aborted |
TYPE:
|
step_is_open |
TYPE:
|
step_stack |
TYPE:
|
step_status_counts |
Tally of every created step by its current status.
TYPE:
|
audit_log
class-attribute
instance-attribute
¶
is_simulated
property
¶
True when this context's report came from the simulate path.
Delegates to self.report.is_simulated; see TestReport.is_simulated
for the full semantics.
measurement_counts
property
¶
Tally of recorded measurements keyed by passed (True/False).
Read at the end of a run for summaries.
report
instance-attribute
¶
report: TestReport = client.test_results.create(
create, log_file=self.log_file
)
step_status_counts
property
¶
step_status_counts: Counter[TestStatus]
Tally of every created step by its current status.
Includes hierarchy/parametrize parent steps. Read at the end of a run for summaries; reflects late status changes since steps are mutated in place.
create_step
¶
create_step(
name: str,
description: str | None = None,
metadata: dict[str, str | float | bool] | None = None,
*,
parent: TestStep | None | object = _USE_STACK_TOP,
push: bool = True,
) -> TestStep
Create a new step in the report context.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The name of the step.
TYPE:
|
description
|
The description of the step.
TYPE:
|
metadata
|
[Optional] Structured key/value metadata to attach to the step. For
metadata shared across every step in a report, prefer the
TYPE:
|
parent
|
The parent step to nest under.
TYPE:
|
push
|
Whether to push the new step onto the step stack. True (the default) for leaf/in-test steps so their substeps nest under them. The pytest plugin passes False for hierarchy/parametrize parents, which live in its own registry and would otherwise trap unrelated steps beneath them.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
TestStep
|
The created step. |
exit_step
¶
exit_step(step: TestStep)
Exit a step and update the report context.
Stacked steps (leaves and their in-test substeps) close in strict LIFO
order, so a step that isn't the current top of the stack is a real
invariant break. Steps created with an explicit parent and push=False
(the pytest plugin's hierarchy/parametrize parents) never sit on the
stack and may close in any order, so clearing open_step_results is all
that's needed; their result was already propagated to their own parent.
get_next_step_path
¶
get_next_step_path(
parent: TestStep | None | object = _USE_STACK_TOP,
) -> str
Preview the path the next step under parent would get (no side effects).
Parent-relative: a child's path is <parent path>.<nth child>, or
<n> at the root. Defaults to the top of the step stack so existing
callers see the same value the next stacked create_step will assign.
mark_step_failed_after_close
¶
mark_step_failed_after_close(step: TestStep)
Mark a step's parent as failed after the step has already been popped from the stack.
Used by the pytest plugin when a teardown-phase report fires after the
fixture's __exit__ has already resolved and exited the step.
new_step
¶
new_step(
name: str,
description: str | None = None,
assertion_as_fail_not_error: bool = True,
metadata: dict[str, str | float | bool] | None = None,
*,
parent: TestStep | None | object = _USE_STACK_TOP,
push: bool = True,
origin: str = "step",
source_path: str | None = None,
) -> NewStep
Alias to return a new step context manager from this report context. Use create_step for actually creating a TestStep in the current context.
parent and push default to the linear, stack-based behavior used
by everyday callers. The pytest plugin passes an explicit parent with
push=False to open report-tree parents that persist outside the stack;
see :meth:create_step.
origin (e.g. hierarchy/parametrize/test/substep) and
source_path (the pytest nodeid the step was created for) are
audit-log labels only; they do not affect step creation.
note_close
¶
note_close(step: TestStep) -> None
Record a just-closed step's end_time against its parent.
Lets a long-lived parent (one closed later, out of band) adopt the finish
time of its latest child instead of wall-clock at its own close. Keyed by
the parent's step_path (the child path minus its last segment).
propagate_step_result
¶
propagate_step_result(
step: TestStep, status: TestStatus
) -> bool
Propagate this step's final status to the parent step.
Status is the governor: anything outside {PASSED, SKIPPED} counts
as a failure for the parent. error_info is intentionally not
consulted here; it is free-form diagnostic data that may sit on a
step regardless of status.
record_measurement
¶
record_measurement(measurement: TestMeasurement) -> None
Retain a recorded measurement for end-of-run summaries.
SiftPytestPluginWarning
¶
Bases: SiftWarning
Base warning for issues raised by the Sift pytest plugin.
SiftPytestStepDrainWarning
¶
Bases: SiftPytestPluginWarning
A parent step's __exit__ raised while the plugin was closing it.
Surfaced when a parent step is closed (early as its subtree finishes, or at session end) so the close can continue and pytest test outcomes stay unaffected; the underlying exception is included in the message for debugging.
abort
¶
Stop the pytest session and record the report and open parent steps as
ABORTED rather than FAILED.
Use this for a system-level stop where the run was cut off rather than a test
failing, such as the device under test going away or a rig fault that makes
the remaining tests meaningless. The step that calls it and any open substeps
resolve to ABORTED, and the enclosing class, module, package, and the
report inherit ABORTED too.
For a stop that should read as a failure, use pytest.exit (the default,
which rolls up FAILED); for a single failing test, use pytest.fail. A
real Ctrl-C / KeyboardInterrupt is treated the same as abort without
needing this call.
| PARAMETER | DESCRIPTION |
|---|---|
reason
|
Message shown by pytest and recorded as the stop reason.
TYPE:
|
returncode
|
Process exit code to pass through to
TYPE:
|
client_has_connection
¶
Verify the SiftClient can reach Sift via /ping.
Consulted at session start by report_context in online mode. A failed
ping aborts the session via pytest.exit. Override this fixture in your
conftest to use a
different reachability signal (e.g. a cached auth token) for environments
where pinging is the wrong check. Returns False in --sift-disabled
mode without constructing a client.
pytest_addoption
¶
Register every CLI flag and pytest ini key declared in PLUGIN_OPTIONS.
pytest_collection_finish
¶
Tally each parent's descendant leaves so parents can close mid-session.
Delegates to tally_expected_parents; runs after deselection so the counts
reflect only the selected, gated-in items. See release_finished_leaf.
pytest_configure
¶
Register the Sift gate markers and warn on unknown SIFT_* settings.
pytest_itemcollected
¶
Cache each test item's hierarchy chain and parametrize path at collection.
This is a per-item hook, not pytest_collection_modifyitems. The plugin
never touches the items list or its order, so it cannot conflict with a
user's (or another plugin's) collection-ordering hook. The report tree is
built from an identity-keyed registry (see get_or_create_parent_chain),
so item order is irrelevant to nesting; pytest-randomly,
pytest-ordering, and pytest's own fixture-scope reordering are all
preserved untouched.
The stash is a cache the autouse fixtures read back; both keys have an on-demand recompute fallback, so an item a later hook injects without going through this hook still resolves correctly.
pytest_keyboard_interrupt
¶
Mark the session aborted for a real Ctrl-C / KeyboardInterrupt.
pytest calls this for both KeyboardInterrupt and its own Exit; only a
genuine interrupt (an operator or system stop) should roll up ABORTED. A
pytest.exit() raises Exit and is left in the FAILED bucket; the
abort() helper sets the flag itself before exiting, so it is unaffected by
the Exit case here.
pytest_report_header
¶
Emit a session-start header with the SDK version and active mode.
Suppressed under -q (negative verbosity), matching how pytest hides its
own platform/plugin header.
pytest_runtest_logfinish
¶
Close report-tree parents whose subtree finished with this item.
Fires once per item (pass / fail / skip / error); delegates to
release_finished_leaf, which decrements the item's parents' remaining-leaf
counts and closes any that reach zero, so containers resolve progressively
rather than all at session end.
pytest_runtest_makereport
¶
Capture per-phase reports and finalize step status after teardown.
Stashes both rep_<when> (the CallInfo, kept for pytest plugins that
expect that conventional attribute) and _sift_phase_<when> (a
SimpleNamespace(call, report) used by resolve_initial_status). The
collection-time skip path is strictly gated on _sift_step being unset
so it does not duplicate steps the fixture already created.
pytest_sessionfinish
¶
Close any report-tree parents still open at session end (innermost first).
Normally a no-op: report_context_impl finalizes the parents inside the
ReportContext block so their updates reach the log before the import
worker drains, and most parents already closed early as their subtrees
finished. This is the idempotent backstop for anything still open.
Runs as a hookwrapper so the drain happens after pytest's own
pytest_sessionfinish (SetupState.teardown_exact), which finalizes the
still-open leaf step and the session-scoped report_context fixture. On a
session abort (pytest.exit) the leaf's fixture teardown is deferred to
that point; finalizing parents before it would close them while their
descendant is still unresolved, so the abort/failure would not roll up. The
yield lets that teardown run first, leaving this call the no-op backstop
it is meant to be.
pytest_terminal_summary
¶
Emit a session-end Sift report summary, adapting per mode.
The printed panel is suppressed under -q, but programmatic side effects
(stashing the report ref for conftest.py, --sift-open-report) still run so
other plugins and CI steps can consume the result. The panel itself is
rendered by write_report_summary; this hook handles the side effects.
pytest_unconfigure
¶
Tear down the audit-log handlers so they don't outlive the session.
A no-op when audit logging was disabled (no handlers were attached).
report_context
¶
report_context(
request: FixtureRequest, pytestconfig: Config
) -> Generator[ReportContext, None, None]
Lazy session-scoped Sift ReportContext.
The fixture is no longer autouse; it's instantiated on the first call
to request.getfixturevalue("report_context"), which today happens
inside the gated step and _sift_parents fixtures. If every test in
the session is excluded via the marker gate, this fixture is never resolved
and no ReportContext (or teardown subprocess) is created.
What gets yielded depends on the mode:
--sift-disabled: a realReportContextagainst a placeholderSiftClientwith_simulate=True. Every test-results write returns a synthesized response without contacting Sift; no log file is written; the replay subprocess never spawns. Test code that callsstep.measure(...)keeps working because bounds are evaluated as usual and routed through the simulate path.--sift-offline: a real ReportContext, but the session-start ping is skipped, all create/update calls go to the JSONL log file, and the import-test-result-log replay subprocess is not spawned at session end.- default (online): verify connectivity via
client_has_connectionbefore constructing the context. A failed ping aborts the session withpytest.exitand points at--sift-offlineand--sift-disabledas escape hatches.
The log-file destination is controlled by
--sift-log-file; defaults to a temp file when unset.
sift_client
¶
sift_client(pytestconfig: Config) -> SiftClient
Default SiftClient resolved from environment variables and ini keys.
Each credential is read from its environment variable first. The URIs
(SIFT_GRPC_URI, SIFT_REST_URI) also fall back to the
sift_grpc_uri / sift_rest_uri ini keys, since they are stable
per-org values that are safe to commit. SIFT_API_KEY is intentionally
env-only; use pytest-dotenv (already a project dependency) to load
it from a .env file kept out of version control.
Projects that need custom construction (TLS toggles, custom timeouts,
etc.) can override this fixture by defining their own sift_client
in their conftest.py; pytest fixture resolution prefers the local
definition.
In --sift-offline mode the missing-credential check is relaxed:
real env vars and ini values still win when set (so the client is
constructible against a real backend even though no calls are made), but
anything still missing is filled with a placeholder. In --sift-disabled
mode the credential resolution is skipped entirely and placeholders are
always used.
sift_report_metadata
¶
Extra report metadata, merged on top of the [tool.sift.pytest.report.metadata]
TOML table.
Returns {} by default. Override this fixture in your conftest to add
metadata computed at runtime (SDK/Python version, CI provider, build id),
which the static TOML table can't express. It's resolved while the report is
built, so it never forces a report to be created on its own: a run that
creates no report (e.g. a unit suite with the Sift gate off) never calls it.
Keys here layer over matching TOML keys; pytest_command is reserved and
always wins.
step
¶
step(
request: FixtureRequest,
pytestconfig: Config,
_sift_parents: None,
) -> Generator[NewStep | None, None, None]
Create an outer step for the function when the Sift gate is on.
Resolves the gate via gate_enabled: the sift_exclude marker forces off,
sift_include forces on, otherwise the sift_autouse ini default applies.
When on, requests the session report_context lazily; the first gated test
in the session triggers its creation, subsequent gated tests reuse it. In
--sift-disabled mode the report context is backed by a
SiftClient(_simulate=True) placeholder, so every write returns a
synthesized response without contacting Sift.