diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index 6b99a8467..31b254092 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -2282,6 +2282,8 @@ parameter is `cur` is excluded for the same reason `wrote` is. | `test_a_hardcoded_sentinel_survives_the_same_rewrite` | a sentinel no producer minted — the corpus writes three, one from inside SQL — is still refused after a rewrite | a matcher reading a rewritable global would stop seeing them | | `test_the_ordering_premise_refuses_a_failed_reading` | `ordering_observable` refuses a failed reading on either side | with the old shared constant two failed readings were identical and it went RED; unique sentinels differ, so it passed and greenlit every ordered assertion resting on it | | `test_a_legitimate_comparison_is_untouched` | equal text, rows, sets and numbers still pass | a refusal that also refuses real data is not a refusal | +| `test_the_minted_failure_scan_sees_the_shape` | the scan finds an except handler returning `f'error: {exc}'`, and not `query_error()`, a SQLSTATE, or the shape inside a string literal | a scan that sees nothing would make the corpus arm below vacuous | +| `test_no_test_mints_its_own_failure_text` | no except handler under test/pytest builds an "error..." string from the error's message (#1525 review) | a helper that mints its own failure text is not the producer's prefix, so no refusal fires and two failures with the same message compare equal; on main it finds 32 sites in 22 files | **Each refusal is proved load-bearing.** Removing the one call from each assertion, one at a time, with `__pycache__` cleared between runs: diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index e4e07f46d..e8f6754e4 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -292,7 +292,10 @@ # in test_compare_to_bash.py: one test, by collection over NO_CLUSTER. # guard_tests 439 -> 440 for #1339's test_a_premise_in_name_only_is_still_caught in # test_loop_coverage_premise.py: one test, by collection over NO_CLUSTER. -guard_tests 440 +# guard_tests 440 -> 442 for #1339's test_the_minted_failure_scan_sees_the_shape and +# test_no_test_mints_its_own_failure_text in test_failed_query_sentinel.py: two tests, +# by collection over NO_CLUSTER. +guard_tests 442 # The complement: tests that need the driver and a throwaway cluster. Until #1016 these ran # in no CI job at all -- a quarter of the corpus, green when somebody ran them by hand and diff --git a/test/pytest/test_advisory_lock_class.py b/test/pytest/test_advisory_lock_class.py index bf3dd3ff9..a8da34a51 100644 --- a/test/pytest/test_advisory_lock_class.py +++ b/test/pytest/test_advisory_lock_class.py @@ -30,6 +30,8 @@ """ import psycopg +from pgc_vacuity import query_error + # The two field4 values an application can produce, from lockfuncs.c. SQL_REACHABLE = (1, 2) PROBE_KEY = 900001 @@ -213,7 +215,7 @@ def test_a_user_advisory_lock_on_that_tag_does_not_block_an_insert( except psycopg.errors.QueryCanceled: verdict = "BLOCKED by the user lock" except psycopg.Error as exc: - verdict = f"ERROR: {str(exc).splitlines()[0]}" + verdict = query_error(str(exc).splitlines()[0]) finally: with pgc_conn.cursor() as cur: cur.execute("RESET statement_timeout") @@ -366,7 +368,7 @@ def test_the_row_identity_lock_cannot_be_reached_from_sql(pgc_cluster, pgc_conn, except psycopg.errors.QueryCanceled: verdict = "BLOCKED by the user lock" except psycopg.Error as exc: - verdict = f"ERROR: {str(exc).splitlines()[0]}" + verdict = query_error(str(exc).splitlines()[0]) finally: with pgc_conn.cursor() as cur: cur.execute("RESET statement_timeout") @@ -407,7 +409,7 @@ def write_second(): sc.execute("INSERT INTO bw VALUES (2)") result["verdict"] = "ok" except psycopg.Error as exc: - result["verdict"] = f"ERROR: {str(exc).splitlines()[0]}" + result["verdict"] = query_error(str(exc).splitlines()[0]) import threading import time diff --git a/test/pytest/test_failed_query_sentinel.py b/test/pytest/test_failed_query_sentinel.py index f00da4f3c..eb59c17e1 100644 --- a/test/pytest/test_failed_query_sentinel.py +++ b/test/pytest/test_failed_query_sentinel.py @@ -23,7 +23,9 @@ accepts one at all; it is NOT safe against a helper that compares by hand, which is why the producer exists and why new code should use it. """ +import ast import inspect +import pathlib import pgc_vacuity from pgc_vacuity import QUERY_ERROR, VacuityError, query_error @@ -398,3 +400,65 @@ def test_a_legitimate_comparison_is_untouched(expect): e.row_set([(1,), (2,)], [(2,), (1,)], "a set comparison still ignores order") e.num(7, 7, "equal numbers still pass") expect.num(1, 1, "four honest comparisons passed through the new refusal") + + +# A HELPER THAT MINTS ITS OWN FAILURE TEXT REOPENS THE MODE (#1525 review). Seventeen +# twins caught a failed query and returned f"error: {message}": not the producer's +# prefix, so no refusal fires, and two failures with the same message compare equal. +# Measured on test_native_skip.py's copy: with the backend aborting on one LIKE, the +# check on that query PASSED, as did two more read on the dead connection afterwards, +# and the run named no red at all. +# +# Parsed, not grepped, for the reason the layer's other scans give: a string literal +# that merely contains the shape is not an except handler. +def _minted_failure_text(source): + """-> line numbers where an except handler builds a string starting "error" from + the error's message. A SQLSTATE alone is a typed field, which a test compares + with the code it expects (test_rls_query_paths.py's "ERROR: 0A000"), so an + f-string whose every interpolation is a `.sqlstate` is not this shape.""" + def starts_error(node): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value.lower().startswith("error") + if isinstance(node, ast.JoinedStr) and node.values: + return starts_error(node.values[0]) and not all( + isinstance(v.value, ast.Attribute) and v.value.attr == "sqlstate" + for v in node.values if isinstance(v, ast.FormattedValue)) + return False + tree = ast.parse(source) + # an f-string's literal parts are judged with the f-string, not alone + parts = {id(v) for j in ast.walk(tree) if isinstance(j, ast.JoinedStr) for v in j.values} + hits = [] + for h in ast.walk(tree): + if isinstance(h, ast.ExceptHandler): + hits += [n.lineno for stmt in h.body for n in ast.walk(stmt) + if id(n) not in parts and starts_error(n)] + return sorted(set(hits)) + + +def test_the_minted_failure_scan_sees_the_shape(expect): + """The scan's own control: it finds the shape, and not the producer's form.""" + bad = ("def f(c):\n try:\n return c()\n" + " except Exception as exc:\n return f'error: {exc}'\n") + good = bad.replace("f'error: {exc}'", "query_error(str(exc))") + code = bad.replace("f'error: {exc}'", "f'error {exc.sqlstate}'") + in_a_string = "SRC = " + repr(bad) + "\n" + expect.text(" ".join(str(_minted_failure_text(x)) for x in (bad, good, code, in_a_string)), + "[5] [] [] []", "the scan finds a minted failure string, and not " + "query_error(), a SQLSTATE, or the shape inside a literal") + + +def test_no_test_mints_its_own_failure_text(expect): + """A failed query becomes a value through query_error(), so it is refused, never + compared. The corpus is every test file and helper under test/pytest/. + + The scan keys on the spelling the tree used, a handler value starting "error" + (any case, f-string, `+`, `.format` or `%`). A handler that builds other text from + the exception (`f"failed: {exc}"`, a bare `str(exc)`) is not seen: most such + handlers are deliberate refusal capture, compared with a fixed message or + SQLSTATE, and the mode is only live when that text reaches a two-sided + comparison (#1527 review).""" + here = pathlib.Path(__file__).resolve().parent + hits = [f"{p.name}:{n}" for p in sorted(here.glob("*.py")) + for n in _minted_failure_text(p.read_text())] + expect.text(" ".join(hits) or "none", "none", + "no except handler turns a failed query into its own error text") diff --git a/test/pytest/test_latest_reading.py b/test/pytest/test_latest_reading.py index 6b87bb4f1..9f5e3a293 100644 --- a/test/pytest/test_latest_reading.py +++ b/test/pytest/test_latest_reading.py @@ -25,6 +25,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + DOC = pathlib.Path(__file__).resolve().parent.parent.parent / "docs" / "how-to.md" SECTION = "## Find the latest reading per device" DEVICES = 25 @@ -74,7 +76,7 @@ def _try(fn, *args): try: return fn(*args) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) @pytest.fixture(scope="module") diff --git a/test/pytest/test_maintenance_crash.py b/test/pytest/test_maintenance_crash.py index 5b5f42aeb..e6720daca 100644 --- a/test/pytest/test_maintenance_crash.py +++ b/test/pytest/test_maintenance_crash.py @@ -21,6 +21,8 @@ import psycopg +from pgc_vacuity import query_error + ROWS = ("SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id)) FROM mc") GROUPS = ("SELECT count(*) || '/' || coalesce(sum(row_count), 0) FROM pgcolumnar.row_group " "WHERE storage_id = pgcolumnar.get_storage_id('mc')") @@ -33,7 +35,7 @@ def _one(dsn, sql, setup=()): c.execute(s) return c.execute(sql).fetchone()[0] except psycopg.Error as exc: - return f"ERROR: {str(exc).splitlines()[0]}" + return query_error(str(exc).splitlines()[0]) BY_INDEX = ("SET enable_seqscan = off", "SET enable_bitmapscan = off", "SET pgcolumnar.enable_custom_scan = off") diff --git a/test/pytest/test_maintenance_rollback.py b/test/pytest/test_maintenance_rollback.py index 0e4b97234..8a26dcc1f 100644 --- a/test/pytest/test_maintenance_rollback.py +++ b/test/pytest/test_maintenance_rollback.py @@ -16,6 +16,8 @@ """ import psycopg +from pgc_vacuity import query_error + ROWS = ("SELECT count(*) || '/' || md5(string_agg(id || ':' || v || ':' || w, ',' ORDER BY id)) " "FROM mr") GROUPS = ("SELECT count(*) || '/' || coalesce(sum(row_count), 0) FROM pgcolumnar.row_group " @@ -29,7 +31,7 @@ def _one(conn, sql): cur.execute(sql) return cur.fetchone()[0] except psycopg.Error as exc: - return f"ERROR: {str(exc).splitlines()[0]}" + return query_error(str(exc).splitlines()[0]) BY_INDEX_SETS = ("enable_seqscan", "enable_bitmapscan", "pgcolumnar.enable_custom_scan") diff --git a/test/pytest/test_native_agg.py b/test/pytest/test_native_agg.py index 920a1ab32..cda10f826 100644 --- a/test/pytest/test_native_agg.py +++ b/test/pytest/test_native_agg.py @@ -29,6 +29,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + ROWS = 300000 @@ -48,7 +50,7 @@ def _try(fn, *args, **kw): try: return fn(*args, **kw) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _blocks(conn, table, sql): diff --git a/test/pytest/test_native_bloom.py b/test/pytest/test_native_bloom.py index 57fdaf54f..0b2b6bcdf 100644 --- a/test/pytest/test_native_bloom.py +++ b/test/pytest/test_native_bloom.py @@ -11,6 +11,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") # a well-spread tag over a large domain: each 2048-row group's [min,max] spans almost @@ -23,7 +25,7 @@ def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _skipped(c, sql, bloom): diff --git a/test/pytest/test_native_fetch_bigcap.py b/test/pytest/test_native_fetch_bigcap.py index 1ee132908..c3bf2db76 100644 --- a/test/pytest/test_native_fetch_bigcap.py +++ b/test/pytest/test_native_fetch_bigcap.py @@ -12,6 +12,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + CAP = 32 * 1024 * 1024 FORCE_INDEX = ("max_parallel_workers_per_gather=0", "enable_seqscan=off", "enable_bitmapscan=off", "pgcolumnar.enable_index_fetch_penalty=off", "pgcolumnar.enable_custom_scan=off") @@ -34,7 +36,7 @@ def _one(c, sql): try: return c.execute(sql).fetchone()[0] except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _index_scan(c, sql): diff --git a/test/pytest/test_native_fetch_position.py b/test/pytest/test_native_fetch_position.py index f3b2bece3..330facd66 100644 --- a/test/pytest/test_native_fetch_position.py +++ b/test/pytest/test_native_fetch_position.py @@ -18,6 +18,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + # the shell reads PGC_FETCHPOS_ROWS, and so does this ROWS = int(os.environ.get("PGC_FETCHPOS_ROWS", "120000")) SRC = pathlib.Path(__file__).resolve().parents[2] / "src" @@ -50,7 +52,7 @@ def _rows_or_error(c, sql): try: return _one(c, sql) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _median_ms(c, sql): diff --git a/test/pytest/test_native_gap.py b/test/pytest/test_native_gap.py index ecc2eb237..26a78ef1e 100644 --- a/test/pytest/test_native_gap.py +++ b/test/pytest/test_native_gap.py @@ -12,6 +12,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") IDX_SETS = ("SET enable_seqscan = off", "SET enable_bitmapscan = off", "SET pgcolumnar.enable_custom_scan = off") @@ -22,7 +24,7 @@ def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _size(c): diff --git a/test/pytest/test_native_groupagg.py b/test/pytest/test_native_groupagg.py index 0d7ae595c..d28cadf56 100644 --- a/test/pytest/test_native_groupagg.py +++ b/test/pytest/test_native_groupagg.py @@ -45,6 +45,8 @@ import json import psycopg +from pgc_vacuity import query_error + ROWS = 20000 MARKER = "Columnar Vectorized Group Keys" @@ -801,7 +803,7 @@ def _groups_or_error(conn, cap, sql): try: return str(len(_rows(conn, sql))) except psycopg.Error as exc: - return f"ERROR: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) finally: _gate_reset(conn) diff --git a/test/pytest/test_native_groupagg_batch.py b/test/pytest/test_native_groupagg_batch.py index 182e7884c..f5172d90f 100644 --- a/test/pytest/test_native_groupagg_batch.py +++ b/test/pytest/test_native_groupagg_batch.py @@ -15,6 +15,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + ANALYZE = "EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF)" PLAIN = "EXPLAIN (COSTS OFF)" MARKER = "Columnar Vectorized Group Keys" @@ -48,7 +50,7 @@ def _rows(c, sql): try: return sorted(tuple("\0" if v is None else str(v) for v in r) for r in c.execute(sql)) except psycopg.Error as exc: - return [(f"error: {exc.diag.message_primary}",)] + return [(query_error(exc.diag.message_primary),)] def _one(c, sql): @@ -60,7 +62,7 @@ def _text(c, sql): try: return "\n".join(str(r[0]) for r in c.execute(sql)) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _groupvec(c, sql): diff --git a/test/pytest/test_native_index.py b/test/pytest/test_native_index.py index 0364bf40e..2bf8e04d0 100644 --- a/test/pytest/test_native_index.py +++ b/test/pytest/test_native_index.py @@ -10,6 +10,8 @@ """ import psycopg +from pgc_vacuity import query_error + FORCE_INDEX = ("SET enable_seqscan=off", "SET enable_bitmapscan=off", "SET enable_indexonlyscan=off", "SET pgcolumnar.enable_custom_scan=off") @@ -27,7 +29,7 @@ def _forced(cur, sql): cur.execute(s) return _one(cur, sql) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) finally: for g in ("enable_seqscan", "enable_bitmapscan", "enable_indexonlyscan", "pgcolumnar.enable_custom_scan"): cur.execute(f"RESET {g}") diff --git a/test/pytest/test_native_ios.py b/test/pytest/test_native_ios.py index 31d186ae2..94b1648e3 100644 --- a/test/pytest/test_native_ios.py +++ b/test/pytest/test_native_ios.py @@ -14,6 +14,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") RANGE = "SELECT id FROM {} WHERE id BETWEEN 1000 AND 5000" @@ -23,7 +25,7 @@ def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _hash(c, sql): diff --git a/test/pytest/test_native_parquet_flba.py b/test/pytest/test_native_parquet_flba.py index f6206c44a..024559d92 100644 --- a/test/pytest/test_native_parquet_flba.py +++ b/test/pytest/test_native_parquet_flba.py @@ -12,6 +12,8 @@ import psycopg +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") SHAPE = "t(id int, u uuid, d numeric(12,4), dn numeric(12,4), fb bytea)" @@ -23,7 +25,7 @@ def _hash(cur, sql): cur.execute(HASH.format(sql)) return cur.fetchone()[0] except psycopg.Error as exc: - return f"error: {exc.diag.message_primary} [{sql}]" + return query_error(f"{exc.diag.message_primary} [{sql}]") def _one(cur, sql): @@ -31,7 +33,7 @@ def _one(cur, sql): cur.execute(sql) return str(cur.fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _patch_scale(path): diff --git a/test/pytest/test_native_parquet_multifile.py b/test/pytest/test_native_parquet_multifile.py index ea3babc61..271cfa120 100644 --- a/test/pytest/test_native_parquet_multifile.py +++ b/test/pytest/test_native_parquet_multifile.py @@ -16,6 +16,8 @@ import psycopg +from pgc_vacuity import query_error + COLS = "id int, v int8" HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") @@ -26,7 +28,7 @@ def _hash(cur, sql): cur.execute(HASH.format(sql)) return cur.fetchone()[0] except psycopg.Error as exc: - return f"error: {exc.diag.message_primary} [{sql}]" + return query_error(f"{exc.diag.message_primary} [{sql}]") def _one(cur, sql): @@ -34,7 +36,7 @@ def _one(cur, sql): cur.execute(sql) return str(cur.fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _errs(cur, sql): @@ -53,7 +55,7 @@ def _plan(cur, sql): cur.execute(f"EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) {sql}") return [r[0] for r in cur.fetchall()] except psycopg.Error as exc: - return [f"error: {exc.diag.message_primary}"] + return [query_error(exc.diag.message_primary)] def _counter(cur, where, label): @@ -95,7 +97,7 @@ def run(): with psycopg.connect(cluster.dsn(), autocommit=True) as c: out.append(str(c.execute(sql).fetchone()[0])) except psycopg.Error as exc: - out.append(f"error: {exc.diag.message_primary}") + out.append(query_error(exc.diag.message_primary)) t = threading.Thread(target=run, daemon=True) t.start() @@ -259,7 +261,7 @@ def part(i, path): cur.execute(f"SELECT id FROM pgcolumnar.read_parquet('{tree}') AS t({COLS}) LIMIT 3") first = " ".join(str(r[0]) for r in cur.fetchall()) + " " except psycopg.Error as exc: - first = f"error: {exc.diag.message_primary}" + first = query_error(exc.diag.message_primary) expect.text(first, "1000 1001 1002 ", "tree read order follows the sorted full path") diff --git a/test/pytest/test_native_parquet_pushdown.py b/test/pytest/test_native_parquet_pushdown.py index e78561294..285401347 100644 --- a/test/pytest/test_native_parquet_pushdown.py +++ b/test/pytest/test_native_parquet_pushdown.py @@ -14,6 +14,8 @@ import psycopg +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") ANALYZE = "EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) SELECT count(*) FROM {} WHERE {}" @@ -25,7 +27,7 @@ def _hash(cur, sql): cur.execute(HASH.format(sql)) return cur.fetchone()[0] except psycopg.Error as exc: - return f"error: {exc.diag.message_primary} [{sql}]" + return query_error(f"{exc.diag.message_primary} [{sql}]") def _counter(cur, table, where, label): diff --git a/test/pytest/test_native_roundtrip.py b/test/pytest/test_native_roundtrip.py index 6ebd44a34..7914a5cc4 100644 --- a/test/pytest/test_native_roundtrip.py +++ b/test/pytest/test_native_roundtrip.py @@ -9,6 +9,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") GEN = """SELECT g, @@ -23,7 +25,7 @@ def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) @pytest.fixture(scope="module") diff --git a/test/pytest/test_native_saop_pushdown.py b/test/pytest/test_native_saop_pushdown.py index 894867158..8cf3e3af1 100644 --- a/test/pytest/test_native_saop_pushdown.py +++ b/test/pytest/test_native_saop_pushdown.py @@ -17,6 +17,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + SRC = pathlib.Path(__file__).resolve().parents[2] / "src" / "columnar_reader.c" REMOVED = "Chunk Groups Removed by Filter" @@ -47,7 +49,7 @@ def _answer(c, sql): try: return str(_one(c, sql)) except psycopg.Error as exc: - return f"error: {exc}" + return query_error(str(exc)) def _removed(c, sql): diff --git a/test/pytest/test_native_skip.py b/test/pytest/test_native_skip.py index 3f016cd3a..ec0b5ef6e 100644 --- a/test/pytest/test_native_skip.py +++ b/test/pytest/test_native_skip.py @@ -20,13 +20,15 @@ ONE CHECK PER TEST (#1339). Module fixtures build each data set once; every reading is read-only after that, so each test runs one query pair and asserts one thing, and a red cannot stop the next check from running. Every reading goes through `_try`, so -an error is a named red rather than an unnamed test ERROR. +an error is a named red rather than an unnamed test ERROR, and each failure is its own +`query_error()`, so two failed readings never agree (#1527). """ import json import re import psycopg import pytest +from pgc_vacuity import QUERY_ERROR, query_error REMOVED = re.compile(r"Columnar Chunk Groups Removed by Filter: (\d+)") @@ -36,16 +38,15 @@ def _try(fn, *args, **kw): try: return fn(*args, **kw) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _num(v, what=""): - """A count for expect.num, or -1 with the error printed: expect.num refuses a - non-number with a VacuityError rather than a named red.""" - if isinstance(v, int): - return v - print(f"-- {what or 'reading'}: not a count: {v}") - return -1 + """A count for expect.num, or the failed reading itself, printed: expect.num refuses + it by the check's name. A -1 stand-in made two failed counts agree (#1527).""" + if not isinstance(v, int): + print(f"-- {what or 'reading'}: not a count: {v}") + return v def _rowsv(v): @@ -135,7 +136,7 @@ def tables(pgc_cluster): @pytest.mark.parametrize("col, what, pred", COERCE_RANGES) def test_heaps_range_on_a_coercible_type_holds_121_rows(tables, expect, col, what, pred): heap = _ids(tables, "skh", f"{col} {pred}") - expect.num(len(heap.split(",")) if heap and not heap.startswith("error") else 0, 121, + expect.num(len(heap.split(",")) if heap and not heap.startswith(QUERY_ERROR) else 0, 121, f"premise: heap's range on {what} holds 121 rows") @@ -204,7 +205,7 @@ def scalar(pgc_cluster): def test_the_scalar_plan_is_a_columnar_custom_scan(scalar, expect): plan = _try(lambda: scalar.execute("EXPLAIN (COSTS OFF, FORMAT JSON) " "SELECT id FROM sqn WHERE id BETWEEN 5000 AND 5100").fetchone()[0]) - if isinstance(plan, str) and plan.startswith("error"): + if isinstance(plan, str) and plan.startswith(QUERY_ERROR): plan = [{"Plan": {"Node Type": plan}}] expect.plan_marker(plan if isinstance(plan, list) else json.loads(plan), "Columnar Projected Columns", diff --git a/test/pytest/test_native_toasted_write.py b/test/pytest/test_native_toasted_write.py index b790bc4c8..dc1474461 100644 --- a/test/pytest/test_native_toasted_write.py +++ b/test/pytest/test_native_toasted_write.py @@ -12,6 +12,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + RANGE = "SELECT count(*) FROM {} WHERE big >= 'e'" BELOW = "SELECT count(*) FROM {} WHERE big < '1'" EQUAL = "SELECT count(*) FROM {} WHERE big = '{}'" @@ -22,7 +24,7 @@ def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _count(c, sql): @@ -30,7 +32,7 @@ def _count(c, sql): try: return int(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) @pytest.fixture(scope="module") diff --git a/test/pytest/test_native_truncate.py b/test/pytest/test_native_truncate.py index 9d53dc9e4..78573ffa4 100644 --- a/test/pytest/test_native_truncate.py +++ b/test/pytest/test_native_truncate.py @@ -12,6 +12,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") IDX_SETS = ("SET enable_seqscan = off", "SET enable_bitmapscan = off", "SET pgcolumnar.enable_custom_scan = off") @@ -22,7 +24,7 @@ def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _size(c): diff --git a/test/pytest/test_sorted_projection.py b/test/pytest/test_sorted_projection.py index 41ddc9c40..efdba89f8 100644 --- a/test/pytest/test_sorted_projection.py +++ b/test/pytest/test_sorted_projection.py @@ -11,6 +11,8 @@ import psycopg import pytest +from pgc_vacuity import query_error + SET_HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY t)), $e$EMPTY$e$) " "FROM (SELECT _row::text AS t FROM ({}) _row) _s") SEQ_HASH = ("SELECT coalesce(md5(string_agg(t, chr(10) ORDER BY n)), $e$EMPTY$e$) " @@ -22,14 +24,14 @@ def _hash(c, tmpl, sql): try: return c.execute(tmpl.format(sql)).fetchone()[0] except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _one(c, sql): try: return str(c.execute(sql).fetchone()[0]) except psycopg.Error as exc: - return f"error: {exc.diag.message_primary}" + return query_error(exc.diag.message_primary) def _pair(c, tmpl, hashing=SET_HASH): diff --git a/test/pytest/test_statement_self_visibility.py b/test/pytest/test_statement_self_visibility.py index 568258426..f9b47e417 100644 --- a/test/pytest/test_statement_self_visibility.py +++ b/test/pytest/test_statement_self_visibility.py @@ -14,6 +14,8 @@ """ import psycopg +from pgc_vacuity import query_error + AMS = ("pgcolumnar", "heap") @@ -44,7 +46,7 @@ def _answer(conn, sql, am): cur.execute(sql.replace("%t", f"sv_{am}")) return cur.fetchone() except psycopg.Error as exc: - return f"ERROR: {str(exc).splitlines()[0]}" + return query_error(str(exc).splitlines()[0]) def _same(conn, sql):