Commit 9e358a32a8d00c3a473b2476db292e22d89e6802
Committed by
GitHub
Merge pull request #1671 from m-holger/rm_develop
Add CI test guidelines to README-developer.md
Showing
1 changed file
with
233 additions
and
0 deletions
README-developer.md
| @@ -9,6 +9,7 @@ qpdf as a library. | @@ -9,6 +9,7 @@ qpdf as a library. | ||
| 9 | * [CHECKING DOCS ON readthedocs](#checking-docs-on-readthedocs) | 9 | * [CHECKING DOCS ON readthedocs](#checking-docs-on-readthedocs) |
| 10 | * [CODING RULES](#coding-rules) | 10 | * [CODING RULES](#coding-rules) |
| 11 | * [ZLIB COMPATIBILITY](#zlib-compatibility) | 11 | * [ZLIB COMPATIBILITY](#zlib-compatibility) |
| 12 | +* [CI Testing](#ci-testing) | ||
| 12 | * [HOW TO ADD A COMMAND-LINE ARGUMENT](#how-to-add-a-command-line-argument) | 13 | * [HOW TO ADD A COMMAND-LINE ARGUMENT](#how-to-add-a-command-line-argument) |
| 13 | * [RUNNING pikepdf's TEST SUITE](#running-pikepdfs-test-suite) | 14 | * [RUNNING pikepdf's TEST SUITE](#running-pikepdfs-test-suite) |
| 14 | * [OTHER NOTES](#other-notes) | 15 | * [OTHER NOTES](#other-notes) |
| @@ -264,6 +265,238 @@ Building docs from pull requests is also enabled. | @@ -264,6 +265,238 @@ Building docs from pull requests is also enabled. | ||
| 264 | * NEVER replace a std::string const& return value with std::string_view in the public API. | 265 | * NEVER replace a std::string const& return value with std::string_view in the public API. |
| 265 | 266 | ||
| 266 | 267 | ||
| 268 | +## CI Testing | ||
| 269 | + | ||
| 270 | +All additions and behavior changes in qpdf should include corresponding tests. If you add or update | ||
| 271 | +functionality, include tests in the same change request. | ||
| 272 | + | ||
| 273 | +### Coverage | ||
| 274 | + | ||
| 275 | +Historically, test coverage was tracked with `QTC::TC` calls as described in the | ||
| 276 | +[manual](https://qpdf.readthedocs.io/en/stable/contributing.html#coverage). | ||
| 277 | + | ||
| 278 | +Coverage reporting is now provided primarily by Codecov, and Codecov reports are generated as | ||
| 279 | +part of CI. If a `QTC::TC` call only duplicates information that Codecov already provides, do not | ||
| 280 | +add it to new code, and remove it when you are updating nearby code. | ||
| 281 | + | ||
| 282 | +Testing should, as far as practical, provide complete coverage. Exceptions are rare and generally | ||
| 283 | +limited to cases that are impractical to exercise in CI, such as highly platform-specific behavior, | ||
| 284 | +defensive paths that are not realistically reachable, or runtime errors that are difficult to | ||
| 285 | +generate during testing. | ||
| 286 | + | ||
| 287 | +Intentional gaps in coverage should be clearly flagged and are preferably avoided to reduce noise in | ||
| 288 | +coverage reports. For rare justified gaps, use helper functions such as | ||
| 289 | +`util::no_ci_rt_error_if` or `util::internal_error_if` (defined in `Util.hh`) to make intent | ||
| 290 | +explicit without adding noise to the coverage report. | ||
| 291 | + | ||
| 292 | +Codecov has limits: it can show that code was exercised, but not necessarily that all paths through | ||
| 293 | +a routine were tested. Keep using `QTC::TC` for path coverage in these cases: | ||
| 294 | + | ||
| 295 | +* The `QTC::TC` call is the only executable statement in a branch. | ||
| 296 | +* The optional third parameter is used. | ||
| 297 | + | ||
| 298 | +### HOW TO ADD A CI TEST | ||
| 299 | + | ||
| 300 | +This section expands on the information provided in the | ||
| 301 | +[manual](https://qpdf.readthedocs.io/en/stable/contributing.html#automated-tests), which should be | ||
| 302 | +read first. | ||
| 303 | + | ||
| 304 | +Tests in qpdf are managed through the `qtest` framework, a Perl-based testing system that runs via | ||
| 305 | +`ctest`. To add a new CI test: | ||
| 306 | + | ||
| 307 | +### Test Output Styles | ||
| 308 | + | ||
| 309 | +Historically, tests produced output messages to the console that were compared to expected console | ||
| 310 | +output files. The preferred current style is to use assertions in the test code rather than relying | ||
| 311 | +on console output comparison. This makes tests clearer and more maintainable. See "Use of assert" in | ||
| 312 | +the CODING RULES section for details on how to include assertion headers in test code. | ||
| 313 | + | ||
| 314 | +### Identifying Test Location | ||
| 315 | + | ||
| 316 | +* **CLI and public API tests**: Add to `qpdf/qtest/` for command-line interface and public API testing. | ||
| 317 | + If a related test file already exists (e.g., `linearization.test` for linearization tests), add your | ||
| 318 | + tests to that file rather than creating a new one. | ||
| 319 | +* **Library unit tests (private API)**: Add to `libtests/` for testing private API functions and | ||
| 320 | + internal library functionality. If a related test file already exists, add your tests to it. | ||
| 321 | +* **Example tests**: Add to `examples/qtest/` for example program validation | ||
| 322 | +* **Fuzzer tests**: Add to `fuzz/` for fuzz testing | ||
| 323 | + | ||
| 324 | +When adding tests to an existing `.test` file, you must update the `$n_tests` variable at the top | ||
| 325 | +of the file to reflect the new total number of tests. This variable is used by the qtest framework | ||
| 326 | +to validate that all expected tests have been run. | ||
| 327 | + | ||
| 328 | +### Adding a Test Case | ||
| 329 | + | ||
| 330 | +1. **Create or modify a .test file**: Test files are in the appropriate `qtest/` subdirectory and use | ||
| 331 | + the `.test` extension. They use the qtest Perl framework syntax. Use qtest framework methods to | ||
| 332 | + define what command to run and what output to expect. | ||
| 333 | + | ||
| 334 | +2. **Comparing console output**: Use the appropriate qtest comparison method based on output length. | ||
| 335 | + In new test cases, the preferred style is to use assertions and therefore typically the only | ||
| 336 | + console output is the message "test N done" and any warning or error messages. | ||
| 337 | + Console output is automatically captured by the test framework; you do not need to redirect it. | ||
| 338 | + By convention, expected console output files use the `.out` extension. | ||
| 339 | + * For single-line console output, use `$td->STRING`: | ||
| 340 | + ```perl | ||
| 341 | + $td->runtest("test description", | ||
| 342 | + {$td->COMMAND => "qpdf some-args"}, | ||
| 343 | + {$td->STRING => "expected output text\n", $td->EXIT_STATUS => 0}, | ||
| 344 | + $td->NORMALIZE_NEWLINES); | ||
| 345 | + ``` | ||
| 346 | + * For longer console output, use `$td->FILE` to compare against an expected output file: | ||
| 347 | + ```perl | ||
| 348 | + $td->runtest("test description", | ||
| 349 | + {$td->COMMAND => "qpdf command"}, | ||
| 350 | + {$td->FILE => "expected-output.out", $td->EXIT_STATUS => 0}, | ||
| 351 | + $td->NORMALIZE_NEWLINES); | ||
| 352 | + ``` | ||
| 353 | + Always include `$td->NORMALIZE_NEWLINES` as the final parameter when comparing console output to | ||
| 354 | + handle platform differences in line endings. | ||
| 355 | + | ||
| 356 | +3. **Comparing output files**: When you need to verify generated files (such as PDFs), use a two-test | ||
| 357 | + pattern. First, run the command that generates the output file `a.pdf`: | ||
| 358 | + ```perl | ||
| 359 | + $td->runtest("test description", | ||
| 360 | + {$td->COMMAND => "test_driver 24 minimal.pdf"}, | ||
| 361 | + {$td->STRING => "test 24 done\n", $td->EXIT_STATUS => 0}, | ||
| 362 | + $td->NORMALIZE_NEWLINES); | ||
| 363 | + ``` | ||
| 364 | + Then, in a separate test, compare the generated file against the expected file. By convention, | ||
| 365 | + "check output" is always used as the test description when checking output files: | ||
| 366 | + ```perl | ||
| 367 | + $td->runtest("check output", | ||
| 368 | + {$td->FILE => "a.pdf"}, | ||
| 369 | + {$td->FILE => "expected-output.pdf"}); | ||
| 370 | + ``` | ||
| 371 | + Always use temporary output filenames like `a.pdf` or `b.pdf` for generated files, as these are | ||
| 372 | + automatically cleaned up between tests. | ||
| 373 | + | ||
| 374 | +### Adding Test Functions to Existing Test Programs | ||
| 375 | + | ||
| 376 | +When adding new functionality that requires testing, check if there are existing related tests in | ||
| 377 | +one of the test programs (examples: `libtests/objects.cc` and `qpdf/test_driver.cc`). If so, add | ||
| 378 | +your new test function to the existing test program rather than creating a new one. | ||
| 379 | + | ||
| 380 | +To add a new test case to an existing test program foo.cc: | ||
| 381 | + | ||
| 382 | +1. **Write your test function**: In foo.cc, define a function with signature: | ||
| 383 | + ```cpp | ||
| 384 | + static void | ||
| 385 | + test_N(QPDF& pdf, char const* arg2) | ||
| 386 | + { | ||
| 387 | + // Test implementation | ||
| 388 | + } | ||
| 389 | + ``` | ||
| 390 | + Where `N` is the test number. Tests are numbered consecutively, so `N` should be one greater than | ||
| 391 | + the highest existing test number in the program. The test function receives: | ||
| 392 | + * `pdf`: A QPDF object pre-loaded with the specified input file (unless the test is in the | ||
| 393 | + `ignore_filename` set) | ||
| 394 | + * `arg2`: An optional second argument passed via command line, useful for parameterizing tests | ||
| 395 | + | ||
| 396 | +2. **Register your test function**: Add your test function to the `test_functions` map in the | ||
| 397 | + `runtest()` function in foo.cc: | ||
| 398 | + ```cpp | ||
| 399 | + std::map<int, void (*)(QPDF&, char const*)> test_functions = { | ||
| 400 | + // ... existing tests ... | ||
| 401 | + {N, test_N}}; | ||
| 402 | + ``` | ||
| 403 | + | ||
| 404 | +3. **Update ignore_filename if needed**: If your test does not require an input file, add your test | ||
| 405 | + number to the `ignore_filename` set in the `runtest()` function in foo.cc: | ||
| 406 | + ```cpp | ||
| 407 | + std::set<int> ignore_filename = {1, 2, N}; | ||
| 408 | + ``` | ||
| 409 | + This prevents the test framework from attempting to load a file for your test. | ||
| 410 | + | ||
| 411 | +4. **Create a corresponding .test file entry**: In `qpdf/qtest/` or `libtests/qtest/`, add a test | ||
| 412 | + case that calls your test program with the appropriate number and arguments: | ||
| 413 | + ```perl | ||
| 414 | + $td->runtest("description of test N", | ||
| 415 | + {$td->COMMAND => "qpdf-ctest N test-file.pdf"}, | ||
| 416 | + {$td->FILE => "expected-output.out", $td->EXIT_STATUS => 0}, | ||
| 417 | + $td->NORMALIZE_NEWLINES); | ||
| 418 | + ``` | ||
| 419 | + | ||
| 420 | +5. **Create expected output files if needed**: If required, create `expected-output.out` containing | ||
| 421 | + the exact expected output from your test function. Expected output files should be located in | ||
| 422 | + subdirectories as follows: | ||
| 423 | + * For `qpdf/qtest/`: in the `qpdf/qtest/qpdf/` subdirectory | ||
| 424 | + * For other test locations: in a subdirectory with the same name as the test program (e.g., for | ||
| 425 | + `libtests/objects.cc`, expected output goes in `libtests/qtest/objects/`) | ||
| 426 | + | ||
| 427 | +6. **Update test count**: Update the `$n_tests` variable at the top of the .test file to include | ||
| 428 | + your new test(s). | ||
| 429 | + | ||
| 430 | +### Creating a New Test Program | ||
| 431 | + | ||
| 432 | +If a new test program is required (when no existing test program has related functionality): | ||
| 433 | + | ||
| 434 | +1. **Include the assertion header**: The first include file must be `#include <qpdf/assert_test.h>`. | ||
| 435 | + See "Use of assert" in the CODING RULES section for details on assertion usage in test code. | ||
| 436 | + | ||
| 437 | +2. **Implement the test functions** following the patterns described above. | ||
| 438 | + | ||
| 439 | +3. **Register and run** your test functions via the `test_functions` map and main dispatcher, similar | ||
| 440 | + to existing test programs. | ||
| 441 | + | ||
| 442 | +**Example**: To add test 200 to `test_driver.cc`: | ||
| 443 | +1. Write `static void test_200(QPDF& pdf, char const* arg2)` with your test implementation | ||
| 444 | +2. Add `{200, test_200}` to the test_functions map | ||
| 445 | +3. If test 200 requires an input file: | ||
| 446 | + ```perl | ||
| 447 | + $td->runtest("test 200 description", | ||
| 448 | + {$td->COMMAND => "test_driver 200 test_200.pdf"}, | ||
| 449 | + {$td->FILE => "test-200.out", $td->EXIT_STATUS => 0}, | ||
| 450 | + $td->NORMALIZE_NEWLINES); | ||
| 451 | + ``` | ||
| 452 | + If test 200 does not require an input file, add 200 to `ignore_filename` and use: | ||
| 453 | + ```perl | ||
| 454 | + $td->runtest("test 200 description", | ||
| 455 | + {$td->COMMAND => "test_driver 200 -"}, | ||
| 456 | + {$td->FILE => "test-200.out", $td->EXIT_STATUS => 0}, | ||
| 457 | + $td->NORMALIZE_NEWLINES); | ||
| 458 | + ``` | ||
| 459 | +4. Create `qpdf/qtest/qpdf/test-200.out` with expected output (or appropriate location for other | ||
| 460 | + test programs) | ||
| 461 | +5. Increment `$n_tests` in `qpdf/qtest/qpdf.test` (or `my-example.test` for a new test program) | ||
| 462 | + | ||
| 463 | +### Running Your Test Locally | ||
| 464 | + | ||
| 465 | +```bash | ||
| 466 | +# Run all tests | ||
| 467 | +cd build && ctest --output-on-failure | ||
| 468 | + | ||
| 469 | +# Run specific test group | ||
| 470 | +ctest -R qpdf # CLI tests | ||
| 471 | +ctest -R libtests # Library tests | ||
| 472 | +ctest -R examples # Example tests | ||
| 473 | + | ||
| 474 | +# To run a specific test file, prefix with "TESTS=test_name", e.g. to run objects.test: | ||
| 475 | +TESTS=objects ctest -R libtests | ||
| 476 | + | ||
| 477 | +# Run a specific test function directly (for debugging) | ||
| 478 | +./test_driver 200 minimal.pdf | ||
| 479 | +./objects 5 minimal.pdf optional-arg | ||
| 480 | +``` | ||
| 481 | + | ||
| 482 | +### CI Integration | ||
| 483 | + | ||
| 484 | +Tests are automatically run as part of the CI pipeline defined in `.github/workflows/main.yml`. The | ||
| 485 | +pipeline includes: | ||
| 486 | + | ||
| 487 | +* Linux builds with full test suite | ||
| 488 | +* Windows builds (MSVC and MinGW) | ||
| 489 | +* macOS builds | ||
| 490 | +* Sanitizer builds (AddressSanitizer, UndefinedBehaviorSanitizer) | ||
| 491 | +* Coverage reporting | ||
| 492 | + | ||
| 493 | +All tests must pass on all platforms before a PR can be merged. Pay attention to: | ||
| 494 | + | ||
| 495 | +* **Platform-specific issues**: Some tests may behave differently on Windows vs. Linux/macOS | ||
| 496 | +* **Output determinism**: Ensure tests produce consistent output; avoid timestamps or random data | ||
| 497 | + unless intentional | ||
| 498 | + | ||
| 499 | + | ||
| 267 | ## ZLIB COMPATIBILITY | 500 | ## ZLIB COMPATIBILITY |
| 268 | 501 | ||
| 269 | The qpdf test suite is designed to be independent of the output of any | 502 | The qpdf test suite is designed to be independent of the output of any |