|
| 1 | +# Documentation Code Testing |
| 2 | + |
| 3 | +<!--skip: Examples show marker syntax, not runnable code--> |
| 4 | + |
| 5 | +PECOS automatically tests code examples in documentation to ensure they remain correct as the codebase evolves. |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The documentation testing system: |
| 10 | + |
| 11 | +1. Extracts Python and Rust code blocks from Markdown files |
| 12 | +2. Generates pytest test files from the extracted code |
| 13 | +3. Runs the tests as part of CI |
| 14 | + |
| 15 | +## Running Documentation Tests |
| 16 | + |
| 17 | +```bash |
| 18 | +# Generate tests from documentation |
| 19 | +uv run python scripts/docs/generate_doc_tests.py |
| 20 | + |
| 21 | +# Run all documentation tests |
| 22 | +uv run pytest python/quantum-pecos/tests/docs/generated -v |
| 23 | + |
| 24 | +# Run tests for a specific document |
| 25 | +uv run pytest python/quantum-pecos/tests/docs/generated/user-guide/test_getting_started.py -v |
| 26 | +``` |
| 27 | + |
| 28 | +## Marker Reference |
| 29 | + |
| 30 | +Markers are HTML comments placed immediately before code blocks to control test behavior. |
| 31 | + |
| 32 | +### Skip Markers |
| 33 | + |
| 34 | +#### Block-level Skip |
| 35 | + |
| 36 | +Skip a single code block: |
| 37 | + |
| 38 | +```markdown |
| 39 | +<!--skip--> |
| 40 | +\```python |
| 41 | +# This code won't be tested |
| 42 | +from some_unavailable_module import thing |
| 43 | +\``` |
| 44 | +``` |
| 45 | + |
| 46 | +Skip with a reason (shown in test output): |
| 47 | + |
| 48 | +```markdown |
| 49 | +<!--skip: requires external hardware--> |
| 50 | +\```python |
| 51 | +connect_to_quantum_computer() |
| 52 | +\``` |
| 53 | +``` |
| 54 | + |
| 55 | +#### Document-level Skip |
| 56 | + |
| 57 | +Skip all code blocks in a document by placing a skip marker at the beginning of the file (after the heading): |
| 58 | + |
| 59 | +```markdown |
| 60 | +# Document Title |
| 61 | + |
| 62 | +<!--skip: All examples require external files--> |
| 63 | + |
| 64 | +Content here... |
| 65 | +``` |
| 66 | + |
| 67 | +### Conditional Skip |
| 68 | + |
| 69 | +Skip tests when CUDA is not available: |
| 70 | + |
| 71 | +```markdown |
| 72 | +<!--skip-if-no-cuda--> |
| 73 | +\```python |
| 74 | +from pecos.simulators import CudaSim |
| 75 | +sim = CudaSim() |
| 76 | +\``` |
| 77 | +``` |
| 78 | + |
| 79 | +### Expected Errors |
| 80 | + |
| 81 | +Test that code raises a specific error: |
| 82 | + |
| 83 | +```markdown |
| 84 | +<!--expect-error: TypeError--> |
| 85 | +\```python |
| 86 | +"string" + 123 # This will raise TypeError |
| 87 | +\``` |
| 88 | +``` |
| 89 | + |
| 90 | +The test passes if the code raises an error containing the specified text. |
| 91 | + |
| 92 | +### Expected Output |
| 93 | + |
| 94 | +Verify code produces specific output: |
| 95 | + |
| 96 | +```markdown |
| 97 | +<!--expect-output: Hello, World!--> |
| 98 | +\```python |
| 99 | +print("Hello, World!") |
| 100 | +\``` |
| 101 | +``` |
| 102 | + |
| 103 | +### Test Names |
| 104 | + |
| 105 | +Give a test a specific name for easier identification: |
| 106 | + |
| 107 | +```markdown |
| 108 | +<!--test-name: bell_state_example--> |
| 109 | +\```python |
| 110 | +# This test will be named test_bell_state_example |
| 111 | +\``` |
| 112 | +``` |
| 113 | + |
| 114 | +### Pytest Marks |
| 115 | + |
| 116 | +Add pytest marks to tests: |
| 117 | + |
| 118 | +```markdown |
| 119 | +<!--mark.slow--> |
| 120 | +\```python |
| 121 | +# This test will have @pytest.mark.slow |
| 122 | +\``` |
| 123 | +``` |
| 124 | + |
| 125 | +### Test Data Files |
| 126 | + |
| 127 | +Copy test data files to the test directory (for Rust cargo tests): |
| 128 | + |
| 129 | +```markdown |
| 130 | +<!--test-data: repetition_code.hugr--> |
| 131 | +\```rust |
| 132 | +fn main() { |
| 133 | + let data = std::fs::read("repetition_code.hugr").unwrap(); |
| 134 | +} |
| 135 | +\``` |
| 136 | +``` |
| 137 | + |
| 138 | +Test data files should be placed in `docs/assets/test-data/`. |
| 139 | + |
| 140 | +## Hidden Preambles |
| 141 | + |
| 142 | +Use hidden code blocks to provide imports or setup code that should be included in tests but not shown in documentation: |
| 143 | + |
| 144 | +```markdown |
| 145 | +\```hidden-python |
| 146 | +from pecos.slr import Main, QReg, CReg |
| 147 | +from pecos.slr.qeclib import qubit as qb |
| 148 | +\``` |
| 149 | + |
| 150 | +Now the visible code can use these imports: |
| 151 | + |
| 152 | +\```python |
| 153 | +prog = Main( |
| 154 | + q := QReg("q", 2), |
| 155 | + qb.H(q[0]), |
| 156 | +) |
| 157 | +\``` |
| 158 | +``` |
| 159 | + |
| 160 | +The hidden preamble is prepended to all subsequent code blocks in the document until a `<!--preamble-reset-->` marker is encountered. |
| 161 | + |
| 162 | +### Preamble Reset |
| 163 | + |
| 164 | +Reset the accumulated preamble: |
| 165 | + |
| 166 | +```markdown |
| 167 | +<!--preamble-reset--> |
| 168 | +\```python |
| 169 | +# This code starts fresh without any preamble |
| 170 | +\``` |
| 171 | +``` |
| 172 | + |
| 173 | +## Code Block Languages |
| 174 | + |
| 175 | +### Python |
| 176 | + |
| 177 | +Python code blocks are tested using `exec()`: |
| 178 | + |
| 179 | +```markdown |
| 180 | +\```python |
| 181 | +from pecos import sim |
| 182 | +result = sim(Qasm("...")).run(10) |
| 183 | +\``` |
| 184 | +``` |
| 185 | + |
| 186 | +### Rust |
| 187 | + |
| 188 | +Rust code blocks can be tested in two ways: |
| 189 | + |
| 190 | +1. **Simple Rust** (rustc): Code with `fn main()` that doesn't use external crates |
| 191 | +2. **Cargo Rust**: Code using `pecos` crates (detected by `use pecos*::`) |
| 192 | + |
| 193 | +Incomplete Rust snippets (no `fn main()`, traits, impls without full context) are automatically skipped. |
| 194 | + |
| 195 | +#### Rust with Cargo Dependencies |
| 196 | + |
| 197 | +For Rust code that uses PECOS crates: |
| 198 | + |
| 199 | +```markdown |
| 200 | +\```rust |
| 201 | +use pecos::prelude::*; |
| 202 | + |
| 203 | +fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 204 | + let results = sim(Qasm::from_string("...")).run(10)?; |
| 205 | + Ok(()) |
| 206 | +} |
| 207 | +\``` |
| 208 | +``` |
| 209 | + |
| 210 | +This creates a temporary Cargo project with the necessary dependencies. |
| 211 | + |
| 212 | +## Fence-level Skip Markers |
| 213 | + |
| 214 | +Standard Rust documentation markers are also supported: |
| 215 | + |
| 216 | +```markdown |
| 217 | +\```rust,skip |
| 218 | +// This code is skipped |
| 219 | +\``` |
| 220 | + |
| 221 | +\```rust,ignore |
| 222 | +// This code is also skipped |
| 223 | +\``` |
| 224 | + |
| 225 | +\```rust,no_run |
| 226 | +// This code is also skipped |
| 227 | +\``` |
| 228 | +``` |
| 229 | + |
| 230 | +## Best Practices |
| 231 | + |
| 232 | +### Do |
| 233 | + |
| 234 | +- Add hidden preambles for common imports to keep visible code focused |
| 235 | +- Use meaningful skip reasons to explain why code can't be tested |
| 236 | +- Test error conditions with `<!--expect-error-->` markers |
| 237 | +- Keep code examples self-contained when possible |
| 238 | + |
| 239 | +### Don't |
| 240 | + |
| 241 | +- Don't skip code unless necessary |
| 242 | +- Don't rely on state from previous code blocks (each block is tested independently) |
| 243 | +- Don't include real API keys or credentials in examples |
| 244 | +- Don't use `exec()` in your actual code examples (it's only used internally for testing) |
| 245 | + |
| 246 | +## Generated Files |
| 247 | + |
| 248 | +The test generator creates files in: |
| 249 | + |
| 250 | +``` |
| 251 | +python/quantum-pecos/tests/docs/generated/ |
| 252 | + conftest.py # Shared fixtures |
| 253 | + test_README.py # Tests from docs/README.md |
| 254 | + user-guide/ # Tests from docs/user-guide/ |
| 255 | + development/ # Tests from docs/development/ |
| 256 | +``` |
| 257 | + |
| 258 | +These files are gitignored and regenerated before tests run. |
| 259 | + |
| 260 | +## Debugging Failed Tests |
| 261 | + |
| 262 | +When a documentation test fails: |
| 263 | + |
| 264 | +1. Check the test docstring for the source file and line number |
| 265 | +2. The test name includes the source file and block number |
| 266 | +3. Run the specific test with `-v` for verbose output |
| 267 | +4. Check if the code requires imports that should be in a hidden preamble |
| 268 | + |
| 269 | +Example: |
| 270 | + |
| 271 | +```bash |
| 272 | +# Run with verbose output |
| 273 | +uv run pytest python/quantum-pecos/tests/docs/generated/user-guide/test_gates.py::test_gates_block_5 -v |
| 274 | + |
| 275 | +# The test docstring shows the source: |
| 276 | +# """Test from docs/user-guide/gates.md:142.""" |
| 277 | +``` |
| 278 | + |
| 279 | +## Adding New Documentation |
| 280 | + |
| 281 | +When adding new documentation: |
| 282 | + |
| 283 | +1. Add code examples in fenced code blocks |
| 284 | +2. Add hidden preambles for common imports if needed |
| 285 | +3. Add skip markers for code that can't be tested |
| 286 | +4. Run `uv run python scripts/docs/generate_doc_tests.py` to generate tests |
| 287 | +5. Run the generated tests to verify examples work |
| 288 | +6. Commit the documentation (generated tests are gitignored) |
0 commit comments