Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-050 · Case file with fixtures · Case 22 of 694 · Cargo test-surface evidence

Why a Rust Doctest Sees Different cfg Values From a Normal Test

rustdoc extracts examples into generated test crates. Compare the rustdoc invocation and public dependency view instead of assuming cfg(test) and unit-test visibility.

Reviewed
Rust
stable Rust, rustdoc stable, Cargo stable
Targets
documented library target
Profiles
test, doctest

Direct answer

What this Rust failure means

Why it happens
rustdoc extracts examples into generated test crates and invokes compilation with a context that is not identical to the library's ordinary test target.
First discriminating check
Capture rustdoc and rustc invocations, then print the relevant cfg values from an extracted minimal doctest and a normal test.

A documentation example is not pasted into the library's unit-test module. Rustdoc extracts the code, creates test source, compiles it as a doctest executable, links it against the public crate, and runs it. Cargo documents that these blocks are compiled on the fly and run in separate processes.

This model explains why imports, privacy, configuration, and working-directory assumptions can differ from an ordinary #[test] inside src/lib.rs.

Start by identifying the test kind

I separate three things:

  • A unit test is compiled inside the library test target and can access private items in its module tree.
  • An integration test under tests/ is a separate crate using the library publicly.
  • A doctest is extracted by rustdoc into generated source and uses the library as an external consumer.

If the behavior matches the integration test but not the unit test, public visibility or cfg(test) assumptions are likely.

The failing library makes this comparison directly: its unit test can call a #[cfg(test)] helper, while its doctest cannot find that helper through the external crate. The repaired library keeps the helper private to tests and documents the public function instead. The Atlas verifier runs cargo test --lib and cargo test --doc separately, so it cannot mistake a general test failure for this boundary.

cfg(doctest) has a specific purpose

Rustdoc sets cfg(doctest) when compiling a crate for doctest collection with its test mode. The rustdoc book shows using it to add items which exist only while collecting documentation tests.

That does not mean every extracted code block should be designed as if it were an internal unit-test module. Doctests still link against public items of the crate. Private helpers belong in unit tests unless a deliberate doctest-only public scaffold is justified.

I avoid broad code like:

#[cfg(any(test, doctest))]
pub fn skip_security_check() { /* ... */ }

Configuration for test support must not silently change the behavior being documented.

Inspect configuration directly

A minimal example can report compile-time configuration:

/// ```
/// assert!(cfg!(target_pointer_width = "64"));
/// ```
pub fn pointer_sized_operation() {}

For custom conditions, the build script can emit checked cfg values. I capture the actual rustdoc command with verbose Cargo output and compare it with the rustc invocation for unit and integration targets.

I record target triple, features, RUSTDOCFLAGS, and build-script output. Testing an assumption in source is more reliable than guessing which command Cargo generated.

Crate injection changes imports

Rustdoc normally injects an external crate declaration when needed, and examples commonly refer to the package by its public crate name:

/// ```
/// use my_library::Client;
/// let client = Client::new();
/// ```

Code which works only with use crate::private_module in a unit test does not represent what a user can write. The doctest failure is useful API feedback.

Crate names can also differ from package names when the library target has an explicit name. I use the import name exposed by the library target.

Rustdoc has controls for crate injection, but disabling it globally should solve a deliberate documentation design, not one broken snippet.

Features and optional APIs

A doctest may demonstrate an item behind a feature not enabled by the current test command. I make the feature requirement visible in the documentation and ensure CI runs the intended feature sets:

cargo test --doc --features transport-tls

I do not make examples pass only under --all-features if users commonly build a smaller set. Mutually exclusive features can make all-features testing invalid, so the project needs an explicit matrix.

Target-specific examples may use ignore with a reason or conditional documentation, but a permanently ignored example is no longer compilation evidence. no_run still compiles and is useful for operations which should not execute in tests.

Working directories are another difference

Cargo describes the compilation and execution directories for doctests. Relative file assumptions can still become confusing across workspaces and packages.

Examples should normally use embedded data, temporary directories, or paths based on explicit inputs. Depending on the command's current directory teaches users an API pattern which may fail outside the repository.

Doctests run concurrently

Documentation test executables may run in parallel. Two examples using the same port or filename can interfere even when each passes alone. Cargo --jobs does not necessarily control doctest execution the way people expect.

I isolate external resources and avoid process-global mutation just as I do for integration tests.

The first discriminating extraction

When a block is hard to understand, I copy its expanded visible code into a small temporary crate which depends on the packaged library. This tells me whether the failure is public API usage or rustdoc-specific generation.

For rustdoc-specific cases, unstable tooling can persist generated doctests for investigation, but the durable regression remains a normal cargo test --doc command on stable.

False repairs

Making a private item public only for the doctest can expand API accidentally. Adding ignore removes the test. Copying internal setup into every block can hide that the public API is too hard to initialize.

I first decide what a reader should be able to paste into an external crate. The documentation test should check that contract.

The regression proof

My CI runs unit, integration, and documentation tests as named stages with the supported target and feature matrix. The failing example gets a companion integration test when the public behavior is important.

I keep any necessary cfg(doctest) narrow and documented. Once the generated-crate model is explicit, the difference is no longer mysterious: the doctest is checking the code from a user's side of the crate boundary.