Architectural Decision Record (ADR-005) introduces a harness adapter layer, so
framework integrations can live in opt-in crates. Phase 9.1 ships the core
contracts in rstest-bdd-harness.
#[scenario] and scenarios! now support harness and attribute-policy macro
parameters. The legacy runtime = "tokio-current-thread" argument in
scenarios! remains supported as compatibility syntax for Tokio harness
selection. The core crate remains immediately useful for adapter authors
building Tokio, Graphical Processing User Interface (GPUI), or other harness
plug-ins.
Defining a harness adapter
Use HarnessAdapter with ScenarioRunRequest<'a, C, T> and
ScenarioRunner<'a, C, T> to execute one scenario closure inside the harness
environment. C is the harness-specific context type:
use rstest_bdd_harness::{
HarnessAdapter, HarnessResult, ScenarioMetadata, ScenarioRunRequest, ScenarioRunner,
};
struct MyHarness;
impl HarnessAdapter for MyHarness {
type Context = ();
fn run<T>(
&self,
request: ScenarioRunRequest<'_, Self::Context, T>,
) -> HarnessResult<T> {
// Optional harness-specific setup using request.metadata().
Ok(request.run_without_context())
}
}
let request = ScenarioRunRequest::new(
ScenarioMetadata::new(
"tests/features/login.feature",
"Successful login",
12,
vec!["@smoke".to_string()],
),
ScenarioRunner::new(|()| "ok"),
);
let harness = MyHarness;
assert_eq!(harness.run(request).expect("harness should not fail"), "ok");
Harnesses that need framework resources can choose a non-unit context type and
pass it through request.run(context). For example, a GPUI harness can use
TestAppContext, and a Bevy harness can use bevy::ecs::World.
The built-in StdHarness implements the same trait and runs the closure
synchronously without an async runtime or UI harness.
This user guide focuses on how to use the delivered harness API. The design
document records the underlying trust model and architectural rationale, in
particular the path-based first-party policy mapping and the associated
Context handoff introduced by ADR-007.
Defining an attribute policy
AttributePolicy supplies test attributes that macro expansion should apply.
The default policy emits only #[rstest::rstest]:
use rstest_bdd_harness::{AttributePolicy, DefaultAttributePolicy};
let attrs = DefaultAttributePolicy::test_attributes();
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].render(), "#[rstest::rstest]");
Adapter crates can define custom policies with additional attributes, such as
Tokio runtime options, while keeping those dependencies out of the core
rstest-bdd crates. At present, macro code generation recognizes only the
first-party canonical policy paths for Tokio and GPUI; unknown third-party
policy paths fall back to #[rstest::rstest].
Verifying an attribute policy
rstest-bdd-harness ships the conformance check every adapter policy is
expected to pass. Supply the attributes the policy should render, in order:
use rstest_bdd_harness::DefaultAttributePolicy;
use rstest_bdd_harness::policy_conformance::assert_attribute_policy_conformance;
assert_attribute_policy_conformance::<DefaultAttributePolicy>(
&["#[rstest::rstest]"],
);
It pins three invariants, panicking with a descriptive message when any of them is violated:
- Emit — the policy returns exactly as many attributes as expected.
- Render — each attribute renders to the corresponding expected string, in the same order.
rstestis first — the leading attribute path isrstest::rstest, so fixture expansion runs before the runtime-specific test macro.
Call it from a single #[test] in your adapter crate rather than
re-implementing the assertions; only the expected rendered attributes are
adapter-specific.
One placement caveat: if the adapter's library target sets test = false in
its manifest — as the GPUI adapter does — an in-module #[cfg(test)] block is
never compiled, so the check would silently never run. Put the call in an
integration test under tests/ in that case.