From 9c41c42bc444be7da8d859c46759ba9f7cff3974 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 01:04:37 +0100 Subject: [PATCH 1/3] Convert footnotes to GFM --- docs/rust-doctest-dry-guide.md | 568 +++++++++++++++++++++++++-------- 1 file changed, 434 insertions(+), 134 deletions(-) diff --git a/docs/rust-doctest-dry-guide.md b/docs/rust-doctest-dry-guide.md index f039be03..270ad63b 100644 --- a/docs/rust-doctest-dry-guide.md +++ b/docs/rust-doctest-dry-guide.md @@ -2,71 +2,153 @@ ## The `rustdoc` Compilation Model: A Foundational Perspective -To master the art of writing effective documentation tests in Rust, one must first understand the foundational principles upon which the `rustdoc` tool operates. Its behavior, particularly its testing mechanism, is not an arbitrary collection of features but a direct consequence of a deliberate design philosophy. The core of this philosophy is that every doctest should validate the public API of a crate from the perspective of an external user. This single principle dictates the entire compilation model and explains both the power and the inherent limitations of doctests. +To master the art of writing effective documentation tests in Rust, one must +first understand the foundational principles upon which the `rustdoc` tool +operates. Its behavior, particularly its testing mechanism, is not an arbitrary +collection of features but a direct consequence of a deliberate design +philosophy. The core of this philosophy is that every doctest should validate +the public API of a crate from the perspective of an external user. This single +principle dictates the entire compilation model and explains both the power and +the inherent limitations of doctests. ### 1.1 The "Separate Crate" Paradigm -At its heart, `rustdoc` treats each documentation test not as a snippet of code running within the library's own context, but as an entirely separate, temporary crate.1 When a developer executes - -`cargo test --doc`, `rustdoc` initiates a multi-stage process for every code block found in the documentation comments 3: - -1. **Parsing and Extraction**: `rustdoc` first parses the source code of the library, resolving conditional compilation attributes (`#[cfg]`) to determine which items are active and should be documented for the current target.2 It then extracts all code examples enclosed in triple-backtick fences (\`\`\`\`). - -2. **Code Generation**: For each extracted code block, `rustdoc` performs a textual transformation to create a complete, self-contained Rust program. If the block does not already contain a `fn main()`, the code is wrapped within one. Crucially, `rustdoc` also injects an `extern crate ;` statement, where `` is the name of the library being documented. This makes the library under test available as an external dependency.3 - -3. **Individual Compilation**: `rustdoc` then invokes the Rust compiler (`rustc`) separately for *each* of these newly generated miniature programs. Each one is compiled and linked against the already-compiled version of the main library.2 - -4. **Execution and Verification**: Finally, if compilation succeeds, the resulting executable is run. The test is considered to have passed if the program runs to completion without panicking. The executable is then deleted.2 - -The significance of this model cannot be overstated. It effectively transforms every doctest into a true integration test.6 The test code does not have special access to the library's internals; it interacts with the library's API precisely as a downstream crate would, providing a powerful guarantee that the public-facing examples are correct and functional.1 +At its heart, `rustdoc` treats each documentation test not as a snippet of code +running within the library's own context, but as an entirely separate, +temporary crate.[^1] When a developer executes + +`cargo test --doc`, `rustdoc` initiates a multi-stage process for every code +block found in the documentation comments [^3]: + +1. **Parsing and Extraction**: `rustdoc` first parses the source code of the + library, resolving conditional compilation attributes (`#[cfg]`) to + determine which items are active and should be documented for the current + target.[^2] It then extracts all code examples enclosed in triple-backtick + fences (\`\`\`\`). + +2. **Code Generation**: For each extracted code block, `rustdoc` performs a + textual transformation to create a complete, self-contained Rust program. If + the block does not already contain a `fn main()`, the code is wrapped within + one. Crucially, `rustdoc` also injects an `extern crate ;` + statement, where `` is the name of the library being documented. + This makes the library under test available as an external dependency.[^3] + +3. **Individual Compilation**: `rustdoc` then invokes the Rust compiler + (`rustc`) separately for *each* of these newly generated miniature programs. + Each one is compiled and linked against the already-compiled version of the + main library.[^2] + +4. **Execution and Verification**: Finally, if compilation succeeds, the + resulting executable is run. The test is considered to have passed if the + program runs to completion without panicking. The executable is then + deleted.[^2] + +The significance of this model cannot be overstated. It effectively transforms +every doctest into a true integration test.[^6] The test code does not have +special access to the library's internals; it interacts with the library's API +precisely as a downstream crate would, providing a powerful guarantee that the +public-facing examples are correct and functional.[^1] ### 1.2 First-Order Consequences of the Model -This "separate crate" paradigm has two immediate and significant consequences that shape all advanced doctesting patterns. - -First, **API visibility is strictly limited to public items**. Because the doctest is compiled as an external crate, it can only access functions, structs, traits, and modules marked with the `pub` keyword. It has no access to private items or even crate-level public items (e.g., `pub(crate)`). This is not a bug or an oversight but a fundamental aspect of the design, enforcing the perspective of an external consumer.1 - -Second, the model has **profound performance implications**. The process of invoking `rustc` to compile and link a new executable for every single doctest is computationally expensive. For small projects, this overhead is negligible. However, for large libraries with hundreds of doctests, the cumulative compilation time can become a significant bottleneck in the development and CI/CD cycle, a common pain point in the Rust community.2 - -The architectural purity of the `rustdoc` model—its insistence on simulating an external user—creates a fundamental trade-off. On one hand, it provides an unparalleled guarantee that the public documentation is accurate and that the examples work as advertised, creating true "living documentation".8 On the other hand, this same purity prevents the use of doctests for verifying documentation of internal, private APIs. This forces a bifurcation of documentation strategy. Public-facing documentation can be tied directly to working, tested code. Internal documentation for maintainers, which is equally vital for a project's health, cannot be verified with the same tools. Examples for private functions must either be marked as - -`ignore`, forgoing the test guarantee, or be duplicated in separate unit tests, violating the "Don't Repeat Yourself" (DRY) principle.1 This reveals that - -`rustdoc`'s design implicitly prioritizes the integrity of the public contract over the convenience of a single, unified system for testable documentation of both public and private code. +This "separate crate" paradigm has two immediate and significant consequences +that shape all advanced doctesting patterns. + +First, **API visibility is strictly limited to public items**. Because the +doctest is compiled as an external crate, it can only access functions, +structs, traits, and modules marked with the `pub` keyword. It has no access to +private items or even crate-level public items (e.g., `pub(crate)`). This is +not a bug or an oversight but a fundamental aspect of the design, enforcing the +perspective of an external consumer.[^1] + +Second, the model has **profound performance implications**. The process of +invoking `rustc` to compile and link a new executable for every single doctest +is computationally expensive. For small projects, this overhead is negligible. +However, for large libraries with hundreds of doctests, the cumulative +compilation time can become a significant bottleneck in the development and +CI/CD cycle, a common pain point in the Rust community.[^2] + +The architectural purity of the `rustdoc` model—its insistence on simulating an +external user—creates a fundamental trade-off. On one hand, it provides an +unparalleled guarantee that the public documentation is accurate and that the +examples work as advertised, creating true "living documentation".[^8] On the +other hand, this same purity prevents the use of doctests for verifying +documentation of internal, private APIs. This forces a bifurcation of +documentation strategy. Public-facing documentation can be tied directly to +working, tested code. Internal documentation for maintainers, which is equally +vital for a project's health, cannot be verified with the same tools. Examples +for private functions must either be marked as + +`ignore`, forgoing the test guarantee, or be duplicated in separate unit tests, +violating the "Don't Repeat Yourself" (DRY) principle.[^1] This reveals that + +`rustdoc`'s design implicitly prioritizes the integrity of the public contract +over the convenience of a single, unified system for testable documentation of +both public and private code. ## Authoring Effective Doctests: From Basics to Best Practices -With a solid understanding of the `rustdoc` compilation model, one can move on to the practical craft of authoring doctests. An effective doctest is more than just a block of code; it is a piece of technical communication that should be clear, illustrative, and robust. +With a solid understanding of the `rustdoc` compilation model, one can move on +to the practical craft of authoring doctests. An effective doctest is more than +just a block of code; it is a piece of technical communication that should be +clear, illustrative, and robust. ### 2.1 The Anatomy of a Doctest Doctests reside within documentation comments. Rust recognizes two types: -- **Outer doc comments (**`///`**)**: These document the item that follows them (e.g., a function, struct, or module). This is the most common type.8 +- **Outer doc comments (**`///`**)**: These document the item that follows them + (e.g., a function, struct, or module). This is the most common type.[^8] -- **Inner doc comments (**`//!`**)**: These document the item they are inside of (e.g., a module or the crate itself). They are typically used at the top of `lib.rs` or `mod.rs` to provide crate- or module-level documentation.9 +- **Inner doc comments (**`//!`**)**: These document the item they are inside + of (e.g., a module or the crate itself). They are typically used at the top + of `lib.rs` or `mod.rs` to provide crate- or module-level documentation.[^9] -Within these comments, a code block is denoted by triple backticks (`). While rustdoc defaults to assuming the language is Rust, explicitly adding the rust language specifier (e.g., `rust\`) is considered good practice for clarity.3 +Within these comments, a code block is denoted by triple backticks While +`rustdoc` defaults to assuming the language is Rust, explicitly adding the rust +language specifier (e.g., `rust`) is considered good practice for clarity.[^3] -A doctest is considered to "pass" if it compiles successfully and runs to completion without panicking. To verify that a function produces a specific output, developers should use the standard assertion macros, such as `assert!`, `assert_eq!`, and `assert_ne!`.3 +A doctest is considered to "pass" if it compiles successfully and runs to +completion without panicking. To verify that a function produces a specific +output, developers should use the standard assertion macros, such as `assert!`, +`assert_eq!`, and `assert_ne!`.[^3] ### 2.2 The Philosophy of a Good Example -The purpose of a documentation example extends beyond merely demonstrating syntax. A reader can typically be expected to understand the mechanics of calling a function or instantiating a struct. A truly valuable example illustrates *why* and in *what context* an item should be used.10 It should tell a small story or solve a miniature problem that illuminates the item's purpose. For instance, an example for +The purpose of a documentation example extends beyond merely demonstrating +syntax. A reader can typically be expected to understand the mechanics of +calling a function or instantiating a struct. A truly valuable example +illustrates *why* and in *what context* an item should be used.[^10] It should +tell a small story or solve a miniature problem that illuminates the item's +purpose. For instance, an example for -`String::clone()` should not just show `hello.clone();`, but should demonstrate a scenario where ownership rules necessitate creating a copy.10 +`String::clone()` should not just show `hello.clone();`, but should demonstrate +a scenario where ownership rules necessitate creating a copy.[^10] -To achieve this, examples must be clear and concise. Any code that is not directly relevant to the point being made—such as complex setup, boilerplate, or unrelated logic—should be hidden to avoid distracting the reader.3 +To achieve this, examples must be clear and concise. Any code that is not +directly relevant to the point being made—such as complex setup, boilerplate, +or unrelated logic—should be hidden to avoid distracting the reader.[^3] ### 2.3 Ergonomic Error Handling: Taming the `?` Operator -One of the most common ergonomic hurdles in writing doctests involves handling functions that return a `Result`. The question mark (`?`) operator is the idiomatic way to propagate errors in Rust, but it presents a challenge for doctests. The implicit `fn main()` wrapper generated by `rustdoc` has a return type of `()`, while the `?` operator can only be used in a function that returns a `Result` or `Option`. This mismatch leads to a compilation error.3 +One of the most common ergonomic hurdles in writing doctests involves handling +functions that return a `Result`. The question mark (`?`) operator is the +idiomatic way to propagate errors in Rust, but it presents a challenge for +doctests. The implicit `fn main()` wrapper generated by `rustdoc` has a return +type of `()`, while the `?` operator can only be used in a function that +returns a `Result` or `Option`. This mismatch leads to a compilation error.[^3] -Using `.unwrap()` or `.expect()` in examples is strongly discouraged. It is considered an anti-pattern because users often copy example code verbatim, and encouraging panicking on errors is contrary to robust application design.10 Instead, two canonical solutions exist. +Using `.unwrap()` or `.expect()` in examples is strongly discouraged. It is +considered an anti-pattern because users often copy example code verbatim, and +encouraging panicking on errors is contrary to robust application design.[^10] +Instead, two canonical solutions exist. -Solution 1: The Explicit main Function +Solution [^1]: The Explicit main Function -The most transparent and recommended approach is to manually write a main function within the doctest that returns a Result. This leverages the Termination trait, which is implemented for Result. The surrounding boilerplate can then be hidden from the rendered documentation. +The most transparent and recommended approach is to manually write a main +function within the doctest that returns a Result. This leverages the +Termination trait, which is implemented for Result. The surrounding boilerplate +can then be hidden from the rendered documentation. ```rust /// # Examples @@ -83,11 +165,14 @@ The most transparent and recommended approach is to manually write a main functi /// ``` ``` -In this pattern, the reader only sees the core, fallible code, while the test itself is a complete, well-behaved program.10 +In this pattern, the reader only sees the core, fallible code, while the test +itself is a complete, well-behaved program.[^10] -Solution 2: The Implicit Result-Returning main +Solution [^2]: The Implicit Result-Returning main -rustdoc provides a lesser-known but more concise shorthand for this exact scenario. If a code block ends with the literal token (()), rustdoc will automatically wrap the code in a main function that returns a Result. +rustdoc provides a lesser-known but more concise shorthand for this exact +scenario. If a code block ends with the literal token (()), rustdoc will +automatically wrap the code in a main function that returns a Result. ```rust /// # Examples @@ -99,61 +184,135 @@ rustdoc provides a lesser-known but more concise shorthand for this exact scenar /// ``` ``` -This is functionally equivalent to the explicit `main` but requires less boilerplate. However, it is critical that the `(())` be written as a single, contiguous sequence of characters, as `rustdoc`'s detection mechanism is purely textual and will not recognize `( () )`.3 +This is functionally equivalent to the explicit `main` but requires less +boilerplate. However, it is critical that the `(())` be written as a single, +contiguous sequence of characters, as `rustdoc`'s detection mechanism is purely +textual and will not recognize `( () )`.[^3] ### 2.4 The Power of Hidden Lines (`#`): Creating Clean Examples -The mechanism that makes clean, focused examples possible is the "hidden line" syntax. Any line in a doctest code block that begins with a `#` character (optionally preceded by whitespace) will be compiled and executed as part of the test, but it will be completely omitted from the final HTML documentation rendered for the user.3 - -This feature is essential for bridging the gap between what makes a good, human-readable example and what constitutes a complete, compilable program. Its primary use cases include: - -1. **Hiding** `main` **Wrappers**: As demonstrated in the error-handling examples, the entire `fn main() -> Result<...> {... }` and `Ok(())` scaffolding can be hidden, presenting the user with only the relevant code.10 - -2. **Hiding Setup Code**: If an example requires some preliminary setup—like creating a temporary file, defining a helper struct for the test, or initializing a server—this logic can be hidden to keep the example focused on the API item being documented.3 - -3. **Hiding** `use` **Statements**: While often useful to show which types are involved, `use` statements can sometimes be hidden to de-clutter very simple examples. - -The existence of features like hidden lines and the `(())` shorthand reveals a core tension in `rustdoc`'s design. The compilation model is rigid: every test must be a valid, standalone program.2 However, the ideal documentation example is often just a small, illustrative snippet that is not a valid program on its own.10 These ergonomic features are pragmatic "patches" designed to resolve this conflict. They allow the developer to inject the necessary boilerplate to satisfy the compiler without burdening the human reader with irrelevant details. Understanding them as clever workarounds, rather than as first-class language features, helps explain their sometimes quirky, text-based behavior. +The mechanism that makes clean, focused examples possible is the "hidden line" +syntax. Any line in a doctest code block that begins with a `#` character +(optionally preceded by whitespace) will be compiled and executed as part of +the test, but it will be completely omitted from the final HTML documentation +rendered for the user.[^3] + +This feature is essential for bridging the gap between what makes a good, +human-readable example and what constitutes a complete, compilable program. Its +primary use cases include: + +1. **Hiding** `main` **Wrappers**: As demonstrated in the error-handling + examples, the entire `fn main() -> Result<...> {... }` and `Ok(())` + scaffolding can be hidden, presenting the user with only the relevant + code.[^10] + +2. **Hiding Setup Code**: If an example requires some preliminary setup—like + creating a temporary file, defining a helper struct for the test, or + initializing a server—this logic can be hidden to keep the example focused + on the API item being documented.[^3] + +3. **Hiding** `use` **Statements**: While often useful to show which types are + involved, `use` statements can sometimes be hidden to de-clutter very simple + examples. + +The existence of features like hidden lines and the `(())` shorthand reveals a +core tension in `rustdoc`'s design. The compilation model is rigid: every test +must be a valid, standalone program.[^2] However, the ideal documentation +example is often just a small, illustrative snippet that is not a valid program +on its own.[^10] These ergonomic features are pragmatic "patches" designed to +resolve this conflict. They allow the developer to inject the necessary +boilerplate to satisfy the compiler without burdening the human reader with +irrelevant details. Understanding them as clever workarounds, rather than as +first-class language features, helps explain their sometimes quirky, text-based +behavior. ## Advanced Doctest Control and Attributes -Beyond basic pass/fail checks, `rustdoc` provides a suite of attributes to control doctest behavior with fine-grained precision. These attributes, placed in the header of a code block (e.g., \`\`\`\`ignore\`), allow developers to handle expected failures, non-executable examples, and other complex scenarios. +Beyond basic pass/fail checks, `rustdoc` provides a suite of attributes to +control doctest behavior with fine-grained precision. These attributes, placed +in the header of a code block (e.g., \`\`\`\`ignore\`), allow developers to +handle expected failures, non-executable examples, and other complex scenarios. ### 3.1 A Comparative Analysis of Doctest Attributes -Choosing the correct attribute is critical for communicating the intent of an example and ensuring the test suite provides meaningful feedback. The following table provides a comparative reference for the most common doctest attributes. +Choosing the correct attribute is critical for communicating the intent of an +example and ensuring the test suite provides meaningful feedback. The following +table provides a comparative reference for the most common doctest attributes. - - -

Attribute

Action

Test Outcome

Primary Use Case & Caveats

ignore

Skips both compilation and execution.

ignored

Use Case: For pseudo-code, examples known to be broken, or to temporarily disable a test. Caveat: Provides no guarantee that the code is even syntactically correct. Generally discouraged in favor of more specific attributes.3

should_panic

Compiles and runs the code. The test passes if the code panics.

ok on panic, failed if it does not panic.

Use Case: Demonstrating functions that are designed to panic on invalid input (e.g., indexing out of bounds).

compile_fail

Attempts to compile the code. The test passes if compilation fails.

ok on compilation failure, failed if it compiles successfully.

Use Case: Illustrating language rules, such as the borrow checker or type system constraints. Caveat: Highly brittle. A future Rust version might make the code valid, causing the test to unexpectedly fail.4

no_run

Compiles the code but does not execute it.

ok if compilation succeeds.

Use Case: Essential for examples with undesirable side effects in a test environment, such as network requests, filesystem I/O, or launching a GUI. Guarantees the example is valid Rust code without running it.5

edition2021

Compiles the code using the specified Rust edition's rules.

ok on success.

Use Case: Demonstrating syntax or idioms that are specific to a particular Rust edition (e.g., edition2018, edition2021).4

+| Attribute | Action | Test Outcome | Primary Use Case & Caveats | +| ------------ | ------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ignore | Skips both compilation and execution. | ignored | Use Case: For pseudo-code, examples known to be broken, or to temporarily disable a test. Caveat: Provides no guarantee that the code is even syntactically correct. Generally discouraged in favor of more specific attributes.3 | +| should_panic | Compiles and runs the code. The test passes if the code panics. | ok on panic, failed if it does not panic. | Use Case: Demonstrating functions that are designed to panic on invalid input (e.g., indexing out of bounds). | +| compile_fail | Attempts to compile the code. The test passes if compilation fails. | ok on compilation failure, failed if it compiles successfully. | Use Case: Illustrating language rules, such as the borrow checker or type system constraints. Caveat: Highly brittle. A future Rust version might make the code valid, causing the test to unexpectedly fail.4 | +| no_run | Compiles the code but does not execute it. | ok if compilation succeeds. | Use Case: Essential for examples with undesirable side effects in a test environment, such as network requests, filesystem I/O, or launching a GUI. Guarantees the example is valid Rust code without running it.5 | +| edition2021 | Compiles the code using the specified Rust edition's rules. | ok on success. | Use Case: Demonstrating syntax or idioms that are specific to a particular Rust edition (e.g., edition2018, edition2021).4 | ### 3.2 Detailed Attribute Breakdown -- `ignore`: This is the bluntest instrument in the toolbox. It tells `rustdoc` to do nothing with the code block. It is almost always better to either fix the example using hidden lines or use a more descriptive attribute like `no_run`.3 Its main legitimate use is for non-Rust code blocks or illustrative pseudo-code. - -- `should_panic`: This attribute inverts the normal test condition. It is used to document and verify behavior that intentionally results in a panic. The test will fail if the code completes successfully or panics for a reason other than the one expected (if a specific panic message is asserted).3 - -- `compile_fail`: This is a powerful tool for creating educational examples that demonstrate what *not* to do. It is frequently used in documentation about ownership, borrowing, and lifetimes to show code that the compiler will correctly reject. However, developers must be aware of its fragility. An evolution in the Rust language or compiler could make previously invalid code compile, which would break the test.4 - -- `no_run`: This attribute strikes a crucial balance between test verification and practicality. For an example that demonstrates how to download a file from the internet, you want to ensure the example code is syntactically correct and uses the API properly, but you do not want your CI server to actually perform a network request every time tests are run. `no_run` provides this guarantee by compiling the code without executing it.5 - -- `edition20xx`: This attribute allows an example to be tested against a specific Rust edition. This is important for crates that support multiple editions and need to demonstrate edition-specific features or migration paths.4 +- `ignore`: This is the bluntest instrument in the toolbox. It tells `rustdoc` + to do nothing with the code block. It is almost always better to either fix + the example using hidden lines or use a more descriptive attribute like + `no_run`.[^3] Its main legitimate use is for non-Rust code blocks or + illustrative pseudo-code. + +- `should_panic`: This attribute inverts the normal test condition. It is used + to document and verify behavior that intentionally results in a panic. The + test will fail if the code completes successfully or panics for a reason + other than the one expected (if a specific panic message is asserted).[^3] + +- `compile_fail`: This is a powerful tool for creating educational examples + that demonstrate what *not* to do. It is frequently used in documentation + about ownership, borrowing, and lifetimes to show code that the compiler will + correctly reject. However, developers must be aware of its fragility. An + evolution in the Rust language or compiler could make previously invalid code + compile, which would break the test.[^4] + +- `no_run`: This attribute strikes a crucial balance between test verification + and practicality. For an example that demonstrates how to download a file + from the internet, you want to ensure the example code is syntactically + correct and uses the API properly, but you do not want your CI server to + actually perform a network request every time tests are run. `no_run` + provides this guarantee by compiling the code without executing it.[^5] + +- `edition20xx`: This attribute allows an example to be tested against a + specific Rust edition. This is important for crates that support multiple + editions and need to demonstrate edition-specific features or migration + paths.[^4] ## The DRY Principle in Doctests: Managing Shared and Complex Logic -The "Don't Repeat Yourself" (DRY) principle is a cornerstone of software engineering, and it applies to test code as much as it does to production code. As a project grows, it is common for multiple doctests to require the same complex setup logic. Copying and pasting this setup into every doctest using hidden lines is tedious, error-prone, and a clear violation of the DRY principle. +The "Don't Repeat Yourself" (DRY) principle is a cornerstone of software +engineering, and it applies to test code as much as it does to production code. +As a project grows, it is common for multiple doctests to require the same +complex setup logic. Copying and pasting this setup into every doctest using +hidden lines is tedious, error-prone, and a clear violation of the DRY +principle. ### 4.1 The Problem of Shared Setup -Consider a library for interacting with a database. Nearly every doctest might need to perform the same initial steps: spin up a temporary database instance, connect to it, and seed it with some initial data. Repeating this multi-line setup in every single example is inefficient and makes maintenance difficult. A change to the setup process would require updating dozens of doctests. +Consider a library for interacting with a database. Nearly every doctest might +need to perform the same initial steps: spin up a temporary database instance, +connect to it, and seed it with some initial data. Repeating this multi-line +setup in every single example is inefficient and makes maintenance difficult. A +change to the setup process would require updating dozens of doctests. ### 4.2 The `#[cfg(doctest)]` Pattern for Shared Helpers -The canonical solution to this problem involves using a special configuration flag provided by `rustdoc`: `doctest`. A common mistake is to try to place shared test logic in a block guarded by `#[cfg(test)]`. This will not work, because `rustdoc` does not enable the `test` configuration flag during its compilation process; `#[cfg(test)]` is reserved for unit and integration tests run directly by `cargo test`.12 +The canonical solution to this problem involves using a special configuration +flag provided by `rustdoc`: `doctest`. A common mistake is to try to place +shared test logic in a block guarded by `#[cfg(test)]`. This will not work, +because `rustdoc` does not enable the `test` configuration flag during its +compilation process; `#[cfg(test)]` is reserved for unit and integration tests +run directly by `cargo test`.[^12] -Instead, `rustdoc` sets its own unique `doctest` flag. By guarding a module or function with `#[cfg(doctest)]`, developers can write helper code that is compiled and available *only* when `cargo test --doc` is running. This code is excluded from normal production builds and standard unit test runs, preventing any pollution of the final binary or the public API. +Instead, `rustdoc` sets its own unique `doctest` flag. By guarding a module or +function with `#[cfg(doctest)]`, developers can write helper code that is +compiled and available *only* when `cargo test --doc` is running. This code is +excluded from normal production builds and standard unit test runs, preventing +any pollution of the final binary or the public API. -The typical implementation pattern is to create a private helper module within your library: +The typical implementation pattern is to create a private helper module within +your library: ```rust // In lib.rs or a submodule @@ -199,23 +358,45 @@ mod doctest_helpers { pub struct TestContext { /*... */ } ``` -This pattern is the most effective way to achieve DRY doctests. It centralizes setup logic, improves maintainability, and cleanly separates testing concerns from production code.12 +This pattern is the most effective way to achieve DRY doctests. It centralizes +setup logic, improves maintainability, and cleanly separates testing concerns +from production code.[^12] ### 4.3 Advanced DRY: Programmatic Doctest Generation -For highly specialized use cases, such as authoring procedural macros, the DRY principle can be taken a step further. A procedural macro generates code, and it is often desirable to test that the generated code itself contains valid and working documentation. Writing these doctests manually can be exceptionally repetitive. +For highly specialized use cases, such as authoring procedural macros, the DRY +principle can be taken a step further. A procedural macro generates code, and +it is often desirable to test that the generated code itself contains valid and +working documentation. Writing these doctests manually can be exceptionally +repetitive. -Crates like `quote-doctest` address this by allowing developers to programmatically construct a doctest from a `TokenStream`. This enables the generation of doctests from the same source of truth that generates the code they are intended to test, representing the ultimate application of the DRY principle in this domain.14 +Crates like `quote-doctest` address this by allowing developers to +programmatically construct a doctest from a `TokenStream`. This enables the +generation of doctests from the same source of truth that generates the code +they are intended to test, representing the ultimate application of the DRY +principle in this domain.[^14] ## Conditional Compilation Strategies for Doctests -Conditional compilation is a powerful feature of Rust, but it introduces significant complexity when interacting with `rustdoc`. A common source of confusion stems from the failure to distinguish between two separate goals: (1) ensuring platform-specific or feature-gated items *appear* in the documentation, and (2) ensuring doctests for those items *execute* only under the correct conditions. These two goals are achieved with different mechanisms that operate at different stages of the `rustdoc` pipeline. +Conditional compilation is a powerful feature of Rust, but it introduces +significant complexity when interacting with `rustdoc`. A common source of +confusion stems from the failure to distinguish between two separate goals: (1) +ensuring platform-specific or feature-gated items *appear* in the +documentation, and (2) ensuring doctests for those items *execute* only under +the correct conditions. These two goals are achieved with different mechanisms +that operate at different stages of the `rustdoc` pipeline. ### 5.1 Documenting Conditionally Compiled Items: `#[cfg(doc)]` -**The Goal**: To ensure that an item, such as a `struct UnixSocket` that is only available on Unix-like systems, is included in the documentation regardless of which platform `rustdoc` is run on (e.g., when generating docs on a Windows machine). +**The Goal**: To ensure that an item, such as a `struct UnixSocket` that is +only available on Unix-like systems, is included in the documentation +regardless of which platform `rustdoc` is run on (e.g., when generating docs on +a Windows machine). -**The Mechanism**: `rustdoc` always invokes the compiler with the `--cfg doc` flag set. By adding `doc` to an item's `#[cfg]` attribute, a developer can instruct the compiler to include that item specifically for documentation builds.15 +**The Mechanism**: `rustdoc` always invokes the compiler with the `--cfg doc` +flag set. By adding `doc` to an item's `#[cfg]` attribute, a developer can +instruct the compiler to include that item specifically for documentation +builds.[^15] **The Pattern**: @@ -225,21 +406,33 @@ Conditional compilation is a powerful feature of Rust, but it introduces signifi pub struct UnixSocket; ``` -This `any` directive ensures the struct is compiled either when the target OS is `unix` OR when `rustdoc` is running. This correctly makes the item visible in the generated HTML. However, it is crucial to understand that this **does not** make the doctest for `UnixSocket` pass on non-Unix platforms. +This `any` directive ensures the struct is compiled either when the target OS +is `unix` OR when `rustdoc` is running. This correctly makes the item visible +in the generated HTML. However, it is crucial to understand that this **does +not** make the doctest for `UnixSocket` pass on non-Unix platforms. -This distinction highlights the "cfg duality." The `#[cfg(doc)]` attribute controls the *table of contents* of the documentation; it determines which items are parsed and rendered. The actual compilation of a doctest, however, happens in a separate, later stage. In that stage, the `doc` cfg is *not* passed to the compiler.15 The compiler only sees the host +This distinction highlights the "cfg duality." The `#[cfg(doc)]` attribute +controls the *table of contents* of the documentation; it determines which +items are parsed and rendered. The actual compilation of a doctest, however, +happens in a separate, later stage. In that stage, the `doc` cfg is *not* +passed to the compiler.[^15] The compiler only sees the host -`cfg` (e.g., `target_os = "windows"`), so the `UnixSocket` type is not available, and the test fails to compile. `#[cfg(doc)]` affects what is documented, not what is testable. +`cfg` (e.g., `target_os = "windows"`), so the `UnixSocket` type is not +available, and the test fails to compile. `#[cfg(doc)]` affects what is +documented, not what is testable. ### 5.2 Executing Doctests Conditionally: Feature Flags -**The Goal**: To ensure a doctest that relies on an optional crate feature (e.g., a feature named `"serde"`) is only executed when that feature is enabled via `cargo test --doc --features "serde"`. +**The Goal**: To ensure a doctest that relies on an optional crate feature +(e.g., a feature named `"serde"`) is only executed when that feature is enabled +via `cargo test --doc --features "serde"`. Two primary patterns exist to achieve this. -Pattern 1: #\[cfg\] Inside the Code Block +Pattern [^1]: #\[cfg\] Inside the Code Block -This pattern involves placing a #\[cfg\] attribute directly on the code within the doctest itself. +This pattern involves placing a #\[cfg\] attribute directly on the code within +the doctest itself. ```rust /// This example only runs if the "serde" feature is enabled. @@ -254,11 +447,16 @@ This pattern involves placing a #\[cfg\] attribute directly on the code within t /// ``` ``` -When the `"serde"` feature is disabled, the code inside the block is compiled out. The doctest becomes an empty program that runs, does nothing, and is reported as `ok`. While simple to write, this can be misleading, as the test suite reports a "pass" for a test that was effectively skipped.16 +When the `"serde"` feature is disabled, the code inside the block is compiled +out. The doctest becomes an empty program that runs, does nothing, and is +reported as `ok`. While simple to write, this can be misleading, as the test +suite reports a "pass" for a test that was effectively skipped.[^16] -Pattern 2: cfg_attr to Conditionally ignore the Test +Pattern [^2]: cfg_attr to Conditionally ignore the Test -A more explicit and accurate pattern uses the cfg_attr attribute to conditionally add the ignore flag to the doctest's header. This is typically done with inner doc comments (//!). +A more explicit and accurate pattern uses the cfg_attr attribute to +conditionally add the ignore flag to the doctest's header. This is typically +done with inner doc comments (//!). ```rust //! #![cfg_attr(not(feature = "serde"), doc = "```ignore")] @@ -270,11 +468,19 @@ A more explicit and accurate pattern uses the cfg_attr attribute to conditionall //! ``` ``` -With this pattern, if the `"serde"` feature is disabled, the test is marked as `ignored` in the test results, which more accurately reflects its status. If the feature is enabled, the `ignore` is omitted, and the test runs normally. This approach provides clearer feedback but is significantly more verbose and less ergonomic, especially when applied to outer (`///`) doc comments, as the `cfg_attr` must be applied to every single line of the comment.16 +With this pattern, if the `"serde"` feature is disabled, the test is marked as +`ignored` in the test results, which more accurately reflects its status. If +the feature is enabled, the `ignore` is omitted, and the test runs normally. +This approach provides clearer feedback but is significantly more verbose and +less ergonomic, especially when applied to outer (`///`) doc comments, as the +`cfg_attr` must be applied to every single line of the comment.[^16] ### 5.3 Displaying Feature Requirements in Docs: `#[doc(cfg(...))]` -To complement conditional execution, Rust provides a way to visually flag feature-gated items in the generated documentation. This is achieved with the `#[doc(cfg(...))]` attribute, which requires enabling the `#![feature(doc_cfg)]` feature gate at the crate root. +To complement conditional execution, Rust provides a way to visually flag +feature-gated items in the generated documentation. This is achieved with the +`#[doc(cfg(...))]` attribute, which requires enabling the +`#![feature(doc_cfg)]` feature gate at the crate root. ```rust // At the crate root (lib.rs) @@ -286,96 +492,190 @@ To complement conditional execution, Rust provides a way to visually flag featur pub fn function_requiring_serde() { /*... */ } ``` -This will render a banner in the documentation for `function_requiring_serde` that reads, "This is only available when the `serde` feature is enabled." This attribute is purely for documentation generation and is independent of, but often used alongside, the conditional test execution patterns.16 +This will render a banner in the documentation for `function_requiring_serde` +that reads, "This is only available when the `serde` feature is enabled." This +attribute is purely for documentation generation and is independent of, but +often used alongside, the conditional test execution patterns.[^16] ## Doctests in the Wider Project Ecosystem -Doctests are a powerful tool, but they are just one component of a comprehensive testing strategy. Understanding their specific role and limitations is key to maintaining a healthy and well-tested Rust project. +Doctests are a powerful tool, but they are just one component of a +comprehensive testing strategy. Understanding their specific role and +limitations is key to maintaining a healthy and well-tested Rust project. ### 6.1 Choosing the Right Test Type: A Decision Framework -A robust testing strategy leverages three distinct types of tests, each with its own purpose: +A robust testing strategy leverages three distinct types of tests, each with +its own purpose: -- **Doctests**: These are ideal for simple, "happy-path" examples of your public API. Their dual purpose is to provide clear documentation for users and to act as a basic sanity check that the examples remain correct over time. They should be easy to read and focused on illustrating a single concept.6 +- **Doctests**: These are ideal for simple, "happy-path" examples of your + public API. Their dual purpose is to provide clear documentation for users + and to act as a basic sanity check that the examples remain correct over + time. They should be easy to read and focused on illustrating a single + concept.[^6] -- **Unit Tests (**`#[test]` **in** `src/`**)**: These are for testing the nitty-gritty details of your implementation. They are placed in submodules within your source files (often `mod tests {... }`) and are compiled only with `#[cfg(test)]`. Because they live inside the crate, they can access private functions and modules, making them perfect for testing internal logic, edge cases, and specific error conditions.1 +- **Unit Tests (**`#[test]` **in** `src/`**)**: These are for testing the + nitty-gritty details of your implementation. They are placed in submodules + within your source files (often `mod tests {... }`) and are compiled only + with `#[cfg(test)]`. Because they live inside the crate, they can access + private functions and modules, making them perfect for testing internal + logic, edge cases, and specific error conditions.[^1] -- **Integration Tests (in the** `tests/` **directory)**: These test the crate from a completely external perspective, much like doctests. However, they are not constrained by the need to be readable documentation. They are suited for testing complex user workflows, interactions between multiple API entry points, and the overall behavior of the library as a black box.6 +- **Integration Tests (in the** `tests/` **directory)**: These test the crate + from a completely external perspective, much like doctests. However, they are + not constrained by the need to be readable documentation. They are suited for + testing complex user workflows, interactions between multiple API entry + points, and the overall behavior of the library as a black box.[^6] ### 6.2 The Unsolved Problem: Testing Private APIs -As established, the `rustdoc` compilation model makes testing private items in doctests impossible by design.1 The community has developed several workarounds, but each comes with significant trade-offs 1: +As established, the `rustdoc` compilation model makes testing private items in +doctests impossible by design.[^1] The community has developed several +workarounds, but each comes with significant trade-offs [^1]: -1. `ignore` **the test**: This allows the example to exist in the documentation but sacrifices the guarantee of correctness. It is the least desirable option. +1. `ignore` **the test**: This allows the example to exist in the documentation + but sacrifices the guarantee of correctness. It is the least desirable + option. -2. **Make items** `pub` **in a** `detail` **or** `internal` **module**: This compromises API design by polluting the public namespace and exposing implementation details that should be encapsulated. It can lead to misuse by users and makes future refactoring difficult. +2. **Make items** `pub` **in a** `detail` **or** `internal` **module**: This + compromises API design by polluting the public namespace and exposing + implementation details that should be encapsulated. It can lead to misuse by + users and makes future refactoring difficult. -3. **Use** `cfg_attr` **to conditionally make items public**: This involves adding an attribute like `#[cfg_attr(feature = "doctest-private", visibility::make(pub))]` to every private item you wish to test. While robust, it is highly invasive and adds significant boilerplate throughout the codebase. +3. **Use** `cfg_attr` **to conditionally make items public**: This involves + adding an attribute like + `#[cfg_attr(feature = "doctest-private", visibility::make(pub))]` to every + private item you wish to test. While robust, it is highly invasive and adds + significant boilerplate throughout the codebase. -The expert recommendation is to acknowledge this limitation and not fight the tool. Do not compromise a clean API design for the sake of doctests. Use doctests for their intended purpose—verifying public API examples—and rely on dedicated unit tests for verifying private logic. The lack of a clean solution for test-verifying private documentation is a known and accepted trade-off within the Rust ecosystem. +The expert recommendation is to acknowledge this limitation and not fight the +tool. Do not compromise a clean API design for the sake of doctests. Use +doctests for their intended purpose—verifying public API examples—and rely on +dedicated unit tests for verifying private logic. The lack of a clean solution +for test-verifying private documentation is a known and accepted trade-off +within the Rust ecosystem. ### 6.3 Practical Challenges and Solutions -Beyond architectural considerations, developers face several practical, real-world challenges when working with doctests. - -- **The** `README.md` **Dilemma**: A project's `README.md` file serves multiple audiences. It needs to render cleanly on platforms like GitHub and crates.io, where hidden lines (`#...`) look like ugly, commented-out code. At the same time, it should contain testable examples, which often require hidden lines for setup.11 The best practice is to avoid maintaining the README manually. Instead, use a tool like - - `cargo-readme`. This tool generates a `README.md` file from your crate-level documentation (in `lib.rs`), automatically stripping out the hidden lines. This provides a single source of truth that is both fully testable via `cargo test --doc` and produces a clean, professional README for external sites.11 - -- **Developer Ergonomics in IDEs**: Writing code inside documentation comments can be a subpar experience. IDEs and tools like `rust-analyzer` often provide limited or no autocompletion, real-time error checking, or refactoring support for code within a comment block.18 A common and effective workflow to mitigate this is to first write and debug the example as a standard - - `#[test]` function in a temporary file or test module. This allows the developer to leverage the full power of the IDE. Once the code is working correctly, it can be copied into the doc comment, and the necessary formatting (`///`, `#`, etc.) can be applied.18 +Beyond architectural considerations, developers face several practical, +real-world challenges when working with doctests. + +- **The** `README.md` **Dilemma**: A project's `README.md` file serves multiple + audiences. It needs to render cleanly on platforms like GitHub and crates.io, + where hidden lines (`#...`) look like ugly, commented-out code. At the same + time, it should contain testable examples, which often require hidden lines + for setup.[^11] The best practice is to avoid maintaining the README + manually. Instead, use a tool like + + `cargo-readme`. This tool generates a `README.md` file from your crate-level + documentation (in `lib.rs`), automatically stripping out the hidden lines. + This provides a single source of truth that is both fully testable via + `cargo test --doc` and produces a clean, professional README for external + sites.[^11] + +- **Developer Ergonomics in IDEs**: Writing code inside documentation comments + can be a subpar experience. IDEs and tools like `rust-analyzer` often provide + limited or no autocompletion, real-time error checking, or refactoring + support for code within a comment block.[^18] A common and effective workflow + to mitigate this is to first write and debug the example as a standard + + `#[test]` function in a temporary file or test module. This allows the + developer to leverage the full power of the IDE. Once the code is working + correctly, it can be copied into the doc comment, and the necessary + formatting (`///`, `#`, etc.) can be applied.[^18] ## Conclusion and Recommendations -Rust's documentation testing framework is a uniquely powerful feature that promotes the creation of high-quality, reliable "living documentation." By deeply understanding its underlying compilation model and the patterns that have evolved to manage its constraints, developers can write doctests that are effective, ergonomic, and maintainable. To summarize the key principles for mastering doctests: +Rust's documentation testing framework is a uniquely powerful feature that +promotes the creation of high-quality, reliable "living documentation." By +deeply understanding its underlying compilation model and the patterns that +have evolved to manage its constraints, developers can write doctests that are +effective, ergonomic, and maintainable. To summarize the key principles for +mastering doctests: -1. **Embrace the Model**: Always remember that a doctest is an external integration test compiled in a separate crate. This mental model explains nearly all of its behavior. +1. **Embrace the Model**: Always remember that a doctest is an external + integration test compiled in a separate crate. This mental model explains + nearly all of its behavior. -2. **Prioritize Clarity**: Write examples that teach the *why*, not just the *how*. Use hidden lines (`#`) ruthlessly to eliminate boilerplate and focus the reader's attention on the relevant code. +2. **Prioritize Clarity**: Write examples that teach the *why*, not just the + *how*. Use hidden lines (`#`) ruthlessly to eliminate boilerplate and focus + the reader's attention on the relevant code. -3. **Handle Errors Gracefully**: For examples of fallible functions, always use the `fn main() -> Result<...>` pattern, hiding the boilerplate. Avoid `.unwrap()` to promote robust error-handling practices. +3. **Handle Errors Gracefully**: For examples of fallible functions, always use + the `fn main() -> Result<...>` pattern, hiding the boilerplate. Avoid + `.unwrap()` to promote robust error-handling practices. -4. **Be DRY**: When setup logic is shared across multiple examples, centralize it in a helper module guarded by `#[cfg(doctest)]` to avoid repetition. +4. **Be DRY**: When setup logic is shared across multiple examples, centralize + it in a helper module guarded by `#[cfg(doctest)]` to avoid repetition. -5. **Master** `cfg`: Use `#[cfg(doc)]` to control an item's *visibility* in the final documentation. Use `#[cfg(feature = "...")]` or other `cfg` flags *inside* the test block to control its conditional *execution*. Do not confuse the two. +5. **Master** `cfg`: Use `#[cfg(doc)]` to control an item's *visibility* in the + final documentation. Use `#[cfg(feature = "...")]` or other `cfg` flags + *inside* the test block to control its conditional *execution*. Do not + confuse the two. -6. **Know When to Stop**: A doctest is not the right tool for every job. When an example becomes overly complex, requires testing intricate error paths, or needs to access private implementation details, move it to a dedicated unit or integration test. Do not compromise your API design or test clarity by forcing a square peg into a round hole. Use the right tool for the job. +6. **Know When to Stop**: A doctest is not the right tool for every job. When + an example becomes overly complex, requires testing intricate error paths, + or needs to access private implementation details, move it to a dedicated + unit or integration test. Do not compromise your API design or test clarity + by forcing a square peg into a round hole. Use the right tool for the job. -#### **Works cited** +### Works cited - 1. rust - How can I write documentation tests for private modules ..., accessed on July 15, 2025, +[^1]: rust - How can I write documentation tests for private modules …, + accessed on July 15, 2025, + - 2. Rustdoc doctests need fixing - Swatinem, accessed on July 15, 2025, +[^2]: Rustdoc doctests need fixing - Swatinem, accessed on July 15, 2025, + - 3. Documentation tests - The rustdoc book - Rust Documentation, accessed on July 15, 2025, +[^3]: Documentation tests - The rustdoc book - Rust Documentation, accessed on + July 15, 2025, + - 4. Documentation tests - - GitHub Pages, accessed on July 15, 2025, +[^4]: Documentation tests - - GitHub Pages, accessed on July 15, 2025, + - 5. Documentation tests - - MIT, accessed on July 15, 2025, +[^5]: Documentation tests - - MIT, accessed on July 15, 2025, + - 6. How to organize your Rust tests - LogRocket Blog, accessed on July 15, 2025, +[^6]: How to organize your Rust tests - LogRocket Blog, accessed on July 15, + 2025, - 7. Best way to organise tests in Rust - Reddit, accessed on July 15, 2025, + - 8. Writing Rust Documentation - DEV Community, accessed on July 15, 2025, +[^8]: Writing Rust Documentation - DEV Community, accessed on July 15, 2025, + - 9. The rustdoc book, accessed on July 15, 2025, +[^9]: The rustdoc book, accessed on July 15, 2025, + -10. Documentation - Rust API Guidelines, accessed on July 15, 2025, +[^10]: Documentation - Rust API Guidelines, accessed on July 15, 2025, + -11. Best practice for doc testing README - help - The Rust Programming Language Forum, accessed on July 15, 2025, +[^11]: Best practice for doc testing README - help - The Rust Programming + Language Forum, accessed on July 15, 2025, + -12. Compile_fail doc test ignored in cfg(test) - help - The Rust Programming Language Forum, accessed on July 15, 2025, +[^12]: Compile_fail doc test ignored in cfg(test) - help - The Rust Programming + Language Forum, accessed on July 15, 2025, + -13. Test setup for doctests - help - The Rust Programming Language Forum, accessed on July 15, 2025, + accessed on July 15, 2025, + -14. quote_doctest - Rust - [Docs.rs](http://Docs.rs), accessed on July 15, 2025, +[^14]: quote_doctest - Rust - [Docs.rs](http://Docs.rs), accessed on July 15, + 2025, -15. Advanced features - The rustdoc book - Rust Documentation, accessed on July 15, 2025, +[^15]: Advanced features - The rustdoc book - Rust Documentation, accessed on + July 15, 2025, -16. rust - How can I conditionally execute a module-level doctest based ..., accessed on July 15, 2025, +[^16]: rust - How can I conditionally execute a module-level doctest based …, + accessed on July 15, 2025, + -17. How would one achieve conditional compilation with Rust projects that have doctests?, accessed on July 15, 2025, + have doctests?, accessed on July 15, 2025, + -How do you write your doc tests? : r/rust - Reddit, accessed on July 15, 2025, +[^18]: How do you write your doc tests? : r/rust - Reddit, accessed on July 15, + 2025, + From e4ada49317f07b28d41ece78075ae106ecd5d64c Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 01:32:39 +0100 Subject: [PATCH 2/3] Address review comments --- docs/rust-doctest-dry-guide.md | 62 +++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/rust-doctest-dry-guide.md b/docs/rust-doctest-dry-guide.md index 270ad63b..96bb03d1 100644 --- a/docs/rust-doctest-dry-guide.md +++ b/docs/rust-doctest-dry-guide.md @@ -4,12 +4,12 @@ To master the art of writing effective documentation tests in Rust, one must first understand the foundational principles upon which the `rustdoc` tool -operates. Its behavior, particularly its testing mechanism, is not an arbitrary -collection of features but a direct consequence of a deliberate design -philosophy. The core of this philosophy is that every doctest should validate -the public API of a crate from the perspective of an external user. This single -principle dictates the entire compilation model and explains both the power and -the inherent limitations of doctests. +operates. Its behaviour, particularly its testing mechanism, is not an +arbitrary collection of features but a direct consequence of a deliberate +design philosophy. The core of this philosophy is that every doctest should +validate the public API of a crate from the perspective of an external user. +This single principle dictates the entire compilation model and explains both +the power and the inherent limitations of doctests. ### 1.1 The "Separate Crate" Paradigm @@ -55,9 +55,9 @@ This "separate crate" paradigm has two immediate and significant consequences that shape all advanced doctesting patterns. First, **API visibility is strictly limited to public items**. Because the -doctest is compiled as an external crate, it can only access functions, -structs, traits, and modules marked with the `pub` keyword. It has no access to -private items or even crate-level public items (e.g., `pub(crate)`). This is +doctest is compiled as an external crate, it can only access functions. +Structs, traits and modules are marked with the `pub` keyword. It has no access +to private items or even crate-level public items (e.g., `pub(crate)`). This is not a bug or an oversight but a fundamental aspect of the design, enforcing the perspective of an external consumer.[^1] @@ -122,7 +122,7 @@ illustrates *why* and in *what context* an item should be used.[^10] It should tell a small story or solve a miniature problem that illuminates the item's purpose. For instance, an example for -`String::clone()` should not just show `hello.clone();`, but should demonstrate +`String::clone()` should not just show `hello.clone();` but should demonstrate a scenario where ownership rules necessitate creating a copy.[^10] To achieve this, examples must be clear and concise. Any code that is not @@ -139,7 +139,7 @@ type of `()`, while the `?` operator can only be used in a function that returns a `Result` or `Option`. This mismatch leads to a compilation error.[^3] Using `.unwrap()` or `.expect()` in examples is strongly discouraged. It is -considered an anti-pattern because users often copy example code verbatim, and +considered an antipattern because users often copy example code verbatim, and encouraging panicking on errors is contrary to robust application design.[^10] Instead, two canonical solutions exist. @@ -160,7 +160,7 @@ can then be hidden from the rendered documentation. /// let config = "key=value".parse::()?; /// assert_eq!(config.get("key"), Some("value")); /// # -/// # Ok(()) +/// # OK(()) /// # } /// ``` ``` @@ -202,7 +202,7 @@ human-readable example and what constitutes a complete, compilable program. Its primary use cases include: 1. **Hiding** `main` **Wrappers**: As demonstrated in the error-handling - examples, the entire `fn main() -> Result<...> {... }` and `Ok(())` + examples, the entire `fn main() -> Result<…> {... }` and `OK(())` scaffolding can be hidden, presenting the user with only the relevant code.[^10] @@ -239,13 +239,13 @@ Choosing the correct attribute is critical for communicating the intent of an example and ensuring the test suite provides meaningful feedback. The following table provides a comparative reference for the most common doctest attributes. -| Attribute | Action | Test Outcome | Primary Use Case & Caveats | -| ------------ | ------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ignore | Skips both compilation and execution. | ignored | Use Case: For pseudo-code, examples known to be broken, or to temporarily disable a test. Caveat: Provides no guarantee that the code is even syntactically correct. Generally discouraged in favor of more specific attributes.3 | -| should_panic | Compiles and runs the code. The test passes if the code panics. | ok on panic, failed if it does not panic. | Use Case: Demonstrating functions that are designed to panic on invalid input (e.g., indexing out of bounds). | -| compile_fail | Attempts to compile the code. The test passes if compilation fails. | ok on compilation failure, failed if it compiles successfully. | Use Case: Illustrating language rules, such as the borrow checker or type system constraints. Caveat: Highly brittle. A future Rust version might make the code valid, causing the test to unexpectedly fail.4 | -| no_run | Compiles the code but does not execute it. | ok if compilation succeeds. | Use Case: Essential for examples with undesirable side effects in a test environment, such as network requests, filesystem I/O, or launching a GUI. Guarantees the example is valid Rust code without running it.5 | -| edition2021 | Compiles the code using the specified Rust edition's rules. | ok on success. | Use Case: Demonstrating syntax or idioms that are specific to a particular Rust edition (e.g., edition2018, edition2021).4 | +| Attribute | Action | Test Outcome | Primary Use Case & Warnings | +| ------------ | ------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ignore | Skips both compilation and execution. | ignored | Use Case: For pseudocode, examples known to be broken, or to temporarily disable a test. Warning: Provides no guarantee that the code is even syntactically correct. Generally discouraged in favour of more specific attributes.3 | +| should_panic | Compiles and runs the code. The test passes if the code panics. | OK on panic, failed if it does not panic. | Use Case: Demonstrating functions that are designed to panic on invalid input (e.g., indexing out of bounds). | +| compile_fail | Attempts to compile the code. The test passes if compilation fails. | OK on compilation failure, failed if it compiles successfully. | Use Case: Illustrating language rules, such as the borrow checker or type system constraints. Warning: Highly brittle. A future Rust version might make the code valid, causing the test to unexpectedly fail.4 | +| no_run | Compiles the code but does not execute it. | OK if compilation succeeds. | Use Case: Essential for examples with undesirable side effects in a test environment, such as network requests, filesystem I/O, or launching a GUI. Guarantees the example is valid Rust code without running it.5 | +| edition2021 | Compiles the code using the specified Rust edition's rules. | OK on success. | Use Case: Demonstrating syntax or idioms that are specific to a particular Rust edition (e.g., edition2018, edition2021).4 | ### 3.2 Detailed Attribute Breakdown @@ -269,10 +269,10 @@ table provides a comparative reference for the most common doctest attributes. - `no_run`: This attribute strikes a crucial balance between test verification and practicality. For an example that demonstrates how to download a file - from the internet, you want to ensure the example code is syntactically - correct and uses the API properly, but you do not want your CI server to - actually perform a network request every time tests are run. `no_run` - provides this guarantee by compiling the code without executing it.[^5] + from the internet, developers want to ensure the example code is + syntactically correct and uses the API properly, but CI servers should not + actually perform a network request every time tests run. `no_run` provides + this guarantee by compiling the code without executing it.[^5] - `edition20xx`: This attribute allows an example to be tested against a specific Rust edition. This is important for crates that support multiple @@ -327,12 +327,12 @@ your library: /// let mut ctx = setup_test_environment()?; /// let result = my_func_that_needs_env(&mut ctx); /// assert!(result.is_ok()); -/// # Ok(()) +/// # OK(()) /// # } /// ``` pub fn my_func_that_needs_env(ctx: &mut TestContext) -> Result<(), ()> { //... function logic... - Ok(()) + OK(()) } // This module and its contents are only compiled for doctests. @@ -449,7 +449,7 @@ the doctest itself. When the `"serde"` feature is disabled, the code inside the block is compiled out. The doctest becomes an empty program that runs, does nothing, and is -reported as `ok`. While simple to write, this can be misleading, as the test +reported as `OK`. While simple to write, this can be misleading, as the test suite reports a "pass" for a test that was effectively skipped.[^16] Pattern [^2]: cfg_attr to Conditionally ignore the Test @@ -601,15 +601,15 @@ mastering doctests: *how*. Use hidden lines (`#`) ruthlessly to eliminate boilerplate and focus the reader's attention on the relevant code. -3. **Handle Errors Gracefully**: For examples of fallible functions, always use - the `fn main() -> Result<...>` pattern, hiding the boilerplate. Avoid +3. **Handle Errors Gracefully**: For fallible functions, always use + the `fn main() -> Result<…>` pattern. Hide the boilerplate and avoid `.unwrap()` to promote robust error-handling practices. 4. **Be DRY**: When setup logic is shared across multiple examples, centralize it in a helper module guarded by `#[cfg(doctest)]` to avoid repetition. -5. **Master** `cfg`: Use `#[cfg(doc)]` to control an item's *visibility* in the - final documentation. Use `#[cfg(feature = "...")]` or other `cfg` flags +5. **Master** `cfg`: Use `#[cfg(doc)]` to control an item's *visibility* in + the final documentation. Use `#[cfg(feature = "...")]` or other `cfg` flags *inside* the test block to control its conditional *execution*. Do not confuse the two. From b20ffb71df73a45eb9fe7ec53baa924cf129ac1c Mon Sep 17 00:00:00 2001 From: Leynos Date: Sat, 19 Jul 2025 01:50:06 +0100 Subject: [PATCH 3/3] Fix doctest guide punctuation --- docs/rust-doctest-dry-guide.md | 37 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/rust-doctest-dry-guide.md b/docs/rust-doctest-dry-guide.md index 96bb03d1..5f87bc55 100644 --- a/docs/rust-doctest-dry-guide.md +++ b/docs/rust-doctest-dry-guide.md @@ -104,7 +104,7 @@ Doctests reside within documentation comments. Rust recognizes two types: of (e.g., a module or the crate itself). They are typically used at the top of `lib.rs` or `mod.rs` to provide crate- or module-level documentation.[^9] -Within these comments, a code block is denoted by triple backticks While +Within these comments, a code block is denoted by triple backticks. while `rustdoc` defaults to assuming the language is Rust, explicitly adding the rust language specifier (e.g., `rust`) is considered good practice for clarity.[^3] @@ -202,9 +202,8 @@ human-readable example and what constitutes a complete, compilable program. Its primary use cases include: 1. **Hiding** `main` **Wrappers**: As demonstrated in the error-handling - examples, the entire `fn main() -> Result<…> {... }` and `OK(())` - scaffolding can be hidden, presenting the user with only the relevant - code.[^10] + examples, the entire `fn main() -> Result<…> {… }` and `OK(())` scaffolding + can be hidden, presenting the user with only the relevant code.[^10] 2. **Hiding Setup Code**: If an example requires some preliminary setup—like creating a temporary file, defining a helper struct for the test, or @@ -331,7 +330,7 @@ your library: /// # } /// ``` pub fn my_func_that_needs_env(ctx: &mut TestContext) -> Result<(), ()> { - //... function logic... + // … function logic… OK(()) } @@ -343,19 +342,19 @@ mod doctest_helpers { use std::io::Result; pub struct TestContext { - //... fields for the test context... + // … fields for the test context… } pub fn setup_test_environment() -> Result { // All the complex, shared setup logic lives here once. - println!("Setting up test environment..."); - Ok(TestContext { /*... */ }) + println!("Setting up test environment…"); + OK(TestContext { /*… */ }) } } // A struct that might be needed by the public function signature. // It can be defined normally. -pub struct TestContext { /*... */ } +pub struct TestContext { /*… */ } ``` This pattern is the most effective way to achieve DRY doctests. It centralizes @@ -475,12 +474,12 @@ This approach provides clearer feedback but is significantly more verbose and less ergonomic, especially when applied to outer (`///`) doc comments, as the `cfg_attr` must be applied to every single line of the comment.[^16] -### 5.3 Displaying Feature Requirements in Docs: `#[doc(cfg(...))]` +### 5.3 Displaying Feature Requirements in Docs: `#[doc(cfg(…))]` To complement conditional execution, Rust provides a way to visually flag feature-gated items in the generated documentation. This is achieved with the -`#[doc(cfg(...))]` attribute, which requires enabling the -`#![feature(doc_cfg)]` feature gate at the crate root. +`#[doc(cfg(…))]` attribute, which requires enabling the `#![feature(doc_cfg)]` +feature gate at the crate root. ```rust // At the crate root (lib.rs) @@ -489,7 +488,7 @@ feature-gated items in the generated documentation. This is achieved with the // On the feature-gated item #[cfg(feature = "serde")] #[doc(cfg(feature = "serde"))] -pub fn function_requiring_serde() { /*... */ } +pub fn function_requiring_serde() { /*… */ } ``` This will render a banner in the documentation for `function_requiring_serde` @@ -516,10 +515,10 @@ its own purpose: - **Unit Tests (**`#[test]` **in** `src/`**)**: These are for testing the nitty-gritty details of your implementation. They are placed in submodules - within your source files (often `mod tests {... }`) and are compiled only - with `#[cfg(test)]`. Because they live inside the crate, they can access - private functions and modules, making them perfect for testing internal - logic, edge cases, and specific error conditions.[^1] + within your source files (often `mod tests {…}`) and are compiled only with + `#[cfg(test)]`. Because they live inside the crate, they can access private + functions and modules, making them perfect for testing internal logic, edge + cases, and specific error conditions.[^1] - **Integration Tests (in the** `tests/` **directory)**: These test the crate from a completely external perspective, much like doctests. However, they are @@ -562,7 +561,7 @@ real-world challenges when working with doctests. - **The** `README.md` **Dilemma**: A project's `README.md` file serves multiple audiences. It needs to render cleanly on platforms like GitHub and crates.io, - where hidden lines (`#...`) look like ugly, commented-out code. At the same + where hidden lines (`#…`) look like ugly, commented-out code. At the same time, it should contain testable examples, which often require hidden lines for setup.[^11] The best practice is to avoid maintaining the README manually. Instead, use a tool like @@ -609,7 +608,7 @@ mastering doctests: it in a helper module guarded by `#[cfg(doctest)]` to avoid repetition. 5. **Master** `cfg`: Use `#[cfg(doc)]` to control an item's *visibility* in - the final documentation. Use `#[cfg(feature = "...")]` or other `cfg` flags + the final documentation. Use `#[cfg(feature = "…")]` or other `cfg` flags *inside* the test block to control its conditional *execution*. Do not confuse the two.