Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,52 @@ true until the next version shipped.

### Fixed

- Every test script is executable, so the commands this project documents run as
written (#852). No released version is affected: nothing in the extension
changed, and this is test tooling only.

103 of the 262 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. The
fix gives those 103 files the execute bit.

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/`: `56ae5f8eb`, 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 instead of
a clean refusal. It now declares `#!/usr/bin/env python3`, as its eleven
siblings do.

A 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 the check
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 `56ae5f8eb`
did, reddens it naming that one file.

The sweep exempts exactly two directories. The parts in `test/selftest/` and
the scripts under `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`: it is a documented command (`docs/testing.md:132`)
that lives one level down, it was already correct, and a sweep of the top level
alone would have left the file most like this defect outside the guard.

- `docs/limitations.md` states the constant-typing rule correctly. The previous
wording said `smallint` and `real` "always need a cast". That is false: an
unadorned quoted literal is `unknown` and takes the column's type, so
Expand Down Expand Up @@ -112,6 +158,7 @@ true until the next version shipped.
Files exported by 1.0-alpha2 or earlier carry no statistics, and nothing
rewrites them in place. Export again to make one skippable.


- Index entries for live rows are no longer destroyed (#838). A released version
is affected: this is present in `v1.0-alpha2`.

Expand Down
9 changes: 9 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ and ends with `pgc_summary`.
- Register every suite in `SUITES` in `test/run_all_versions.sh`. That array is
**one name per line and sorted**; insert in sorted position, never at the end.
`harness_selftest` fails if the order decays.
- **A script is executable and declares its interpreter on line 1.**
`harness_selftest` sweeps every `.sh` and `.py` under `test/`, at any depth,
and fails if one has either without the other. Both halves are needed. The
matrix starts a suite as `bash test/<name>.sh`, which never reads the mode, so
only the documentation and a reader's shell ever see it: 103 scripts were
100644 when this rule was written, and 30 documented commands died with
`Permission denied` (#852). `test/selftest/` and `test/fixtures/` are exempt,
and only those two. Their contents are sourced or imported rather than run, so
the bit would advertise a way to run them that does not work.
- Count suites by asking the runner, never by parsing the source:
`bash test/run_all_versions.sh --list-suites | wc -l`. A text parser over the
array disagrees with bash on exactly the mistake this invites, and the
Expand Down
6 changes: 6 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ test/native_ios.sh /path/to/pg_config # native index-only scan
test/native_projection.sh /path/to/pg_config # native projections
```

Every script in that list is executable and names its interpreter on its first
line. Run it directly, as shown, or as `bash test/smoke.sh` if you prefer. The
matrix always uses the second form, so a script's own mode is invisible to it.
`harness_selftest` checks the mode instead, because a documented command that
fails with `Permission denied` is a defect a green matrix cannot see.

## Defects are fixed, not documented

A limitation written into the documentation stops looking like a defect. It reads
Expand Down
Empty file modified test/arrow_corpus.py
100644 → 100755
Empty file.
Empty file modified test/arrow_export.sh
100644 → 100755
Empty file.
Empty file modified test/arrow_import.sh
100644 → 100755
Empty file.
Empty file modified test/arrow_mutate.py
100644 → 100755
Empty file.
Empty file modified test/avro_corpus.py
100644 → 100755
Empty file.
Empty file modified test/avro_manifest.sh
100644 → 100755
Empty file.
Empty file modified test/bloom_sizing.sh
100644 → 100755
Empty file.
Empty file modified test/build_san.sh
100644 → 100755
Empty file.
Empty file modified test/catalog_natts.sh
100644 → 100755
Empty file.
Empty file modified test/concurrent_diff.sh
100644 → 100755
Empty file.
Empty file modified test/corruption.sh
100644 → 100755
Empty file.
Empty file modified test/craft_parquet_counts.py
100644 → 100755
Empty file.
1 change: 1 addition & 0 deletions test/crlf_listener.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import socket, sys
# Minimal capture listener: accept one connection, record the raw request bytes,
# return a short 200 so the client does not hang, then exit.
Expand Down
Empty file modified test/entry_point_privilege.sh
100644 → 100755
Empty file.
Empty file modified test/export_sink.sh
100644 → 100755
Empty file.
Empty file modified test/fsst_verdict_cache.sh
100644 → 100755
Empty file.
Empty file modified test/fuzz_arrow.sh
100644 → 100755
Empty file.
Empty file modified test/fuzz_avro.sh
100644 → 100755
Empty file.
Empty file modified test/fuzz_listing.sh
100644 → 100755
Empty file.
Empty file modified test/generated_columns.sh
100644 → 100755
Empty file.
Empty file modified test/hardening.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_catalog.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_data_files.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_deletes.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_dv.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_fdw.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_fdw_estimate.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_fdw_projection.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_malformed.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_name_mapping.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_objstore.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_rest.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_rest_scan.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_rest_server.py
100644 → 100755
Empty file.
Empty file modified test/iceberg_rest_server.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_rest_vended.sh
100644 → 100755
Empty file.
Empty file modified test/iceberg_scan.sh
100644 → 100755
Empty file.
Empty file modified test/index_delete_liveness.sh
100644 → 100755
Empty file.
Empty file modified test/index_only.sh
100644 → 100755
Empty file.
Empty file modified test/listing_corpus.py
100644 → 100755
Empty file.
Empty file modified test/native_agg.sh
100644 → 100755
Empty file.
Empty file modified test/native_batch_fold_projection.sh
100644 → 100755
Empty file.
Empty file modified test/native_bloom.sh
100644 → 100755
Empty file.
Empty file modified test/native_decode_gating.sh
100644 → 100755
Empty file.
Empty file modified test/native_dict_underfill.sh
100644 → 100755
Empty file.
Empty file modified test/native_dml.sh
100644 → 100755
Empty file.
Empty file modified test/native_encoding.sh
100644 → 100755
Empty file.
Empty file modified test/native_fetch_bigcap.sh
100644 → 100755
Empty file.
Empty file modified test/native_fold_deferral.sh
100644 → 100755
Empty file.
Empty file modified test/native_fold_skipguard.sh
100644 → 100755
Empty file.
Empty file modified test/native_gap.sh
100644 → 100755
Empty file.
Empty file modified test/native_groupagg.sh
100644 → 100755
Empty file.
Empty file modified test/native_groupagg_wide_cost.sh
100644 → 100755
Empty file.
Empty file modified test/native_index.sh
100644 → 100755
Empty file.
Empty file modified test/native_index_fetch_stripe_cost.sh
100644 → 100755
Empty file.
Empty file modified test/native_ios.sh
100644 → 100755
Empty file.
Empty file modified test/native_late_materialization.sh
100644 → 100755
Empty file.
Empty file modified test/native_ownership.sh
100644 → 100755
Empty file.
Empty file modified test/native_parquet_dict_oob.sh
100644 → 100755
Empty file.
Empty file modified test/native_parquet_fieldid.sh
100644 → 100755
Empty file.
Empty file modified test/native_projection.sh
100644 → 100755
Empty file.
Empty file modified test/native_reclaim_cycles.sh
100644 → 100755
Empty file.
Empty file modified test/native_reclaim_frag.sh
100644 → 100755
Empty file.
Empty file modified test/native_reclaim_reconcile.sh
100644 → 100755
Empty file.
Empty file modified test/native_roundtrip.sh
100644 → 100755
Empty file.
Empty file modified test/native_scale.sh
100644 → 100755
Empty file.
Empty file modified test/native_skip.sh
100644 → 100755
Empty file.
Empty file modified test/native_sort_by.sh
100644 → 100755
Empty file.
Empty file modified test/native_truncate.sh
100644 → 100755
Empty file.
Empty file modified test/native_upgrade_converge.sh
100644 → 100755
Empty file.
Empty file modified test/native_vacuum_race.sh
100644 → 100755
Empty file.
Empty file modified test/native_vecdecode.sh
100644 → 100755
Empty file.
Empty file modified test/native_vecskip.sh
100644 → 100755
Empty file.
Empty file modified test/native_writer.sh
100644 → 100755
Empty file.
Empty file modified test/native_zonemap.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_addressing.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_allowlist.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_credentials.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_crlf.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_http_read.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_http_server.py
100644 → 100755
Empty file.
Empty file modified test/objstore_listing.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_s3_read.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_sink_write.sh
100644 → 100755
Empty file.
Empty file modified test/objstore_tls_read.sh
100644 → 100755
Empty file.
Empty file modified test/parallel.sh
100644 → 100755
Empty file.
Empty file modified test/parallel_export_parquet.sh
100644 → 100755
Empty file.
Empty file modified test/parallel_vector_agg.sh
100644 → 100755
Empty file.
Empty file modified test/parquet_export.sh
100644 → 100755
Empty file.
Empty file modified test/parquet_nested_import.sh
100644 → 100755
Empty file.
Empty file modified test/phase6.sh
100644 → 100755
Empty file.
Empty file modified test/portlib.sh
100644 → 100755
Empty file.
Empty file modified test/read_stream.sh
100644 → 100755
Empty file.
Empty file modified test/run_all_versions.sh
100644 → 100755
Empty file.
Empty file modified test/run_san.sh
100644 → 100755
Empty file.
Empty file modified test/scan_direction.sh
100644 → 100755
Empty file.
192 changes: 192 additions & 0 deletions test/selftest/300-a-test-script-must-be-runnable.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# ---- a test script must be runnable the way it is documented (#852) ---------
#
# Every document that tells a reader how to run a script spells it as a command:
# `test/temporal.sh /path/to/pg_config`, `PGC_RUN_UPGRADE=1
# test/run_all_versions.sh`, `test/pbt/run.sh [seed] [iterations]`. 103 of the
# 262 top-level scripts were mode 100644 at the base this lands on, so 30 of
# those documented invocations died with `Permission denied` before running a
# single statement.
#
# THE DENOMINATOR MOVED UNDER THIS BRANCH AND THE NUMERATOR DID NOT. It was 260
# when this was written; #851 landed first and added test/parquet_export_stats.sh
# and test/parquet_stats.py, both 100755, so the population grew by two and the
# defect did not. Re-measured on the rebased base rather than carried over.
#
# THE MATRIX NEVER SAW IT, AND THAT IS THE POINT. ci.yml:485 and nightly.yml:188
# both call `bash test/run_all_versions.sh`, and the runner launches each suite
# as `bash "$builddir/test/${s}.sh"` (lines 712 and 749). An interpreter named on
# the command line does not consult the execute bit, so a suite's own mode is
# invisible to every green run in this project's history. Those runs are honest.
# The gap is between the documentation and a shell, and only a check that reads
# the MODE can stand in it.
#
# TWO SITUATIONS, NOT ONE. Exactly one 100755 -> 100644 transition exists in the
# whole history of test/ -- 56ae5f8eb, 2026-08-16, 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 that a check like this one would have
# caught the day it happened, and the rest are a habit that nothing ever
# contradicted.
#
# WHY THE RULE IS ANCHORED ON THE SHEBANG. The alternative populations both
# fail. A documentation-derived list ("every script docs/testing.md names") makes
# the gate a hostage to prose: delete a line from a document and the check
# silently narrows, which is the same class of accident as the one being fixed.
# A hand-maintained allowlist of names drifts, and 102 files born wrong is what
# drift looks like. The file's own first line is the honest population: a script
# that opens `#!/usr/bin/env bash` has declared that it is meant to be run, and a
# mode that forbids running it contradicts what the file already says about
# itself. So the rule needs no name list, and the two shared libraries need no
# exemption -- lib.sh carries a shebang and the bit, and portlib.sh gets the bit
# here for the same reason every suite does.
#
# THE EXEMPTION IS TWO DIRECTORIES, NOT A LIST OF FILES, AND THE TWO ARE EXEMPT
# FOR DIFFERENT REASONS. An earlier version of this comment gave one reason for
# both -- "SOURCED or imported, never executed" -- and it was false for
# test/fixtures/ in both halves.
#
# test/selftest/ holds SOURCED fragments. harness_selftest.sh:53-56 sources every
# part, and they carry no shebang precisely because nothing executes them. Giving
# them the bit would advertise a way to run them that does not work, which is
# this defect pointing the other way.
#
# test/fixtures/ is DATA. Nothing in test/ sources, imports or runs any of it:
# every reference in test/*.sh is a PATH -- 18 of them, all of the shape
# `FX="$(dirname "${BASH_SOURCE[0]}")/fixtures/iceberg"`, plus one .sql read in
# native_upgrade_converge.sh:71 -- and no suite invokes a generator. Measured, on
# main, rather than assumed. Of 406 tracked files there, 393 have no first line
# to declare anything. The 13 that do are host tools rather than suite entry
# points: three crosschecks, each documenting an explicit interpreter
# (crosscheck_dv.py:23, `V/bin/python test/fixtures/iceberg/crosscheck_dv.py`),
# and ten generators that record how a committed fixture was made, run by hand
# when one is regenerated. An interpreter named on the command line does not
# consult the execute bit, which is the same mechanism this file relies on for
# the matrix above.
#
# AND WHAT THE EXEMPTION COSTS, said rather than left for a reader to find. Those
# 13 are shebang-without-the-bit -- the exact state this file calls
# self-contradictory -- and the way out below is taken for none of them. It is a
# real gap in the rule's coverage, left deliberately because it is not #852's
# defect: no documented command invokes any of them bare, so none of them is
# broken today. Closing it is its own change.
#
# Every OTHER directory under test/ is swept, so a runnable entry
# point in a new subdirectory is covered the day it is added. test/pbt/run.sh is
# why that matters: it is a documented command (docs/testing.md:132) that lives
# one level down, it is already correct, and a top-level-only sweep would have
# left the one file most like the defect outside the guard.
#
# THE WAY OUT, IF A FUTURE SCRIPT MUST NOT BE EXECUTABLE, IS TO DROP ITS SHEBANG
# AND SAY WHY IN ITS HEADER -- not to leave a 100644 file whose first line still
# claims otherwise. The second check below is what makes that an explicit act: a
# file with neither is caught too, so "no shebang" cannot become the new silent
# default.

# ---- controls ---------------------------------------------------------------
#
# A mode sweep that has never fired is indistinguishable from a tree that is
# already clean, and this one is added to a tree where it fires 103 times. The
# fixtures pin all three verdicts, so the sweep below is known to separate them
# before it is believed about the real tree.

_tsm_has_shebang() { # _tsm_has_shebang FILE
head -1 "$1" 2>/dev/null | grep -q '^#!'
}

_tsm_verdict() { # _tsm_verdict FILE -> ok | noexec | noshebang
if ! _tsm_has_shebang "$1"; then echo noshebang
elif [ ! -x "$1" ]; then echo noexec
else echo ok
fi
}

_tsm_fix="$PGC_WORKDIR/tsm_shebang_noexec.sh"
printf '#!/usr/bin/env bash\nexit 0\n' > "$_tsm_fix"; chmod 644 "$_tsm_fix"
check "control: a script that declares an interpreter without the bit is caught" \
"$(_tsm_verdict "$_tsm_fix")" "noexec"

_tsm_ok="$PGC_WORKDIR/tsm_shebang_exec.sh"
printf '#!/usr/bin/env bash\nexit 0\n' > "$_tsm_ok"; chmod 755 "$_tsm_ok"
check "control: and the same script with the bit is not" \
"$(_tsm_verdict "$_tsm_ok")" "ok"

_tsm_none="$PGC_WORKDIR/tsm_no_shebang.py"
printf 'import sys\n' > "$_tsm_none"; chmod 644 "$_tsm_none"
check "control: and a script with no interpreter line is caught separately" \
"$(_tsm_verdict "$_tsm_none")" "noshebang"

# The bit has to survive the copy, or this check reads a mode the repository
# does not have. run_all_versions.sh:668 stages the tree with `cp -a`, and the
# matrix runs these parts out of that staged copy. Pinned as a property of the
# copy rather than trusted from the flag.
_tsm_cp_src="$PGC_WORKDIR/tsm_copy_src.sh"
printf '#!/usr/bin/env bash\nexit 0\n' > "$_tsm_cp_src"; chmod 755 "$_tsm_cp_src"
cp -a "$_tsm_cp_src" "$PGC_WORKDIR/tsm_copy_dst.sh"
check "control: cp -a preserves the execute bit, so a staged tree reads the same" \
"$(_tsm_verdict "$PGC_WORKDIR/tsm_copy_dst.sh")" "ok"

# ---- the population ---------------------------------------------------------
#
# Every .sh and .py under test/, at any depth, MINUS the two sourced
# directories. Pruned with -path so a file is excluded by the directory it is
# in, not by matching its name against a list.

_tsm_scripts=()
while IFS= read -r _tsm_f; do
_tsm_scripts+=("$_tsm_f")
done < <(find "$PGC_TESTDIR" \
\( -path "$PGC_TESTDIR/selftest" -o -path "$PGC_TESTDIR/fixtures" \) -prune -o \
-type f \( -name '*.sh' -o -name '*.py' \) -print | sort)

# ---- premises ---------------------------------------------------------------

# A sweep over an empty population passes vacuously, and a find that matched
# nothing looks exactly like a clean tree.
check "premise: the sweep reads a population of scripts, not an empty find" \
"$([ "${#_tsm_scripts[@]}" -ge 200 ] && echo enough || echo "${#_tsm_scripts[@]}")" "enough"

# The prune must exclude those two directories and nothing else. Both halves
# matter: an over-broad prune would silently empty the sweep, and a prune that
# missed would redden every sourced part.
check "premise: the sourced parts and the fixtures are outside the population" \
"$(printf '%s\n' "${_tsm_scripts[@]}" | grep -cE '/(selftest|fixtures)/')" "0"

# and the prune did not also swallow a subdirectory that IS swept
check "premise: a runnable script one level down is inside the population" \
"$(printf '%s\n' "${_tsm_scripts[@]}" | grep -c '/pbt/run\.sh$')" "1"

# The two exempted directories are non-empty, so the exemption is a real
# decision about real files rather than a prune of nothing.
check "premise: the exempted directories actually hold scripts" \
"$([ "$(find "$PGC_TESTDIR/selftest" "$PGC_TESTDIR/fixtures" -type f \
\( -name '*.sh' -o -name '*.py' \) | wc -l)" -ge 40 ] && echo yes || echo no)" "yes"

# ---- the sweep --------------------------------------------------------------

_tsm_noexec=""; _tsm_noexec_n=0
_tsm_noshebang=""; _tsm_noshebang_n=0
for _tsm_f in "${_tsm_scripts[@]}"; do
case "$(_tsm_verdict "$_tsm_f")" in
noexec)
_tsm_noexec_n=$((_tsm_noexec_n + 1))
[ "$_tsm_noexec_n" -le 5 ] && _tsm_noexec="$_tsm_noexec ${_tsm_f#"$PGC_TESTDIR"/}"
;;
noshebang)
_tsm_noshebang_n=$((_tsm_noshebang_n + 1))
[ "$_tsm_noshebang_n" -le 5 ] && _tsm_noshebang="$_tsm_noshebang ${_tsm_f#"$PGC_TESTDIR"/}"
;;
esac
done

# The count leads the message and at most five names follow it: a real failure
# is one or two files and wants naming, while the 103 this was written against
# would otherwise print an unreadable line.
_tsm_fmt() { # _tsm_fmt N NAMES
[ "$1" -eq 0 ] && { echo "[]"; return; }
echo "[$1:$2]"
}

check "every documented test script is executable" \
"$(_tsm_fmt "$_tsm_noexec_n" "$_tsm_noexec")" "[]"

check "and every one of them declares its interpreter" \
"$(_tsm_fmt "$_tsm_noshebang_n" "$_tsm_noshebang")" "[]"
Empty file modified test/server_file_privilege.sh
100644 → 100755
Empty file.
Empty file modified test/sorted_projection.sh
100644 → 100755
Empty file.
Empty file modified test/temporal.sh
100644 → 100755
Empty file.
Empty file modified test/ungrouped_vector_agg.sh
100644 → 100755
Empty file.
Empty file modified test/update_conc.sh
100644 → 100755
Empty file.
Empty file modified test/zonemap_cost.sh
100644 → 100755
Empty file.
Empty file modified test/zonemap_estimate_sample.sh
100644 → 100755
Empty file.
Loading