Commit 9e358a32a8d00c3a473b2476db292e22d89e6802

Authored by m-holger
Committed by GitHub
2 parents 8c886812 45b64adf

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 9 * [CHECKING DOCS ON readthedocs](#checking-docs-on-readthedocs)
10 10 * [CODING RULES](#coding-rules)
11 11 * [ZLIB COMPATIBILITY](#zlib-compatibility)
  12 +* [CI Testing](#ci-testing)
12 13 * [HOW TO ADD A COMMAND-LINE ARGUMENT](#how-to-add-a-command-line-argument)
13 14 * [RUNNING pikepdf's TEST SUITE](#running-pikepdfs-test-suite)
14 15 * [OTHER NOTES](#other-notes)
... ... @@ -264,6 +265,238 @@ Building docs from pull requests is also enabled.
264 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 500 ## ZLIB COMPATIBILITY
268 501  
269 502 The qpdf test suite is designed to be independent of the output of any
... ...