From 41fdfcf6f38de5314d03abf03cb4402bb5c47fc5 Mon Sep 17 00:00:00 2001 From: OffgridwithJD Date: Mon, 31 Aug 2026 16:47:03 +0000 Subject: [PATCH 1/3] fix: every test script is executable, so documented commands run (#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/: 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 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 56ae5f8eb 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. --- CHANGELOG.md | 47 +++++ CONTEXT.md | 9 + docs/testing.md | 6 + test/arrow_corpus.py | 0 test/arrow_export.sh | 0 test/arrow_import.sh | 0 test/arrow_mutate.py | 0 test/avro_corpus.py | 0 test/avro_manifest.sh | 0 test/bloom_sizing.sh | 0 test/build_san.sh | 0 test/catalog_natts.sh | 0 test/concurrent_diff.sh | 0 test/corruption.sh | 0 test/craft_parquet_counts.py | 0 test/crlf_listener.py | 1 + test/entry_point_privilege.sh | 0 test/export_sink.sh | 0 test/fsst_verdict_cache.sh | 0 test/fuzz_arrow.sh | 0 test/fuzz_avro.sh | 0 test/fuzz_listing.sh | 0 test/generated_columns.sh | 0 test/hardening.sh | 0 test/iceberg_catalog.sh | 0 test/iceberg_data_files.sh | 0 test/iceberg_deletes.sh | 0 test/iceberg_dv.sh | 0 test/iceberg_fdw.sh | 0 test/iceberg_fdw_estimate.sh | 0 test/iceberg_fdw_projection.sh | 0 test/iceberg_malformed.sh | 0 test/iceberg_name_mapping.sh | 0 test/iceberg_objstore.sh | 0 test/iceberg_rest.sh | 0 test/iceberg_rest_scan.sh | 0 test/iceberg_rest_server.py | 0 test/iceberg_rest_server.sh | 0 test/iceberg_rest_vended.sh | 0 test/iceberg_scan.sh | 0 test/index_delete_liveness.sh | 0 test/index_only.sh | 0 test/listing_corpus.py | 0 test/native_agg.sh | 0 test/native_batch_fold_projection.sh | 0 test/native_bloom.sh | 0 test/native_decode_gating.sh | 0 test/native_dict_underfill.sh | 0 test/native_dml.sh | 0 test/native_encoding.sh | 0 test/native_fetch_bigcap.sh | 0 test/native_fold_deferral.sh | 0 test/native_fold_skipguard.sh | 0 test/native_gap.sh | 0 test/native_groupagg.sh | 0 test/native_groupagg_wide_cost.sh | 0 test/native_index.sh | 0 test/native_index_fetch_stripe_cost.sh | 0 test/native_ios.sh | 0 test/native_late_materialization.sh | 0 test/native_ownership.sh | 0 test/native_parquet_dict_oob.sh | 0 test/native_parquet_fieldid.sh | 0 test/native_projection.sh | 0 test/native_reclaim_cycles.sh | 0 test/native_reclaim_frag.sh | 0 test/native_reclaim_reconcile.sh | 0 test/native_roundtrip.sh | 0 test/native_scale.sh | 0 test/native_skip.sh | 0 test/native_sort_by.sh | 0 test/native_truncate.sh | 0 test/native_upgrade_converge.sh | 0 test/native_vacuum_race.sh | 0 test/native_vecdecode.sh | 0 test/native_vecskip.sh | 0 test/native_writer.sh | 0 test/native_zonemap.sh | 0 test/objstore_addressing.sh | 0 test/objstore_allowlist.sh | 0 test/objstore_credentials.sh | 0 test/objstore_crlf.sh | 0 test/objstore_http_read.sh | 0 test/objstore_http_server.py | 0 test/objstore_listing.sh | 0 test/objstore_s3_read.sh | 0 test/objstore_sink_write.sh | 0 test/objstore_tls_read.sh | 0 test/parallel.sh | 0 test/parallel_export_parquet.sh | 0 test/parallel_vector_agg.sh | 0 test/parquet_export.sh | 0 test/parquet_nested_import.sh | 0 test/phase6.sh | 0 test/portlib.sh | 0 test/read_stream.sh | 0 test/run_all_versions.sh | 0 test/run_san.sh | 0 test/scan_direction.sh | 0 .../300-a-test-script-must-be-runnable.sh | 162 ++++++++++++++++++ test/server_file_privilege.sh | 0 test/sorted_projection.sh | 0 test/temporal.sh | 0 test/ungrouped_vector_agg.sh | 0 test/update_conc.sh | 0 test/zonemap_cost.sh | 0 test/zonemap_estimate_sample.sh | 0 107 files changed, 225 insertions(+) mode change 100644 => 100755 test/arrow_corpus.py mode change 100644 => 100755 test/arrow_export.sh mode change 100644 => 100755 test/arrow_import.sh mode change 100644 => 100755 test/arrow_mutate.py mode change 100644 => 100755 test/avro_corpus.py mode change 100644 => 100755 test/avro_manifest.sh mode change 100644 => 100755 test/bloom_sizing.sh mode change 100644 => 100755 test/build_san.sh mode change 100644 => 100755 test/catalog_natts.sh mode change 100644 => 100755 test/concurrent_diff.sh mode change 100644 => 100755 test/corruption.sh mode change 100644 => 100755 test/craft_parquet_counts.py mode change 100644 => 100755 test/crlf_listener.py mode change 100644 => 100755 test/entry_point_privilege.sh mode change 100644 => 100755 test/export_sink.sh mode change 100644 => 100755 test/fsst_verdict_cache.sh mode change 100644 => 100755 test/fuzz_arrow.sh mode change 100644 => 100755 test/fuzz_avro.sh mode change 100644 => 100755 test/fuzz_listing.sh mode change 100644 => 100755 test/generated_columns.sh mode change 100644 => 100755 test/hardening.sh mode change 100644 => 100755 test/iceberg_catalog.sh mode change 100644 => 100755 test/iceberg_data_files.sh mode change 100644 => 100755 test/iceberg_deletes.sh mode change 100644 => 100755 test/iceberg_dv.sh mode change 100644 => 100755 test/iceberg_fdw.sh mode change 100644 => 100755 test/iceberg_fdw_estimate.sh mode change 100644 => 100755 test/iceberg_fdw_projection.sh mode change 100644 => 100755 test/iceberg_malformed.sh mode change 100644 => 100755 test/iceberg_name_mapping.sh mode change 100644 => 100755 test/iceberg_objstore.sh mode change 100644 => 100755 test/iceberg_rest.sh mode change 100644 => 100755 test/iceberg_rest_scan.sh mode change 100644 => 100755 test/iceberg_rest_server.py mode change 100644 => 100755 test/iceberg_rest_server.sh mode change 100644 => 100755 test/iceberg_rest_vended.sh mode change 100644 => 100755 test/iceberg_scan.sh mode change 100644 => 100755 test/index_delete_liveness.sh mode change 100644 => 100755 test/index_only.sh mode change 100644 => 100755 test/listing_corpus.py mode change 100644 => 100755 test/native_agg.sh mode change 100644 => 100755 test/native_batch_fold_projection.sh mode change 100644 => 100755 test/native_bloom.sh mode change 100644 => 100755 test/native_decode_gating.sh mode change 100644 => 100755 test/native_dict_underfill.sh mode change 100644 => 100755 test/native_dml.sh mode change 100644 => 100755 test/native_encoding.sh mode change 100644 => 100755 test/native_fetch_bigcap.sh mode change 100644 => 100755 test/native_fold_deferral.sh mode change 100644 => 100755 test/native_fold_skipguard.sh mode change 100644 => 100755 test/native_gap.sh mode change 100644 => 100755 test/native_groupagg.sh mode change 100644 => 100755 test/native_groupagg_wide_cost.sh mode change 100644 => 100755 test/native_index.sh mode change 100644 => 100755 test/native_index_fetch_stripe_cost.sh mode change 100644 => 100755 test/native_ios.sh mode change 100644 => 100755 test/native_late_materialization.sh mode change 100644 => 100755 test/native_ownership.sh mode change 100644 => 100755 test/native_parquet_dict_oob.sh mode change 100644 => 100755 test/native_parquet_fieldid.sh mode change 100644 => 100755 test/native_projection.sh mode change 100644 => 100755 test/native_reclaim_cycles.sh mode change 100644 => 100755 test/native_reclaim_frag.sh mode change 100644 => 100755 test/native_reclaim_reconcile.sh mode change 100644 => 100755 test/native_roundtrip.sh mode change 100644 => 100755 test/native_scale.sh mode change 100644 => 100755 test/native_skip.sh mode change 100644 => 100755 test/native_sort_by.sh mode change 100644 => 100755 test/native_truncate.sh mode change 100644 => 100755 test/native_upgrade_converge.sh mode change 100644 => 100755 test/native_vacuum_race.sh mode change 100644 => 100755 test/native_vecdecode.sh mode change 100644 => 100755 test/native_vecskip.sh mode change 100644 => 100755 test/native_writer.sh mode change 100644 => 100755 test/native_zonemap.sh mode change 100644 => 100755 test/objstore_addressing.sh mode change 100644 => 100755 test/objstore_allowlist.sh mode change 100644 => 100755 test/objstore_credentials.sh mode change 100644 => 100755 test/objstore_crlf.sh mode change 100644 => 100755 test/objstore_http_read.sh mode change 100644 => 100755 test/objstore_http_server.py mode change 100644 => 100755 test/objstore_listing.sh mode change 100644 => 100755 test/objstore_s3_read.sh mode change 100644 => 100755 test/objstore_sink_write.sh mode change 100644 => 100755 test/objstore_tls_read.sh mode change 100644 => 100755 test/parallel.sh mode change 100644 => 100755 test/parallel_export_parquet.sh mode change 100644 => 100755 test/parallel_vector_agg.sh mode change 100644 => 100755 test/parquet_export.sh mode change 100644 => 100755 test/parquet_nested_import.sh mode change 100644 => 100755 test/phase6.sh mode change 100644 => 100755 test/portlib.sh mode change 100644 => 100755 test/read_stream.sh mode change 100644 => 100755 test/run_all_versions.sh mode change 100644 => 100755 test/run_san.sh mode change 100644 => 100755 test/scan_direction.sh create mode 100644 test/selftest/300-a-test-script-must-be-runnable.sh mode change 100644 => 100755 test/server_file_privilege.sh mode change 100644 => 100755 test/sorted_projection.sh mode change 100644 => 100755 test/temporal.sh mode change 100644 => 100755 test/ungrouped_vector_agg.sh mode change 100644 => 100755 test/update_conc.sh mode change 100644 => 100755 test/zonemap_cost.sh mode change 100644 => 100755 test/zonemap_estimate_sample.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index dae0eba0..a70f9054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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. 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 @@ -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`. diff --git a/CONTEXT.md b/CONTEXT.md index 1e8e9c2a..ab190d53 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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/.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 diff --git a/docs/testing.md b/docs/testing.md index 29704e23..3d66bc6f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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 diff --git a/test/arrow_corpus.py b/test/arrow_corpus.py old mode 100644 new mode 100755 diff --git a/test/arrow_export.sh b/test/arrow_export.sh old mode 100644 new mode 100755 diff --git a/test/arrow_import.sh b/test/arrow_import.sh old mode 100644 new mode 100755 diff --git a/test/arrow_mutate.py b/test/arrow_mutate.py old mode 100644 new mode 100755 diff --git a/test/avro_corpus.py b/test/avro_corpus.py old mode 100644 new mode 100755 diff --git a/test/avro_manifest.sh b/test/avro_manifest.sh old mode 100644 new mode 100755 diff --git a/test/bloom_sizing.sh b/test/bloom_sizing.sh old mode 100644 new mode 100755 diff --git a/test/build_san.sh b/test/build_san.sh old mode 100644 new mode 100755 diff --git a/test/catalog_natts.sh b/test/catalog_natts.sh old mode 100644 new mode 100755 diff --git a/test/concurrent_diff.sh b/test/concurrent_diff.sh old mode 100644 new mode 100755 diff --git a/test/corruption.sh b/test/corruption.sh old mode 100644 new mode 100755 diff --git a/test/craft_parquet_counts.py b/test/craft_parquet_counts.py old mode 100644 new mode 100755 diff --git a/test/crlf_listener.py b/test/crlf_listener.py old mode 100644 new mode 100755 index 249a11e3..3e4c1512 --- a/test/crlf_listener.py +++ b/test/crlf_listener.py @@ -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. diff --git a/test/entry_point_privilege.sh b/test/entry_point_privilege.sh old mode 100644 new mode 100755 diff --git a/test/export_sink.sh b/test/export_sink.sh old mode 100644 new mode 100755 diff --git a/test/fsst_verdict_cache.sh b/test/fsst_verdict_cache.sh old mode 100644 new mode 100755 diff --git a/test/fuzz_arrow.sh b/test/fuzz_arrow.sh old mode 100644 new mode 100755 diff --git a/test/fuzz_avro.sh b/test/fuzz_avro.sh old mode 100644 new mode 100755 diff --git a/test/fuzz_listing.sh b/test/fuzz_listing.sh old mode 100644 new mode 100755 diff --git a/test/generated_columns.sh b/test/generated_columns.sh old mode 100644 new mode 100755 diff --git a/test/hardening.sh b/test/hardening.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_catalog.sh b/test/iceberg_catalog.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_data_files.sh b/test/iceberg_data_files.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_deletes.sh b/test/iceberg_deletes.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_dv.sh b/test/iceberg_dv.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_fdw.sh b/test/iceberg_fdw.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_fdw_estimate.sh b/test/iceberg_fdw_estimate.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_fdw_projection.sh b/test/iceberg_fdw_projection.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_malformed.sh b/test/iceberg_malformed.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_name_mapping.sh b/test/iceberg_name_mapping.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_objstore.sh b/test/iceberg_objstore.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_rest.sh b/test/iceberg_rest.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_rest_scan.sh b/test/iceberg_rest_scan.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_rest_server.py b/test/iceberg_rest_server.py old mode 100644 new mode 100755 diff --git a/test/iceberg_rest_server.sh b/test/iceberg_rest_server.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_rest_vended.sh b/test/iceberg_rest_vended.sh old mode 100644 new mode 100755 diff --git a/test/iceberg_scan.sh b/test/iceberg_scan.sh old mode 100644 new mode 100755 diff --git a/test/index_delete_liveness.sh b/test/index_delete_liveness.sh old mode 100644 new mode 100755 diff --git a/test/index_only.sh b/test/index_only.sh old mode 100644 new mode 100755 diff --git a/test/listing_corpus.py b/test/listing_corpus.py old mode 100644 new mode 100755 diff --git a/test/native_agg.sh b/test/native_agg.sh old mode 100644 new mode 100755 diff --git a/test/native_batch_fold_projection.sh b/test/native_batch_fold_projection.sh old mode 100644 new mode 100755 diff --git a/test/native_bloom.sh b/test/native_bloom.sh old mode 100644 new mode 100755 diff --git a/test/native_decode_gating.sh b/test/native_decode_gating.sh old mode 100644 new mode 100755 diff --git a/test/native_dict_underfill.sh b/test/native_dict_underfill.sh old mode 100644 new mode 100755 diff --git a/test/native_dml.sh b/test/native_dml.sh old mode 100644 new mode 100755 diff --git a/test/native_encoding.sh b/test/native_encoding.sh old mode 100644 new mode 100755 diff --git a/test/native_fetch_bigcap.sh b/test/native_fetch_bigcap.sh old mode 100644 new mode 100755 diff --git a/test/native_fold_deferral.sh b/test/native_fold_deferral.sh old mode 100644 new mode 100755 diff --git a/test/native_fold_skipguard.sh b/test/native_fold_skipguard.sh old mode 100644 new mode 100755 diff --git a/test/native_gap.sh b/test/native_gap.sh old mode 100644 new mode 100755 diff --git a/test/native_groupagg.sh b/test/native_groupagg.sh old mode 100644 new mode 100755 diff --git a/test/native_groupagg_wide_cost.sh b/test/native_groupagg_wide_cost.sh old mode 100644 new mode 100755 diff --git a/test/native_index.sh b/test/native_index.sh old mode 100644 new mode 100755 diff --git a/test/native_index_fetch_stripe_cost.sh b/test/native_index_fetch_stripe_cost.sh old mode 100644 new mode 100755 diff --git a/test/native_ios.sh b/test/native_ios.sh old mode 100644 new mode 100755 diff --git a/test/native_late_materialization.sh b/test/native_late_materialization.sh old mode 100644 new mode 100755 diff --git a/test/native_ownership.sh b/test/native_ownership.sh old mode 100644 new mode 100755 diff --git a/test/native_parquet_dict_oob.sh b/test/native_parquet_dict_oob.sh old mode 100644 new mode 100755 diff --git a/test/native_parquet_fieldid.sh b/test/native_parquet_fieldid.sh old mode 100644 new mode 100755 diff --git a/test/native_projection.sh b/test/native_projection.sh old mode 100644 new mode 100755 diff --git a/test/native_reclaim_cycles.sh b/test/native_reclaim_cycles.sh old mode 100644 new mode 100755 diff --git a/test/native_reclaim_frag.sh b/test/native_reclaim_frag.sh old mode 100644 new mode 100755 diff --git a/test/native_reclaim_reconcile.sh b/test/native_reclaim_reconcile.sh old mode 100644 new mode 100755 diff --git a/test/native_roundtrip.sh b/test/native_roundtrip.sh old mode 100644 new mode 100755 diff --git a/test/native_scale.sh b/test/native_scale.sh old mode 100644 new mode 100755 diff --git a/test/native_skip.sh b/test/native_skip.sh old mode 100644 new mode 100755 diff --git a/test/native_sort_by.sh b/test/native_sort_by.sh old mode 100644 new mode 100755 diff --git a/test/native_truncate.sh b/test/native_truncate.sh old mode 100644 new mode 100755 diff --git a/test/native_upgrade_converge.sh b/test/native_upgrade_converge.sh old mode 100644 new mode 100755 diff --git a/test/native_vacuum_race.sh b/test/native_vacuum_race.sh old mode 100644 new mode 100755 diff --git a/test/native_vecdecode.sh b/test/native_vecdecode.sh old mode 100644 new mode 100755 diff --git a/test/native_vecskip.sh b/test/native_vecskip.sh old mode 100644 new mode 100755 diff --git a/test/native_writer.sh b/test/native_writer.sh old mode 100644 new mode 100755 diff --git a/test/native_zonemap.sh b/test/native_zonemap.sh old mode 100644 new mode 100755 diff --git a/test/objstore_addressing.sh b/test/objstore_addressing.sh old mode 100644 new mode 100755 diff --git a/test/objstore_allowlist.sh b/test/objstore_allowlist.sh old mode 100644 new mode 100755 diff --git a/test/objstore_credentials.sh b/test/objstore_credentials.sh old mode 100644 new mode 100755 diff --git a/test/objstore_crlf.sh b/test/objstore_crlf.sh old mode 100644 new mode 100755 diff --git a/test/objstore_http_read.sh b/test/objstore_http_read.sh old mode 100644 new mode 100755 diff --git a/test/objstore_http_server.py b/test/objstore_http_server.py old mode 100644 new mode 100755 diff --git a/test/objstore_listing.sh b/test/objstore_listing.sh old mode 100644 new mode 100755 diff --git a/test/objstore_s3_read.sh b/test/objstore_s3_read.sh old mode 100644 new mode 100755 diff --git a/test/objstore_sink_write.sh b/test/objstore_sink_write.sh old mode 100644 new mode 100755 diff --git a/test/objstore_tls_read.sh b/test/objstore_tls_read.sh old mode 100644 new mode 100755 diff --git a/test/parallel.sh b/test/parallel.sh old mode 100644 new mode 100755 diff --git a/test/parallel_export_parquet.sh b/test/parallel_export_parquet.sh old mode 100644 new mode 100755 diff --git a/test/parallel_vector_agg.sh b/test/parallel_vector_agg.sh old mode 100644 new mode 100755 diff --git a/test/parquet_export.sh b/test/parquet_export.sh old mode 100644 new mode 100755 diff --git a/test/parquet_nested_import.sh b/test/parquet_nested_import.sh old mode 100644 new mode 100755 diff --git a/test/phase6.sh b/test/phase6.sh old mode 100644 new mode 100755 diff --git a/test/portlib.sh b/test/portlib.sh old mode 100644 new mode 100755 diff --git a/test/read_stream.sh b/test/read_stream.sh old mode 100644 new mode 100755 diff --git a/test/run_all_versions.sh b/test/run_all_versions.sh old mode 100644 new mode 100755 diff --git a/test/run_san.sh b/test/run_san.sh old mode 100644 new mode 100755 diff --git a/test/scan_direction.sh b/test/scan_direction.sh old mode 100644 new mode 100755 diff --git a/test/selftest/300-a-test-script-must-be-runnable.sh b/test/selftest/300-a-test-script-must-be-runnable.sh new file mode 100644 index 00000000..24caa3fc --- /dev/null +++ b/test/selftest/300-a-test-script-must-be-runnable.sh @@ -0,0 +1,162 @@ +# ---- 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 +# 260 top-level scripts were mode 100644 when this was written, so 30 of those +# documented invocations died with `Permission denied` before running a single +# statement. +# +# 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. test/selftest/ (these +# parts) and test/fixtures/ are SOURCED or imported, never executed: +# harness_selftest.sh:53-56 sources every part, and a suite invokes a fixture +# generator as `python3 test/fixtures/.../gen_x.py`. Giving those the bit would +# advertise a way to run them that does not work, which is this defect pointing +# the other way. 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")" "[]" diff --git a/test/server_file_privilege.sh b/test/server_file_privilege.sh old mode 100644 new mode 100755 diff --git a/test/sorted_projection.sh b/test/sorted_projection.sh old mode 100644 new mode 100755 diff --git a/test/temporal.sh b/test/temporal.sh old mode 100644 new mode 100755 diff --git a/test/ungrouped_vector_agg.sh b/test/ungrouped_vector_agg.sh old mode 100644 new mode 100755 diff --git a/test/update_conc.sh b/test/update_conc.sh old mode 100644 new mode 100755 diff --git a/test/zonemap_cost.sh b/test/zonemap_cost.sh old mode 100644 new mode 100755 diff --git a/test/zonemap_estimate_sample.sh b/test/zonemap_estimate_sample.sh old mode 100644 new mode 100755 From fb6409a7a5fe59d12c1297992acf55ecf428186a Mon Sep 17 00:00:00 2001 From: OffgridwithJD Date: Mon, 31 Aug 2026 18:56:53 +0000 Subject: [PATCH 2/3] docs: the test/fixtures/ exemption had the wrong reason (#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 #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) Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9 --- .../300-a-test-script-must-be-runnable.sh | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/test/selftest/300-a-test-script-must-be-runnable.sh b/test/selftest/300-a-test-script-must-be-runnable.sh index 24caa3fc..d5942087 100644 --- a/test/selftest/300-a-test-script-must-be-runnable.sh +++ b/test/selftest/300-a-test-script-must-be-runnable.sh @@ -34,12 +34,37 @@ # 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. test/selftest/ (these -# parts) and test/fixtures/ are SOURCED or imported, never executed: -# harness_selftest.sh:53-56 sources every part, and a suite invokes a fixture -# generator as `python3 test/fixtures/.../gen_x.py`. Giving those the bit would -# advertise a way to run them that does not work, which is this defect pointing -# the other way. Every OTHER directory under test/ is swept, so a runnable entry +# 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 From 943f92cd0671db00699ed96dddaa8763ecd2de71 Mon Sep 17 00:00:00 2001 From: OffgridwithJD Date: Mon, 31 Aug 2026 19:30:10 +0000 Subject: [PATCH 3/3] docs: the rebase moved the denominator, so re-measure it (#852) #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 0e4884c1. 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) Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9 --- CHANGELOG.md | 2 +- test/selftest/300-a-test-script-must-be-runnable.sh | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a70f9054..d8ad9d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ true until the next version shipped. written (#852). No released version is affected: nothing in the extension changed, and this is test tooling only. - 103 of the 260 top-level scripts in `test/` were mode 100644, and 30 documented + 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. diff --git a/test/selftest/300-a-test-script-must-be-runnable.sh b/test/selftest/300-a-test-script-must-be-runnable.sh index d5942087..dfb9420f 100644 --- a/test/selftest/300-a-test-script-must-be-runnable.sh +++ b/test/selftest/300-a-test-script-must-be-runnable.sh @@ -3,9 +3,14 @@ # 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 -# 260 top-level scripts were mode 100644 when this was written, so 30 of those -# documented invocations died with `Permission denied` before running a single -# statement. +# 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