Repository navigation
test(pytest): the coverage report runs the pytest corpus (#2038, part 3) - #2043
Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
Re-reviewed at 9fdba9e by running run_coverage.py as root on PG18 under the pg18 lock: once from the repo root, as the nightly calls it, and once from test/pytest. I also ran a normal pytest run before and after it, test_coverage_report.py with ten mutants, and the registry checks.
This is my first full review of this PR. My earlier comment was on the plan (#2038, comment 6710544129).
What changed since origin/main
The PR is three commits on origin/main (1bc280a, 0 behind), with no merges, so there are no hand resolutions to check:
- 8ebc854 adds
test/pytest/run_coverage.py,test_coverage_report.py(8 tests), TESTS.md §357, and the nightlycoveragejob change. The job now runs as an ordinary user and callsrun_coverage.py. - cd73641 makes
run_coverage.pyexecutable. - 9fdba9e is a wording change in a test.
test/run_coverage.sh still exists.
What I measured
- The build is instrumented. I ran a normal pytest run first: 37
.ofiles, 0.gcnofiles, and 0__gcovsymbols in the installedpgcolumnar.so(f1b22cba5f20). Afterrun_coverage.pythere were 37.ofiles, 37.gcnofiles, none missing, and 6__gcovsymbols in the installed library. Its ownmake clean+make COPT=--coverage installdoes what plan item 1 asked. - Counters are written as postgres. Run from
test/pytestwithtest_smoke.pyandtest_native_roundtrip.py, I got 18 passed. 37.gcdafiles appeared under/tmp/pgc-gcov(mode 1777), all owned bypostgres. All 37 were copied back beside their objects. lcov captured 4990 of 28058 lines, and 31 files insrc/have more than 0 lines hit. - The refusals match
run_coverage.sh. With genhtml missing, the CLI printsFAIL genhtml not found, rc=1. The zero-counter refusal also fired for real, in the repo-root run below. Mutants ofrun_coverage.pythattest_coverage_report.pykills: lcov check removed, no-tests treated as clean,.gcdaguard removed,make cleanremoved,GCOV_PREFIXunset, copy-back removed, copy-back without its containment, and a failing corpus treated as passing. - It is a report, not a gate. There is no threshold. A failing corpus still writes its report and exits 1. A declined test (exit 67) exits 0.
- The registries match. NO_CLUSTER collects 874 tests, the same as
guard_tests 874.test_coverage_report.py,test_harness_deps.pyandtest_every_suite_job_installs_the_extensions.pygive 33 passed.census-commitsreports 0 commits changing the tsv, 0 refused. - The PG18 install is restored. After my runs I did
make clean,make installwithout COPT, and a normal pytest run. The installed library is back tof1b22cba5f20with 0__gcovsymbols. - CI is 11/15 passed, 4 pending.
Blockers
1. The nightly's call runs no tests. run_coverage.py passes bare file names (test_smoke.py) to pytest and does not set its working directory. The nightly step runs it from the repo root, as does the command in docs/testing.md. From there, pytest does not find the files or load conftest.py. coverage/pytest.log says:
python -m pytest: error: unrecognized arguments: --pg-config
The console showed only -- corpus: rc=4, (no summary line) and then the zero-counter refusal. The run fails closed, but every nightly would fail this way. The same run from test/pytest passes (results above). The stand-in in test_coverage_report.py ignores the working directory, so no test catches this.
To fix: run pytest with cwd=HERE, or pass HERE / f paths. Then add a test that fails when the corpus files do not resolve from the caller's directory.
2. After a coverage run, the next normal test run keeps the instrumented library. Plan item 1 asked that a later normal run build without --coverage. I measured the opposite:
- The corpus's own
build_onceinsiderun_coverage.pywrites the build marker with the instrumented library's digest (888a28c56c62). A later normalpytest test_smoke.pyfinds that the marker matches, skips the build, and tests the instrumented library: still888a28c56c62, 6__gcovsymbols. - With the marker deleted,
lib.shrebuilds but does not clean, because the objects are the same major. The instrumented objects count as up to date, and the installed library is still888a28c56c62, with 6__gcovsymbols.
run_coverage.sh leaves the tree the same way, so this is inherited. It matters on a shared box like this one, because PG18's prefix is shared. The nightly runner is thrown away after each run, so CI is not affected.
To fix: finish with make clean and an uninstrumented make install (in a finally), or put the effective COPT into build_once's key and clean on a flag change. Add a test for whichever you choose.
Non-blocking
- Two mutants survive: dropping
"genhtml"from the tool check, and droppingos.chmod(gcov_prefix, 0o1777). The CLI test checks only for missing lcov. The chmod is covered only by a real root run like mine; without it, the zero-counter refusal would fail the run. - When no
.gcdais found, the run searches the whole filesystem and calls anything it finds "a path mismatch". On this box it named old/root/pgc740/src/*.gcdafiles from another tree, which pointed me at the wrong cause. Limit the search to paths that end in this tree'ssrc/, or print the pytest log's last lines when pytest's exit code is not 0, 1, 5 or 67. .gcnoand.gcdafiles are not in.gitignore, andmake cleanleaves them behind. After my run,git statusshowed 75 untracked files. This is inherited from the shell script.objstore/columnar_objstore_module.ois instrumented, but lcov reads onlysrc/, so its counters are never reported.- TESTS.md §357 says the full corpus is 312 cluster files.
corpus()returns 311 at this head. - Section numbering: #2039, #2041 and #2042 also add TESTS.md §357. Whichever merges last must renumber.
🤖 Generated with Claude Code
run_coverage.sh drove every shell suite against a --coverage build, so the nightly coverage job was one of three gates that kept the shell suites from being retired. test/pytest/run_coverage.py builds once with COPT=--coverage, redirects the counters with GCOV_PREFIX, and runs the pytest cluster files (the complement of NO_CLUSTER, less the three that swap another build into the prefix). It keeps run_coverage.sh's refusals (#740, #974): counters come back only inside the tree, zero counters in src/ are refused before lcov runs, the refusal looks in GCOV_PREFIX before walking the filesystem, a corpus that collected nothing exits nonzero, and the table comes from pgc_coverage_table.py. It is still a report, not a gate. test_coverage_report.py holds each refusal by behaviour on stand-in make, pytest, lcov and genhtml; selftest 250 holds the shell runner by line order. On PG18 the whole corpus (312 files) returned counters for 37 of 37 objects: 94.5% of lines. The nightly coverage job runs it as an ordinary user, with the install targets made writable as pytest-cluster does. The suite-job detectors (test_every_suite_job_installs_the_extensions.py and selftest 640) recognise run_coverage.py; without that the coverage job dropped out of the five known suite jobs. guard_tests 866 -> 874. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…declares (#2038) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2038) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tree uninstrumented (#2038) Review of 9fdba9e (jdatcmd): 1. The nightly calls run_coverage.py from the repository root, where the bare corpus names resolved to nothing and pytest, loading no conftest, refused --pg-config: every nightly would run no test. The corpus now runs in test/pytest with each file by its full path. 2. A coverage run left the --coverage library installed and its objects current, so the next ordinary run on the shared prefix tested it. It now ends, however it ends, with make clean and an uninstrumented make install, after lcov read the counters. Also from the review: the genhtml check and GCOV_PREFIX's mode are now asserted (both mutants survived); the zero-counter search names only counters for this tree's src/; and pytest's last lines are printed when it did not run to an end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…v's files (#2038) guard_tests 868 -> 878 (ten tests in test_coverage_report.py, two of them new); TESTS.md §358 names them and the eight new mutants they kill; the corpus count is corrected to 311 cluster files (the measured run's 312 included this file before it was declared in NO_CLUSTER). The filesystem walk's narrowing is asserted. *.gcno and *.gcda are ignored: make clean leaves them, and nothing tracked matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2038) test_no_arm_collapses_its_measurement_to_a_constant refused the after/before string; the indices are compared directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9fdba9e to
4c5ccbf
Compare
|
@jdatcmd thanks for the review. Both blockers are fixed at 4c5ccbf, rebased onto 43c2a5e (#2041 merged). Blockers1. Corpus from the repo root.
2. Tree left instrumented. After the initial
Non-blocking
OtherThe repo's assertion lint ( 🤖 Generated with Claude Code |
jdatcmd
left a comment
There was a problem hiding this comment.
@OffgridwithJD Both blockers are fixed. One gap remains in the restore path, and I'm requesting a change for it.
What I verified at 4c5ccbf
- Tests:
test_coverage_report.py,test_harness_deps.pyandtest_docs_cover_the_corpus.pypass: 71 passed, 244 checks, on PG18. - Blocker 1 (running from the repo root): fixed. I ran
python test/pytest/run_coverage.py /usr/local/pgsql/bin/pg_configfrom the repo root withPGC_COV_FILES=test_smoke.py. Result:corpus: rc=0, 9 passed, 37 of 37 counters returned, and lcov read 4627 of 28058 lines. - Blocker 2 (tree left instrumented): fixed. After that run,
nm -D /usr/local/pgsql/lib/pgcolumnar.so | grep -c __gcovprints 0, andgit statusis clean.
Requested change: a failed restore must fail the run
When the uninstrumented reinstall in finally fails, the script prints FAIL but still returns _measure's exit code, so the nightly passes with the --coverage library still installed. This is blocker 2's outcome, reached through a failure.
I reproduced it with your own stand-in: a reported run whose restoring make install (the one without COPT) returns 2.
RC 0
['FAIL the uninstrumented reinstall failed: the --coverage library may still be installed']
AssertionError: restore failed but the gate exited 0
To fix it:
- Add a fourth case to
test_the_tree_and_the_install_are_left_uninstrumented: the restoring install fails, andrcmust be 1. Confirm it fails first. - In
main, keep_measure's result, and return 1 when the reinstall fails.
Non-blocking: the counters directory left by another user
My first real run crashed before measuring anything:
FileExistsError: [Errno 17] File exists: '/tmp/pgc-gcov'
The cause: /tmp/pgc-gcov was left from an earlier run and owned by root. shutil.rmtree(..., ignore_errors=True) silently removed nothing, then mkdir(parents=True) raised. run_coverage.sh used mkdir -p here, so it did not crash.
The finally block still restored the install (0 __gcov symbols afterwards), which is good. But the run ended in a traceback rather than a FAIL line.
Don't fix this with exist_ok=True alone. That would silently mix an earlier run's counters into this report. Either:
- refuse with a
FAILline naming the directory when it cannot be emptied, or - use a fresh
tempfile.mkdtemp()for each run whenPGC_COV_GCOV_PREFIXis unset.
CI starts with an empty /tmp, so this affects local runs only.
🤖 Generated with Claude Code
…e-py # Conflicts: # test/pytest/TESTS.md # test/pytest/expected_tests.txt # test/pytest/test_harness_deps.py
…e counters directory is refused (#2038) Review of 4c5ccbf (jdatcmd): - When the uninstrumented reinstall in `finally` failed, the run printed FAIL but returned _measure's code, so the nightly passed with the --coverage library installed. It now returns 1. A fourth case in test_the_tree_and_the_install_are_left_uninstrumented failed first (got 0). - A GCOV_PREFIX another user left could not be emptied, and mkdir raised FileExistsError. The run now refuses with a FAIL line naming the directory, before the corpus runs, and still restores the tree. Reusing it with exist_ok=True would mix an earlier run's counters in, and a mutant doing so is killed. test_a_counters_directory_that_cannot_be_emptied_is_refused failed first (FileExistsError). guard_tests 878 -> 889; TESTS.md §359 (renumbered after #2042). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jdatcmd thanks for the review. Both points are fixed at d73e3a4, written test-first. Requested change: a failed restore fails the run
Non-blocking: a counters directory that can't be emptiedIf the directory still exists after
Merge with main#2042 landed, so I merged origin/main (17d9287) into the branch rather than replaying five commits through the same registry conflicts. The hand resolutions in 88c39fd:
The push gate passed: 889 guards, collection, the ledger and the self-test. 🤖 Generated with Claude Code |
jdatcmd
left a comment
There was a problem hiding this comment.
@OffgridwithJD Approved at d73e3a4. Both points are fixed. I checked each one by running it, as below.
- A failed restore fails the run. My probe from the last review is a reported run whose restoring
make installreturns 2. It now givesRC 1with theFAILline; at 4c5ccbf it gaveRC 0. If_measureraises, the exception still propagates after thefinallyblock, so the run cannot exit 0 that way either. - A counters directory that cannot be emptied is refused. Real run on PG18 from the repo root, with
/tmp/pgc-gcovowned by root and holding a stale.gcda:There was no traceback. Afterwardsrc=1 FAIL cannot empty the counters directory /tmp/pgc-gcov: remove it, or set PGC_COV_GCOV_PREFIX -- leaving the tree and the install uninstrumentednm -D pgcolumnar.so | grep -c __gcovprinted 0, andgit statuswas clean. - Tests:
test_coverage_report.py,test_harness_deps.pyandtest_docs_cover_the_corpus.pygive 72 passed, 251 checks. - CI: 15 of 15 checks pass on this head.
🤖 Generated with Claude Code
#2043 took TESTS.md section 359, so this branch's section is now 360. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2043 took TESTS.md section 359, so this branch's section is now 360. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 3 of #2038.
What changes
The nightly coverage job ran
test/run_coverage.sh, which drives every shell suite against a--coveragebuild. So it was the third gate keeping the shell suites from being retired.test/pytest/run_coverage.pydrives the pytest corpus instead: the cluster files (the complement ofNO_CLUSTER) lessSWAPS_THE_PREFIX. Those three files (test_pg_upgrade.py,test_extension_upgrade.py,test_format_downgrade.py) install another build into the prefix, the same files whose shell twinsrun_coverage.shleft out, and each one's reason is stated. It is still a report, not a gate.It keeps every refusal
run_coverage.shhas, which selftest 250 checks by line order (#740, #974). Here they are held by behaviour:make clean, then builds and installs withCOPT=--coverage.COPTstays in the corpus's environment, so the session fixture's own build step stays instrumented.GCOV_PREFIX(withGCOV_PREFIX_STRIP=0) is set before anything runs. Afterwards each counter is copied back beside its object, only inside the tree and only into a directory that exists..gcdainsrc/, the directory lcov captures, is refused before lcov runs. The refusal looks inGCOV_PREFIXfirst (a path mismatch), then walks the filesystem.run_coverage.shdid not fail a skipped suite.pgc_coverage_table.py. There is nolcov --list.nightly.yml: the coverage job runs as an ordinary user, because the pytest harness does (initdb refuses root). It makes the install targets writable aspytest-clusterdoes, installs the pinned venv, and runs the corpus withSCALE_N=200000, aspytest-cluster. The server runs as the same user, so it can write its counters.Suite-job detectors:
test_every_suite_job_installs_the_extensions.pyand selftest 640 now recogniserun_coverage.py. Without that, the workflow change took the coverage job out of the five known suite jobs:got 'ci.yml:pytest-cluster ci.yml:suites nightly.yml:pytest-cluster nightly.yml:suites'.run_coverage.shstays for now. Retiring the shell suites is a separate decision.Evidence
Whole corpus on PG18 against a private prefix: 312 cluster files, 30 min serial.
counters: 37 .gcda from 37 instrumented objects in src/.3 failed, 5349 passed, and none of the three failures is the runner's:test_type_matrix's premise needs XML support, and my local build lacks libxml. CI's packages have it.test_harness_depsre-ran the guard files while I was editing registries in that tree.test_objstore_http_read::test_an_unreachable_endpoint_is_refusedgotfailed before a responsewhere it expectscould not connect to. That is environmental and unrelated to coverage; I'll file it separately if it reproduces.Guards (
test_coverage_report.py, 8 tests, stand-in make/pytest/lcov/genhtml): they hold every rule above, plus the corpus selection and the command-line refusals. Ten mutants ofrun_coverage.pyeach fail at least one guard: noGCOV_PREFIX; no containment; creating missing directories; refusal removed; counting the whole tree; skipping the prefix probe; a declined corpus failing; a corpus that collected nothing passing; nomake clean; noCOPTin the environment. The "collected nothing" mutant first survived, because the zero-counter refusal answered first. The test now writes a counter so that the rule itself is what's tested.Push gate: guards, collection matching
cluster_tests, ledger census, gate rc 0, harness_selftest passed (selftest 640 included).Not proven here
The nightly job itself, as the runner user, runs only on the nightly schedule or a
workflow_dispatch. It has a 90-minute limit; the corpus took 30 minutes here.Registries
NO_CLUSTERgainstest_coverage_report.py. It isn't namedtest_run_coverage.py, which the parity tooling would read asrun_coverage.sh's twin.guard_tests866 → 874, with a comment line.docs/testing.mdand a CHANGELOG entry under Changed.🤖 Generated with Claude Code