Commit 45b64adfdd04ef7a01198c0455ea007d49f528c2

Authored by m-holger
1 parent 9352f6f8

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