Skip to content

Recognize rstest and rstest_parametrize as test-only code in Whitaker #189

@leynos

Description

@leynos

Problem statement

Whitaker currently fails to recognize test functions declared with #[rstest] or #[rstest_parametrize] as test-only code. That causes false positives in lints that intentionally forbid patterns such as .expect() or .unwrap() outside tests.

This is a Whitaker bug or gap in test-context detection, not an rstest bug.

Current behavior

Given code like this:

#[rstest]  
#[case::syntactic(LockFailureKind::Syntactic, "syntax error")]  
#[case::semantic(LockFailureKind::Semantic, "type error")]  
fn content_transaction_rejects_lock_failure(  
    #[case] kind: LockFailureKind,  
    #[case] message: &str,  
) {  
    let dir = TempDir::new().expect("temp dir");  
    // ...  
}

Whitaker reports .expect() as if it were being called outside test-only code.

Example diagnostic:

error: Avoid calling expect on `std::result::Result<tempfile::TempDir, std::io::Error>` outside test-only code.  
  --> crates/weaverd/src/safety_harness/transaction/content_transaction_tests.rs:62:15  
   |  
62 |     let dir = TempDir::new().expect("temp dir");  
   |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  
   |  
   = note: The call originates within function `content_transaction_rejects_lock_failure` which is not recognised as a test.

That diagnosis is wrong. The function is test code.

Root cause

Whitaker’s current test-detection logic appears to rely on patterns such as:

  • #[test]
  • well-known async test attributes such as #[tokio::test]
  • possibly naming heuristics such as test_*

That misses rstest, because #[rstest] and related forms are procedural macro attributes that imply test semantics without necessarily exposing a plain #[test] attribute at the AST stage Whitaker inspects.

In other words, Whitaker currently treats “is this syntactically a standard Rust test?” as too close to “is this test-only code?”. Those are not equivalent.

Expected behavior

Whitaker should classify functions as test-only code when they are annotated with rstest test macros, including at least:

  • #[rstest]

  • #[rstest_parametrize]

It may also be worth considering whether supporting associated rstest patterns such as #[case::...] improves robustness, but the primary detection should sit on the function-level attributes.

For the example above, Whitaker should not emit no_expect_outside_tests or similar diagnostics.

Scope

This affects any Whitaker lint that needs to distinguish production code from test-only code, including but not limited to:

  • no_expect_outside_tests
  • no_unwrap_outside_tests
  • any future lint that grants more latitude inside tests

It also matters for projects using:

  • rstest
  • rstest-bdd
  • other proc-macro-based test frameworks with nonstandard test markers

Why Whitaker should handle this

It is reasonable for lint tools to recognize widely used test frameworks directly. Requiring every upstream test framework to emit custom tooling hints, or requiring every downstream project to configure special cases, creates unnecessary friction.

rstest is established enough that first-class support in Whitaker makes sense.

More importantly, the semantic question Whitaker is trying to answer is not “does this function have a literal #[test] attribute?” but “is this function test-only code?”. #[rstest] clearly satisfies that criterion.

Workarounds in the meantime

Projects can work around the issue, but all current workarounds are inferior to fixing detection in Whitaker.

Return Result from tests

#[rstest]  
#[case::syntactic(LockFailureKind::Syntactic, "syntax error")]  
fn content_transaction_rejects_lock_failure(  
    #[case] kind: LockFailureKind,  
    #[case] message: &str,  
) -> Result<(), String> {  
    let dir = TempDir::new().map_err(|e| format!("temp dir: {e}"))?;  
    // ...  
    Ok(())  
}

This avoids .expect(), but it changes test style and pushes projects toward ceremony they may not want.

Add redundant attributes

#[test]  
#[rstest]  
#[case::foo(1)]  
fn my_test(#[case] x: i32) { ... }

This is clumsy, may be fragile, and should not be necessary merely to satisfy a lint tool.

Add framework-specific configuration

Whitaker could support configurable test attributes, for example:

[no_expect_outside_tests]  
test_attributes = ["test", "rstest", "rstest_parametrize"]

That is better than nothing, but rstest is common enough that built-in support is preferable.

Suggested fix

Extend Whitaker’s existing “is this test-only code?” detection to recognize rstest attributes directly.

At minimum:

  • treat a function annotated with #[rstest] as test-only
  • treat a function annotated with #[rstest_parametrize] as test-only

If Whitaker already has shared test-detection logic used by multiple lints, this should go there rather than being implemented ad hoc in one lint.

It would also be sensible to structure the detection as a broader framework mechanism, so additional test frameworks can be supported consistently.

Acceptance criteria

  • A function annotated with #[rstest] is recognized as test-only code.
  • A function annotated with #[rstest_parametrize] is recognized as test-only code.
  • no_expect_outside_tests does not fire for .expect() calls inside such functions.
  • Existing support for #[test] and async test frameworks continues to work.
  • The test-detection logic remains reusable across Whitaker lints.

Example regression test

#[rstest]  
#[case(1)]  
fn rstest_allows_expect_in_test_context(#[case] value: i32) {  
    let parsed = Some(value).expect("value should be present");  
    assert_eq!(parsed, 1);  
}

Whitaker should not report no_expect_outside_tests here.

Environment

Investigation in Whitaker

I reviewed the current test-detection path before assuming the recent Tokio work had dropped rstest support.

This does not look like a simple matcher regression. The shared test-like attribute registry in common/src/attributes/mod.rs still includes:

  • rstest
  • rstest::rstest
  • case
  • rstest::case

The HIR adapter in src/hir.rs still delegates to that shared matcher, and no_expect_outside_tests still uses that path in crates/no_expect_outside_tests/src/driver/mod.rs.

That suggests the recent Tokio work did not simply remove rstest from the recognised attribute list.

Likely culprit: hollow rstest regression coverage

The more likely problem is that Whitaker’s existing rstest coverage is too weak to catch a real regression.

The positive rstest UI fixture is:

  • crates/no_expect_outside_tests/ui/pass_expect_in_rstest.rs

That fixture depends on the auxiliary proc-macro:

  • crates/no_expect_outside_tests/ui/auxiliary/rstest.rs

The auxiliary macro simply prepends a literal #[test] to the item. That means the current UI test proves only that “a proc macro which emits #[test] is accepted”. It does not prove that real rstest is recognised correctly by Whitaker.

In other words, if native rstest detection were broken, this test could still pass.

Contrast with the recent Tokio work

The recent Tokio fix explicitly hardened coverage with a real example-based regression:

  • crates/no_expect_outside_tests/src/lib_ui_tests.rs
  • crates/no_expect_outside_tests/examples/pass_expect_in_tokio_test_harness.rs

That regression compiles a real #[tokio::test] example under --test, precisely because the earlier proc-macro fixture only covered token shape rather than the real lowering and harness path.

Whitaker currently lacks an equivalent end-to-end regression for real rstest usage.

Implication

The safest reading is:

  • the recent Tokio work may have exposed the gap, but
  • the underlying problem is more likely that Whitaker has a hollow rstest regression test which masks a real detection failure.

So the issue is probably not “Tokio support removed rstest from the matcher”. It is more likely “Whitaker does not currently prove that real rstest tests are classified as test-only code”.

Proposed follow-up

  1. Add a real rstest example-based regression using the actual rstest crate rather than the current auxiliary proc-macro that injects #[test].

  2. If that regression fails, inspect the lowered or HIR shape to determine whether rstest needs:

    • only direct source-level attribute recognition, or
    • an additional fallback path analogous to the Tokio harness-descriptor recovery.
  3. Treat the current pass_expect_in_rstest fixture as insufficient on its own for future regressions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions