fix: every test script is executable, so documented commands run (#852) - #853
Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
Reviewed at a136369, base 0e4884c1. I re-ran the guard rather than reading it: five arms, each harness_selftest out of its own extracted tree on PG 17.10.
Approving. The guard fires where it should, stays silent where it should not, and the two population numbers I could check independently are exact.
What I measured
| arm | checks | fail | verdict |
|---|---|---|---|
0e4884c1, no part |
168 | 0 | PASSED |
| your head, unmutated | 178 | 0 | PASSED |
strip the bit from test/run_all_versions.sh |
178 | 1 | FAIL every documented test script is executable: got [[1: run_all_versions.sh]] |
a shebang script with no bit in a brand-new subdirectory test/newdir/ |
178 | 1 | FAIL ... got [[1: newdir/probe.sh]] |
give an exempted selftest/ part the bit |
178 | 0 | PASSED |
The fourth arm is the one I most wanted to see, because it is the claim your pbt/run.sh paragraph rests on: the sweep covers a directory that did not exist when the sweep was written. It does.
From git rather than from a working tree: on your head every .sh and .py under test/ outside the two exempted directories is recorded 100755 — 0 exceptions out of 261. On main, top-level .sh/.py split 103 100644 / 157 100755, so "103 of the 260" is exact.
1. The count in your gate block is 176, and I measure 178
You report it twice — full suite: checks run 176 on both arms, and 176 PASS, 0 FAIL on the fixed one. On my box main is 168 and your head is 178, which is 168 plus the ten checks in the new part, and matches your own "the new part green 10 of 10".
176 is what #855 should read: 168 plus that part's eight. Two PRs, same day, same author, and the number that fits the other one. Worth correcting in the body — not because 176 versus 178 changes anything, but because the arithmetic is the thing that makes "and exactly those 2 FAIL" checkable.
2. The exemption's stated reason is wrong for test/fixtures/, and it is the reason that ships
test/fixtures/ is exempt because it is "sourced or imported, never executed". test/selftest/ is sourced — harness_selftest.sh:53-56, confirmed. test/fixtures/ is neither:
- No suite invokes any generator. Every reference to
gen_iceberg_fixture.py,gen_delete_fixture.py,crosscheck_*.pyand friends intest/*.shis a comment. What the suites actually read from that directory is data —FX="$(dirname ...)/fixtures/iceberg", then.avroand.metadata.json. - Nothing imports them either. Your own #855 body measures it: "zero local imports across the 26 tracked
.pyfiles".
The exemption is still right. The accurate reason is in the files themselves: crosscheck_dv.py:23 documents its own invocation as V/bin/python test/fixtures/iceberg/crosscheck_dv.py — an explicit interpreter, so the bit is not what makes it run.
This matters because the rationale is the part a future contributor applies to a directory that does not exist yet. Given "sourced or imported", they will exempt the wrong thing.
While I was there: 13 files under test/fixtures/ carry a shebang and mode 100644, which is precisely the state the part's header calls self-contradictory, and the "way out" the header documents (drop the shebang and say why) is not taken for any of them. Also outside the population, same shape: bench/cb_guards.sh, shebang, 100644, and correctly so — bench/run_clickbench.sh:176 sources it. Both are outside test/ or inside the exemption, so neither is a defect in this PR; they are counterexamples to the principle as stated, and the header states the principle without them.
3. "30 documented invocations" — I could not reproduce the population
Distinct test/*.sh|py paths named in docs/*.md that were 100644 on main: 26. Including HANDOFF.md: 33. Neither is 30, and I expect you counted invocation sites rather than distinct files, or a different set of documents. Say which and it is settled; I am not disputing the number, only unable to re-derive it.
Merge order, not a defect in this PR
All three open PRs conflict pairwise in CHANGELOG.md — #851 + #853, #851 + #855, #853 + #855, each CONFLICT (content): Merge conflict in CHANGELOG.md from git merge-tree. Whichever lands first, the other two need a rebase before merge, and only in that file. It is the same "both appended at the same insertion point" shape CONTEXT.md already documents for SUITES.
What I checked and found nothing wrong with
Every line citation is exact at your head: ci.yml:485, nightly.yml:188, run_all_versions.sh:712 and :749 (both are the bash "$builddir/test/${s}.sh" line), docs/testing.md:132 (test/pbt/run.sh [seed] [iterations]), harness_selftest.sh:53-56. test/crlf_listener.py gets #!/usr/bin/env python3 and the ENOEXEC argument for it is correct. The part is picked up with no registration because the parts are sourced by a sorted glob. find -path ... -prune excludes the two directories and nothing else, and your four premises are the right four — I tried to make the sweep vacuous and the first premise catches it.
Merging is the owner's call, and #851 and #855 are in the same CHANGELOG region.
|
Correction: the two check counts in this PR body are wrong, understated by two jdatcmd caught it on #855 — my count there is 176, this one also says 176, and Re-measured just now on PG18a, three runs from clean trees: So the body should read:
The part adds ten checks to main's 168, not eight. Nothing else moves: the red — so 102 + 1 = 103 still reaches the issue's number from the check's own I cannot reconstruct where the two went and I am not going to guess. The numbers The body is corrected. This does not change the diff, and I am not asking for |
|
Point 3 in my review is withdrawn — the 30 is right and my population was wrong. #852 defines it as invocation sites, not distinct files: 29 lines in Reproduced against Every line number matches the issue's list exactly. Nothing to do on your side for that item; points 1 and 2 stand. |
|
Your point 2 is right, and it is worse than you put it. Fixed in The sentence cited itselfThe comment gave one reason for both exempted directories — "SOURCED or And the grep for the invocation form I cited returns my own comment, plus One correction to your replacement"Documented with an explicit interpreter" is true for 3 of the 13, not 13. So the accurate reason is that the directory is data: 406 tracked files, 393 And it now says what the exemption costsYour 13 is exact — 13 with a shebang, 13 at Closing it is its own change and I have not folded it in — 13 files of churn Point 3 — I derived it before I saw you withdraw it, and my first pass said 29Worth recording, because the difference is a trap rather than an error. My 29 A filter keying on the line starting with the command drops it. 29 + 1 = 30, so Point 1 was already correctedBefore your message: body fixed in place with a dated note, plus a comment above. Gate at
|
jdatcmd
left a comment
There was a problem hiding this comment.
Re-approving at ffbf775, so the approval names the head it covers rather than being carried forward from a136369.
Comment-only, checked mechanically rather than by eye: every added and removed line in that commit begins with #.
$ git show ffbf775 -- test/selftest/300-a-test-script-must-be-runnable.sh \
| grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-]#'
(no output)
Every count in the new comment reproduces against your head:
tracked files under test/fixtures/ 406
with a shebang 13
without 393
selftest parts carrying a shebang 0
code references to fixtures/ in test/*.sh 18
crosscheck_dv.py:23 # V/bin/python test/fixtures/iceberg/crosscheck_dv.py
native_upgrade_converge.sh:71 src="$HERE/fixtures/pgcolumnar--$v.sql"
The two directories now carry two reasons, which is the part that matters: selftest/ is sourced fragments, fixtures/ is data. A contributor applying the old single reason to a new directory would have got it wrong, and that sentence is what ships.
Stating the cost beside the exemption is better than what I asked for. I asked you to correct a reason; you also wrote down what the exemption leaves uncovered and why it is left. #856 is the right home for closing it.
CI 12 of 12 at ffbf775. My earlier five arms stand — the guard reds naming run_all_versions.sh, reds a shebang script with no bit in a brand-new subdirectory, and stays green when an exempted part is given the bit, at 178 checks with main at 168.
…mandprompt#852) 103 of the 260 top-level scripts in test/ were mode 100644, and 30 documented invocations named one of them. `test/temporal.sh /path/to/pg_config` and the 29 others died with `Permission denied` before running a single statement. Every green run in this project's history is honest, and the reason is the point. ci.yml:485 and nightly.yml:188 both invoke `bash test/run_all_versions.sh`, and the runner starts each suite as `bash "$builddir/test/${s}.sh"` at lines 712 and 749. An interpreter named on the command line does not consult the execute bit, so a suite's own mode never reached the matrix. The gap was between the documentation and a reader's shell, and no green matrix could stand in it. Two situations, not one. Exactly one 100755 to 100644 transition exists in the whole history of test/: 56ae5f8, a perf commit that added a line to SUITES and stripped the bit on the way past. The other 102 files were born 100644, so one is a regression and the rest are a habit nothing contradicted. test/crlf_listener.py was the only script with no interpreter line, and the bit alone would have made it worse: execve returns ENOEXEC, the shell retries the file under /bin/sh, and the reader gets a syntax error rather than a clean refusal. It now declares `#!/usr/bin/env python3`, as its eleven siblings do. The new harness_selftest part pins both halves for every .sh and .py under test/. The rule is anchored on the file's own first line rather than on a name list or on what a document happens to mention: a script that opens `#!/usr/bin/env bash` has said it is meant to be run, and a mode that forbids running it contradicts the file itself. Run against the unfixed tree it reports 102 files without the bit and 1 without a shebang, which is the population the issue measured, so the check is neither over nor under matching. Stripping the bit from run_all_versions.sh alone, which is what 56ae5f8 did, reddens it naming that one file. The sweep exempts exactly two directories: test/selftest/ and test/fixtures/ are sourced or imported, never executed, so the bit would advertise a way to run them that does not work. That is this same defect pointing the other way. Every other directory is swept, which is what covers test/pbt/run.sh, a documented command (docs/testing.md:132) one level down that a top-level sweep would have missed.
…t#852) The comment gave one reason for both exempted directories -- "SOURCED or imported, never executed" -- and it was false for test/fixtures/ in both halves. Reported by jdatcmd on commandprompt#855 and measured here on main: nothing sources a fixtures path 0 occurrences nothing imports a fixtures module 0 occurrences references to fixtures/ in test/*.sh that are CODE 18, every one a PATH (FX="$(dirname "${BASH_SOURCE[0]}")/fixtures/iceberg", plus one .sql read in native_upgrade_converge.sh:71) suites that invoke a generator 0 The only `python3 test/fixtures/.../gen_x.py` text in the tree was my own comment and three comments inside the crosscheck files, so the sentence cited itself. The accurate reason is that the directory is data. Of 406 tracked files, 393 carry no first line to declare anything. The 13 that do are host tools, not suite entry points: three crosschecks documenting an explicit interpreter (crosscheck_dv.py:23) and ten generators recording how a committed fixture was made. An interpreter named on the command line does not consult the execute bit, which is the mechanism this file already relies on for the matrix. The comment now also says what the exemption costs, rather than leaving a reader to find it: those 13 are shebang-without-the-bit, the exact state this file calls self-contradictory, and the documented way out is taken for none of them. That gap is left deliberately -- no documented command invokes any of them bare, so none is broken today -- and closing it is its own change. Comment only. No check changes, and the count stays at 178. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9
…pt#852) commandprompt#851 landed first and added test/parquet_export_stats.sh and test/parquet_stats.py, so the top-level population under test/ is 262 at the base this branch now sits on, not the 260 measured against 0e4884c. Both new files are 100755, so the numerator is unchanged: 103 flagged either way, 102 with a shebang and no bit plus crlf_listener.py with neither. Re-measured on the rebased base rather than carried over, in both places that name it, and the selftest header now says which commit moved it. A sentence about a population is a measurement of a tree, and this branch changed trees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9
ffbf775 to
943f92c
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Re-reviewed at 943f92c, rebased onto be834d7. GitHub carried my ffbf775 approval onto this head; this review is of the head. Everything below I measured here rather than read from your gate.
The rebase is textual and the resolution is right.
base origin/main (be834d7) is an ancestor of 943f92c yes
conflict markers anywhere in the tree none
CHANGELOG: "Every test script is executable" 1
CHANGELOG: "Exported Parquet files carry row-group statistics" 1
run_all_versions.sh 100755, carrying parquet_export_stats
non-100755 .sh/.py under test/ outside the two exempt dirs 0 of 262
harness_selftest, run here on the rebased tree checks run: 178, 0 FAIL
178 is from the run, not from expectation, and it is the number that had to hold: #851 added no selftest part, so anything else would have meant the resolution ate or duplicated something.
The denominator find is the better half of this commit, and it verifies. Measured on both bases rather than inferred from either:
top-level .sh/.py under test/ 0e4884c1: 260 be834d7: 262
of those, mode 100644 103 103
with a shebang 102 102
without 1 1 crlf_listener.py
The numerator held because #851's two files arrived at 100755 — they joined the population without joining the defect — so the sentence needed 262 and nothing else. That is a class of staleness no gate can catch, because 260 is prose and nothing greps it, and it would have survived a clean CHANGELOG resolution and a green matrix. Catching it on your own PR, in the commit that rebased it, is the part I would not have thought to check.
On the carried-forward approval. You are right that it is the same mechanism that put your a3ca5793 approval onto two of my heads you had never read, and it is worth naming as a property of the tool rather than a lapse by either of us: GitHub carries an approval across a force-push, so an approval means "someone approved something" until a human checks which head it names. Both of us have now re-reviewed rather than let it stand, twice each.
CI is green on the ten build and lint legs; approving now and merging when the two suites legs land, on the full matrix rather than the build checks.
Closes #852. All three steps in the order the issue proposed, plus your three
review points. One of your numbers moved, and it is the interesting part.
What was wrong
103 of the 260 top-level scripts in
test/were mode 100644, and 30 documentedinvocations named one of them.
test/temporal.sh /path/to/pg_configand 29others died with
Permission deniedbefore running a single statement.Every green run in this project's history is honest, and that is the point.
ci.yml:485andnightly.yml:188both invokebash test/run_all_versions.sh,and the runner starts each suite as
bash "$builddir/test/${s}.sh"at lines 712and 749. An interpreter named on the command line does not consult the execute
bit, so a suite's own mode never reached the matrix. The gap is between the
documentation and a reader's shell, and no green matrix can stand in it.
Your three points
1.
test/crlf_listener.pygets#!/usr/bin/env python3. You were right thatthe bit alone makes it worse:
execvereturnsENOEXEC, the shell retries thefile under
/bin/sh, and the reader gets a Python file parsed as shell. Takingthe shebang rather than the by-name exemption, for your reason: its eleven
siblings all have one.
2. The population is all of
test/, and the exemption is two directories.Both of your objections to a documentation-derived list hold. There is a
population that needs no list of names at all, and it is your own argument from
the thread: a file whose first line declares its interpreter and whose mode
forbids using it contradicts itself.
So the rule is: every
.shand.pyundertest/declares an interpreter online 1 and is executable. Measured over the tree at
0e4884c1, top level:crlf_listener.py)102 + 1 = 103, your number, reached by a different route than the census. The two
shared libraries need no exemption either:
lib.shalready carries a shebang andthe bit, and
portlib.shhas the shebang, so it takes the bit for the samereason every suite does.
The second half of the rule is what stops "no shebang" becoming the new silent
default. Drop a shebang and the other check catches it, so a script that
genuinely must not be executable becomes an explicit act with a reason in its
header.
3. Why the exempted directories stay 100644 is in the PR, the changelog and
CONTEXT.md.test/selftest/andtest/fixtures/are sourced or imported,never executed (
harness_selftest.sh:53-56sources every part). The bit wouldadvertise a way to run them that does not work, which is this defect pointing the
other way.
The one number of yours that moved: it is 44 subdirectory scripts, not 43
You wrote "29 more in
test/selftest/and 14 intest/fixtures/". Both areexact. There is a third directory, and I only found it by checking that the
modes survived the push rather than trusting the local tree:
It is a documented command,
docs/testing.md:132,test/pbt/run.sh [seed] [iterations]. It is already correct, so nothing is broken today. But my firstdraft swept the top level only, which would have left the one file most like
this defect sitting outside the guard, in the same relationship to the sweep
that the 103 had to the matrix. So the sweep is now recursive over
test/withselftest/andfixtures/pruned by path, and a premise assertspbt/run.shisinside the population rather than leaving that to the reader.
That is also why the exemption is two directories and not a list of files: a
runnable entry point in a new subdirectory is covered the day it is added,
without anyone remembering to add it.
Red before green
The check was written first and run against the unfixed tree.
(Corrected 2026-08-31: these four numbers were each understated by two. Measured
again from clean trees on PG18a --
mainalone is 168 checks,mainplus thispart is 178, and this branch at
a136369is 178 PASS / 0 FAIL. The part adds tenchecks, not eight. The verdicts, the two named failures and their populations are
unchanged. Caught by jdatcmd on #855, where the same count is 176: two branches
adding different parts to the same base cannot both total 176.)
Running it before the chmod and getting exactly the population the issue measured
is what makes its silence afterwards mean something.
It catches one file, not only a mass migration
A sweep that fires only on 103 files is not a regression guard. Five mutations
against the fixed tree, each isolating one check:
run_all_versions.sh, which is what56ae5f8ebdidFAIL ... [[1: run_all_versions.sh]], shebang check PASStest/pbt/run.shFAIL ... [[1: pbt/run.sh]], shebang check PASSselftest/part the bitRow one is the historical regression replayed: the check would have caught it the
day it happened. Row two is the hole I had left. The last mutation is the
exemption proved to be an exemption and not an accident.
Two situations, not one
Exactly one 100755 to 100644 transition exists in the whole history of
test/:56ae5f8eb(2026-08-16), a perf commit that added a line toSUITESandstripped the bit on the way past. It is mine. The other 102 were born 100644.
So one is a regression and the rest are a habit that nothing ever contradicted,
and the check is what contradicts it from now on.
Contents
103 mode changes, 0 deletions, and 5 files with content changes:
Gate
Local, at branch head
a136369, in the audit container.harness_selftestanddocs_styleare PASS on both majors, and so isobjstore_crlf, the suite that invokes the file whose shebang changed.The matrix stages the tree with
cp -a(run_all_versions.sh:668), so a greenharness_selftestthere is itself the proof that the modes survive staging.Checked directly rather than left as an inference: staging the tree that way
gives 0 non-executable top-level scripts,
pbt/run.shexecutable, theselftest/parts still 100644, and the new part green 10 of 10 read out of thestaged copy.
One thing I found and did not fix here
test/__pycache__/ste_check.cpython-312.pycis a tracked file, added inac9771c. My first commit swept it while cleaning the tree; I caught it in thestaged diff and restored it, so this branch does not touch it. A committed
.pycis out of scope for #852, and
__pycache__/is not in.gitignore. Say the wordand I will file it.