diff --git a/CHANGELOG.md b/CHANGELOG.md index dae0eba0..d8ad9d3a 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 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 @@ -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..dfb9420f --- /dev/null +++ b/test/selftest/300-a-test-script-must-be-runnable.sh @@ -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")" "[]" 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