From e4a0e27f0be3cc15f68fdb4423d4ff78fd582177 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Mon, 27 Jul 2026 00:11:59 +0200
Subject: [PATCH v1 09/12] POC: test_aio: port TAP test 001_aio to pytest
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The 001_aio test is one of the Perl tests that were the most unreadable
to me. Using Python for this one improves that a lot and actually
removes bugs in the tests by making those bugs impossible. It also
almost halves the number of lines of code in this test.

The Perl tests were using regex matching on the output which resulted in code
like this:

    psql_like(
        $io_method,
        $psql_a,
        "$persistency: read buffers, doesn't combine hits, block 0-1",
        qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|,
        qr/^0\|0\|f\|1\n1\|1\|f\|1$/,
        qr/^$/);

In Python the equivalent is this (read_buffers is a Python helper
function that runs the read_buffers SQL function):

    assert read_buffers(table, 0, 2) == [
        (0, 0, False, 1),
        (1, 1, False, 1),
    ], f"{table}: read buffers, doesn't combine hits, block 0-1"

psql_like() matches stdout and stderr together, so a WARNING is asserted as a
regex over psql's stderr decoration:

    # Verify that bufmgr.c IO zeroes out pages with page validity errors
    psql_like(
        $io_method,
        $psql_a,
        "$persistency: verify zero_damaged_pages=on",
        qq(
    BEGIN;
    SET LOCAL zero_damaged_pages = true;
    SELECT count(*) FROM tbl_zero;
    COMMIT;
    ),
        qr/^\d+$/,
        qr/^psql:<stdin>:\d+: WARNING:  invalid page in block 2 of relation "base\/.*\/.*$/
    );

In Python server messages arrive as Python warnings, so they can be
asserted on with pytest.warns:

    a.sql("BEGIN")
    a.sql("SET LOCAL zero_damaged_pages = true")
    with pytest.warns(PostgresWarning, match=r"invalid page in block 2 of relation"):
        a.sql("SELECT count(*) FROM tbl_zero")
    a.sql("COMMIT")

The regex matching on results also makes it very easy to make mistakes:
the three error recovery checks *try to* check for an 'ok' literal in
the output to see that a query passed. However, it uses `^|ok$` as a
regex instead of `^\|ok$`. That small typo makes it so that any output
matches because the output should match either `^` or `ok$` and that
first regex matches any string:

    psql_like(
        $io_method,
        $psql,
        "handle error recovery in implicit xact",
        qq(SELECT handle_get_and_error(); SELECT 'ok', handle_get_release()),
        qr/^|ok$/,
        qr/ERROR.*as you command/);

In Python the equivalent needs no marker column at all to test that the
query succeeds:

    with pytest.raises(LibpqError, match="as you command"):
        node.sql("SELECT handle_get_and_error()")
    node.sql("SELECT handle_get_release()")

Test runtime changes in CI:

    platform    perl             pytest           diff
    ----------- ---------------- ---------------- ----------------
    mingw        10.6s (±0.1)      4.4s (±0.1)      -6.3s (-59%)
    windows      10.9s (±0.2)      5.6s (±0.2)      -5.3s (-49%)
    linux-64      7.8s (±0.0)      5.8s (±0.1)      -2.1s (-26%)
    macos         4.8s (±0.5)      3.8s (±0.7)      -1.0s (-21%)
    linux-32      3.4s (±0.0)      2.7s (±0.1)      -0.8s (-23%)

Timings are means of 5 runs of each form, interleaved on one CI runner with
nothing else running on it; ± is the standard deviation across the 5 runs.

LOC (no comments or blanks, with tokei): 1469 -> 819 (-44%).
---
 src/test/modules/test_aio/Makefile            |    1 +
 src/test/modules/test_aio/meson.build         |    6 +-
 src/test/modules/test_aio/pyt/test_001_aio.py | 1138 ++++++++++
 src/test/modules/test_aio/t/001_aio.pl        | 1902 -----------------
 4 files changed, 1144 insertions(+), 1903 deletions(-)
 create mode 100644 src/test/modules/test_aio/pyt/test_001_aio.py
 delete mode 100644 src/test/modules/test_aio/t/001_aio.pl

diff --git a/src/test/modules/test_aio/Makefile b/src/test/modules/test_aio/Makefile
index f53cc64671a..5b4df369932 100644
--- a/src/test/modules/test_aio/Makefile
+++ b/src/test/modules/test_aio/Makefile
@@ -11,6 +11,7 @@ EXTENSION = test_aio
 DATA = test_aio--1.0.sql
 
 TAP_TESTS = 1
+PYTEST_TESTS = 1
 
 export enable_injection_points
 
diff --git a/src/test/modules/test_aio/meson.build b/src/test/modules/test_aio/meson.build
index 909f81d96c1..2f9fdd7a72c 100644
--- a/src/test/modules/test_aio/meson.build
+++ b/src/test/modules/test_aio/meson.build
@@ -25,12 +25,16 @@ tests += {
   'name': 'test_aio',
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
+  'pytest': {
+    'tests': [
+      'pyt/test_001_aio.py',
+    ],
+  },
   'tap': {
     'env': {
        'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
     },
     'tests': [
-      't/001_aio.pl',
       't/002_io_workers.pl',
       't/003_initdb.pl',
       't/004_read_stream.pl',
diff --git a/src/test/modules/test_aio/pyt/test_001_aio.py b/src/test/modules/test_aio/pyt/test_001_aio.py
new file mode 100644
index 00000000000..b36730c2efe
--- /dev/null
+++ b/src/test/modules/test_aio/pyt/test_001_aio.py
@@ -0,0 +1,1138 @@
+# Copyright (c) 2025-2026, PostgreSQL Global Development Group
+
+"""Port of src/test/modules/test_aio/t/001_aio.pl.
+
+Exercises the AIO subsystem through the test_aio extension, once per supported
+io_method. Where the Perl test matches psql stderr (WARNING/ERROR text) with
+`psql_like`, this uses captured server messages (``pytest.warns`` /
+``PostgresWarning``/``PostgresNotice``) and ``pytest.raises`` instead, and
+asserts on real result values rather than psql's text output.
+"""
+
+import contextlib
+import re
+import warnings
+
+import pytest
+from libpq import LibpqError, PostgresMessage, PostgresWarning
+from pypg import skip_unless_injection_points, wait_until
+from pypg.bins import postgres
+
+IO_METHODS = ["worker", "io_uring", "sync"]
+
+CONFIGURE = {
+    "shared_preload_libraries": "test_aio",
+    "log_min_messages": "DEBUG3",
+    "log_statement": "all",
+    "log_error_verbosity": "default",
+    "restart_after_crash": False,
+    "temp_buffers": 100,
+}
+
+
+def supported_io_methods():
+    # Probe the valid io_method values from the error message for an invalid
+    # one. -C avoids the superuser check (needed when running as administrator
+    # on Windows).
+    r = postgres.check_all("-C", "invalid", "-c", "io_method=invalid", exit_code=1)
+    m = re.search(r"Available values: ([^.]+)\.", r.stderr)
+    assert m, f"can't determine supported io_method values: {r.stderr}"
+    return m.group(1)
+
+
+@pytest.fixture(scope="module", params=IO_METHODS)
+def node(request, create_pg_module):
+    # One configured node per io_method, shared across this module's sub-tests
+    # (mirrors the Perl test_io_method, which reuses a single node per method).
+    method = request.param
+    if method not in supported_io_methods():
+        pytest.skip(f"io_method {method} not supported by this build")
+
+    conf = {**CONFIGURE, "io_method": method}
+    if method == "sync":
+        conf["io_max_concurrency"] = 4
+    node = create_pg_module(f"aio_{method}", conf=conf)
+
+    assert node.sql("SHOW io_method") == method, "io_method set correctly"
+
+    node.sql("CREATE EXTENSION test_aio")
+    node.sql(
+        "CREATE TABLE tbl_corr(data int not null) WITH (AUTOVACUUM_ENABLED = false)"
+    )
+    node.sql("CREATE TABLE tbl_ok(data int not null) WITH (AUTOVACUUM_ENABLED = false)")
+    node.sql("INSERT INTO tbl_corr SELECT generate_series(1, 10000)")
+    node.sql("INSERT INTO tbl_ok SELECT generate_series(1, 10000)")
+    node.sql("SELECT grow_rel('tbl_corr', 16)")
+    node.sql("SELECT grow_rel('tbl_ok', 16)")
+    node.sql("SELECT modify_rel_block('tbl_corr', 1, corrupt_header=>true)")
+    node.sql("CHECKPOINT")
+    return node
+
+
+def wait_block(node, bg, sql, wait_event, params=(), simplify_result=True):
+    """Dispatch ``sql`` on background session ``bg``, wait until bg's own
+    backend parks on ``wait_event``, and return the Future.
+
+    ``params`` are bound to ``sql``'s placeholders. ``simplify_result`` is
+    passed to background_sql: pass False when the result is a row set, so it
+    arrives as a list of tuples however many rows it has.
+    """
+    pid = bg.sql("SELECT pg_backend_pid()")
+    fut = bg.background_sql(sql, *params, simplify_result=simplify_result)
+    node.poll_query_until(
+        "SELECT wait_event FROM pg_stat_activity WHERE pid = $1",
+        pid,
+        expected=wait_event,
+    )
+    return fut
+
+
+def wait_block_any_backend(node, bg, sql, wait_event, params=(), simplify_result=True):
+    """Like ``wait_block``, but wait for *any* backend to reach ``wait_event``.
+
+    For waits that happen somewhere other than the session that issued the
+    query: the completion_wait injection point below fires in whichever process
+    runs the IO, which under io_method=worker is an IO worker rather than bg.
+    """
+    fut = bg.background_sql(sql, *params, simplify_result=simplify_result)
+    node.poll_query_until(
+        "SELECT count(*) > 0 FROM pg_stat_activity WHERE wait_event = $1",
+        wait_event,
+        expected=True,
+    )
+    return fut
+
+
+@contextlib.contextmanager
+def no_messages():
+    """Assert the server sends no NOTICE/WARNING during the block.
+
+    Uses recording (not a ``"error"`` warning filter): a warning raised inside
+    libpq's notice callback would be swallowed by ctypes rather than
+    propagated, so we collect and check afterwards instead.
+    """
+    with warnings.catch_warnings(record=True) as caught:
+        warnings.simplefilter("always")
+        yield
+    msgs = [str(w.message) for w in caught if issubclass(w.category, PostgresMessage)]
+    assert not msgs, f"unexpected server message(s): {msgs}"
+
+
+def test_handle(node):
+    """Sanity checks for the IO handle API."""
+
+    def leaks():
+        return pytest.warns(PostgresWarning, match="leaked AIO handle")
+
+    # The leak warning is reported when the (sub)transaction ends, so the
+    # explicit cases run their COMMIT/ROLLBACK inside the pytest.warns block.
+
+    # leak warning: implicit xact
+    with leaks():
+        node.sql("SELECT handle_get()")
+
+    # leak warning: explicit xact
+    with leaks():
+        node.sql("BEGIN")
+        node.sql("SELECT handle_get()")
+        node.sql("COMMIT")
+
+    # leak warning: explicit xact, rollback
+    with leaks():
+        node.sql("BEGIN")
+        node.sql("SELECT handle_get()")
+        node.sql("ROLLBACK")
+
+    # leak warning: subtrans
+    with leaks():
+        node.sql("BEGIN")
+        node.sql("SAVEPOINT foo")
+        node.sql("SELECT handle_get()")
+        node.sql("COMMIT")
+
+    # leak warning + error: released in different command (thus resowner)
+    #
+    # The COMMIT both cleans up the aborted transaction and is where the leaked
+    # handle is reported.
+    with leaks():
+        node.sql("BEGIN")
+        node.sql("SELECT handle_get()")
+        with pytest.raises(LibpqError, match="release in unexpected state"):
+            node.sql("SELECT handle_release_last()")
+        node.sql("COMMIT")
+
+    # no leak, release in same command
+    with no_messages():
+        node.sql("BEGIN")
+        node.sql("SELECT handle_get() UNION ALL SELECT handle_release_last()")
+        node.sql("COMMIT")
+
+        # normal handle use
+        node.sql("SELECT handle_get_release()")
+
+    # should error out, API violation
+    with pytest.raises(
+        LibpqError, match="API violation: Only one IO can be handed out"
+    ):
+        node.sql("SELECT handle_get_twice()")
+
+    # recover after error in implicit xact
+    with pytest.raises(LibpqError, match="as you command"):
+        node.sql("SELECT handle_get_and_error()")
+    node.sql("SELECT handle_get_release()")
+
+    # recover after error in explicit xact
+    node.sql("BEGIN")
+    with pytest.raises(LibpqError, match="as you command"):
+        node.sql("SELECT handle_get_and_error()")
+    node.sql("ROLLBACK")
+    node.sql("SELECT handle_get_release()")
+
+    # recover after error in subtrans
+    node.sql("BEGIN")
+    node.sql("SAVEPOINT foo")
+    with pytest.raises(LibpqError, match="as you command"):
+        node.sql("SELECT handle_get_and_error()")
+    node.sql("ROLLBACK TO SAVEPOINT foo")
+    node.sql("SELECT handle_get_release()")
+    node.sql("ROLLBACK")
+
+
+def test_batchmode(node):
+    """Sanity checks for the batchmode API."""
+
+    # In a build with RELCACHE_FORCE_RELEASE and CATCACHE_FORCE_RELEASE, just
+    # using SELECT batch_start() causes spurious test failures, because the
+    # lookup of the type information when printing the result tuple also
+    # starts a batch. The easiest way around is to not print a result tuple.
+    batch_start = "SELECT WHERE batch_start() IS NULL"
+
+    def open_batch():
+        return pytest.warns(PostgresWarning, match="open AIO batch at end")
+
+    # leak warning & recovery: implicit xact
+    with open_batch():
+        node.sql(batch_start)
+
+    # leak warning & recovery: explicit xact
+    with open_batch():
+        node.sql("BEGIN")
+        node.sql(batch_start)
+        node.sql("COMMIT")
+
+    # leak warning & recovery: explicit xact, rollback
+    #
+    # XXX: This doesn't fail right now, due to not getting a chance to do
+    # something at transaction command commit. That's not a correctness issue,
+    # it just means it's a bit harder to find buggy code, so the rollback case
+    # is not asserted here either.
+
+    # no warning, batch closed in same command
+    with no_messages():
+        node.sql(f"{batch_start} UNION ALL SELECT WHERE batch_end() IS NULL")
+
+
+@pytest.mark.parametrize("persistency", ["normal", "temporary"])
+def test_io_error(node, persistency):
+    """Simple cases of invalid pages are reported."""
+
+    if persistency == "normal":
+        tbl = "tbl_corr"
+        page_re = r'invalid page in block 1 of relation "base/\d+/\d+'
+    else:
+        tbl = "tmp_corr"
+        # A temporary relation's file path carries a t<backend>_ prefix.
+        page_re = r'invalid page in block 1 of relation "base/\d+/t\d+_\d+'
+
+        # It also has to be corrupted and read in the same session, so unlike
+        # in the "normal" case it cannot come from the module fixture.
+        node.sql(f"CREATE TEMPORARY TABLE {tbl}(data int not null)")
+        node.sql(f"INSERT INTO {tbl} SELECT generate_series(1, 10000)")
+        node.sql("SELECT modify_rel_block($1, 1, corrupt_header=>true)", tbl)
+
+    # verify the error is reported in custom C code
+    with pytest.raises(LibpqError, match=page_re):
+        node.sql("SELECT read_rel_block_ll($1, 1)", tbl)
+    # verify the error is reported for bufmgr reads, seq scan
+    with pytest.raises(LibpqError, match=page_re):
+        node.sql(f"SELECT count(*) FROM {tbl}")
+    # verify the error is reported for bufmgr reads, tid scan
+    with pytest.raises(LibpqError, match=page_re):
+        node.sql(f"SELECT count(*) FROM {tbl} WHERE ctid = '(1, 1)'")
+
+
+def test_complete_foreign(node):
+    """A read started but not awaited by one backend can be completed by
+    another (or after the starting backend exits)."""
+    a = node.connect()
+    b = node.connect()
+
+    # Test that if the backend issuing a read doesn't wait for the IO's
+    # completion, another backend can complete the IO
+    a.sql("SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false)")
+
+    # Check that another backend can read the relevant block
+    with no_messages():
+        assert b.sql("SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1") == 1, (
+            "another backend can read the relevant block"
+        )
+
+    # Test that if the backend issuing a read exits before the IO completes,
+    # another backend can still complete the IO
+    a.sql("SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false)")
+    a.close()
+    a = node.connect()
+    with no_messages():
+        assert b.sql("SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1") == 1, (
+            "another backend completes the IO after the issuing backend exited"
+        )
+
+    # Read a tbl_corr block, then sleep. The other session will retry the IO
+    # and also fail. The easiest thing to verify that seems to be to check
+    # that both are in the log.
+    offset = node.current_log_position()
+    a.sql("SELECT read_rel_block_ll('tbl_corr', 1, wait_complete=>false)")
+    with pytest.raises(LibpqError, match="invalid page in block"):
+        b.sql("SELECT count(*) FROM tbl_corr WHERE ctid = '(1,1)' LIMIT 1")
+    node.wait_for_log(r"LOG[^\n]+invalid page in", offset)
+    node.wait_for_log(r"ERROR[^\n]+invalid page in", offset)
+
+
+def test_close_fd(node):
+    """FDs being closed while IO is in progress is handled."""
+
+    with no_messages():
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>true, "
+            "batchmode_enter=>true, smgrreleaseall=>true, batchmode_exit=>true)"
+        )
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false, "
+            "batchmode_enter=>true, smgrreleaseall=>true, batchmode_exit=>true)"
+        )
+        assert (
+            node.sql("SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1") == 1
+        ), "reads work after smgrreleaseall() in batchmode"
+
+
+@pytest.mark.parametrize("persistency", ["normal", "unlogged", "temporary"])
+def test_invalidate(node, persistency):
+    """A relation removed (rollback or DROP) while IO is ongoing is handled."""
+
+    kind = "" if persistency == "normal" else persistency
+    tbl = f"{persistency}_transactional"
+    create = (
+        f"CREATE {kind} TABLE {tbl} (id int not null, data text not null) "
+        "WITH (AUTOVACUUM_ENABLED = false)"
+    )
+    insert = (
+        f"INSERT INTO {tbl}(id, data) "
+        "SELECT generate_series(1, 10000) as id, repeat('a', 200)"
+    )
+
+    # Verify that outstanding read IO does not cause problems with
+    # AbortTransaction -> smgrDoPendingDeletes -> smgrdounlinkall -> ...
+    # -> Invalidate[Local]Buffer.
+    node.sql("BEGIN")
+    node.sql(create)
+    node.sql(insert)
+    node.sql("SELECT read_rel_block_ll($1, 1, wait_complete=>false)", tbl)
+    with no_messages():
+        node.sql("ROLLBACK")
+
+    # Verify that outstanding read IO does not cause problems with
+    # CommitTransaction -> smgrDoPendingDeletes -> smgrdounlinkall -> ...
+    # -> Invalidate[Local]Buffer.
+    node.sql("BEGIN")
+    node.sql(create)
+    node.sql(insert)
+    node.sql("COMMIT")
+    node.sql("BEGIN")
+    node.sql("SELECT read_rel_block_ll($1, 1, wait_complete=>false)", tbl)
+    with no_messages():
+        node.sql(f"DROP TABLE {tbl}")
+        node.sql("COMMIT")
+
+
+def test_inject(node):
+    """Tests using injection points. Mostly to exercise hard IO errors that are
+    hard to trigger without using injection points."""
+    skip_unless_injection_points()
+
+    base_err = r'could not read blocks 2\.\.2 in file "base/.*"'
+
+    # injected what we'd expect
+    node.sql("SELECT inj_io_short_read_attach(8192)")
+    node.sql("SELECT invalidate_rel_block('tbl_ok', 2)")
+    with no_messages():
+        assert node.sql("SELECT count(*) FROM tbl_ok WHERE ctid = '(2, 1)'") == 1, (
+            "injected what we'd expect"
+        )
+
+    # injected a read shorter than a single block, expecting error
+    node.sql("SELECT inj_io_short_read_attach(17)")
+    node.sql("SELECT invalidate_rel_block('tbl_ok', 2)")
+    with pytest.raises(LibpqError, match=base_err + ": read only 0 of 8192 bytes"):
+        node.sql("SELECT count(*) FROM tbl_ok WHERE ctid = '(2, 1)'")
+
+    inval = [
+        f"SELECT invalidate_rel_block('tbl_ok', {b})" for b in (0, 1, 2, 3, 5, 6, 7, 8)
+    ]
+
+    # shorten multi-block read to a single block, should retry
+    node.sql_batch(*inval)
+    node.sql("SELECT inj_io_short_read_attach(8192)")
+    with no_messages():
+        assert node.sql("SELECT count(*) FROM tbl_ok") == 10000, (
+            "shortened multi-block read to a single block, retried"
+        )
+
+    # shorten multi-block read to two blocks, should retry
+    node.sql_batch(*inval)
+    node.sql("SELECT inj_io_short_read_attach(8192*2)")
+    with no_messages():
+        assert node.sql("SELECT count(*) FROM tbl_ok") == 10000, (
+            "shortened multi-block read to two blocks, retried"
+        )
+
+    # verify that page verification errors are detected even as part of a
+    # shortened multi-block read (tbl_corr, block 1 is corrupted)
+    node.sql_batch(
+        "SELECT invalidate_rel_block('tbl_corr', 0)",
+        "SELECT invalidate_rel_block('tbl_corr', 1)",
+        "SELECT invalidate_rel_block('tbl_corr', 2)",
+        "SELECT inj_io_short_read_attach(8192)",
+    )
+    with pytest.raises(
+        LibpqError, match=r'invalid page in block 1 of relation "base/.*'
+    ):
+        node.sql("SELECT count(*) FROM tbl_corr WHERE ctid < '(2, 1)'")
+
+    # trigger a hard error, should error out
+    node.sql("SELECT inj_io_short_read_attach(-errno_from_string('EIO'))")
+    node.sql("SELECT invalidate_rel_block('tbl_ok', 2)")
+    eio = base_err + r": (?:I/O|Input/output) error"
+    with pytest.raises(LibpqError, match=eio):
+        node.sql("SELECT count(*) FROM tbl_ok")
+    with pytest.raises(LibpqError, match=eio):
+        node.sql("SELECT count(*) FROM tbl_ok")
+    # now the IO should be ok.
+    node.sql("SELECT inj_io_short_read_detach()")
+    with no_messages():
+        assert node.sql("SELECT count(*) FROM tbl_ok") == 10000, (
+            "IO is ok again after detaching the injection point"
+        )
+
+    # trigger a different hard error, should error out
+    node.sql("SELECT inj_io_short_read_attach(-errno_from_string('EROFS'))")
+    node.sql("SELECT invalidate_rel_block('tbl_ok', 2)")
+    with pytest.raises(LibpqError, match=base_err + ": Read-only file system"):
+        node.sql("SELECT count(*) FROM tbl_ok")
+    node.sql("SELECT inj_io_short_read_detach()")
+
+
+def test_inject_worker(node):
+    """Tests using injection points, only for io_method=worker.
+
+    io_method=worker has the special case of needing to reopen files. That can
+    in theory fail, because the file could be gone. That's a hard path to test
+    for real, so we use an injection point to trigger it.
+    """
+    if node.sql("SHOW io_method") != "worker":
+        pytest.skip("worker-specific test")
+    skip_unless_injection_points()
+
+    # trigger a failure to reopen, should error out, but should recover
+    node.sql("SELECT inj_io_reopen_attach()")
+    node.sql("SELECT invalidate_rel_block('tbl_ok', 1)")
+    with pytest.raises(
+        LibpqError,
+        match=r'could not read blocks 1\.\.1 in file "base/.*": No such file or directory',
+    ):
+        node.sql("SELECT count(*) FROM tbl_ok")
+    node.sql("SELECT inj_io_reopen_detach()")
+    with no_messages():
+        assert node.sql("SELECT count(*) FROM tbl_ok") == 10000, (
+            "IO is ok again after detaching the reopen injection point"
+        )
+
+
+def checksum_failures(conn, datname):
+    """The (count, last failure time) pair for a database, or for shared
+    relations when ``datname`` is None."""
+    if datname is None:
+        return conn.sql(
+            "SELECT checksum_failures, checksum_last_failure "
+            "FROM pg_stat_database WHERE datname IS NULL"
+        )
+    return conn.sql(
+        "SELECT checksum_failures, checksum_last_failure "
+        "FROM pg_stat_database WHERE datname = $1",
+        datname,
+    )
+
+
+def checksum_count(conn, datname):
+    return checksum_failures(conn, datname)[0]
+
+
+def assert_checksum_increased(node, before, datname):
+    """Wait until the database's checksum_failures has risen past ``before``
+    and a failure timestamp is recorded (stats flush asynchronously).
+
+    The comparison is made here rather than in the polled query so a failure
+    reports the count it actually saw.
+    """
+    for _ in wait_until(f"checksum_failures did not rise above {before}"):
+        count, last_failure = checksum_failures(node, datname)
+        if count > before and last_failure is not None:
+            return
+
+
+def start_buffer_io(buf, wait):
+    return f"SELECT buffer_call_start_io({buf}, for_input=>true, wait=>{wait})"
+
+
+def terminate_buffer_io(buf, succeed):
+    return (
+        f"SELECT buffer_call_terminate_io({buf}, for_input=>true, "
+        f"succeed=>{succeed}, io_error=>false, release_aio=>false)"
+    )
+
+
+def test_startwait_io(node):
+    """Test interplay between StartBufferIO and TerminateBufferIO."""
+    a = node.connect()
+    b = node.connect()
+
+    # Verify behavior for normal tables
+
+    # create a buffer we can play around with
+    buf = a.sql("SELECT buffer_create_toy('tbl_ok', 1)")
+
+    # check that one backend can perform StartBufferIO
+    assert a.sql(start_buffer_io(buf, "true")) is True, "first StartBufferIO"
+
+    # but not twice on the same buffer (non-waiting)
+    assert a.sql(start_buffer_io(buf, "false")) is False, (
+        "second StartBufferIO fails, same session"
+    )
+    assert b.sql(start_buffer_io(buf, "false")) is False, (
+        "second StartBufferIO fails, other session"
+    )
+
+    # start io in a different session, will block
+    fut = wait_block(node, b, start_buffer_io(buf, "true"), "BufferIo")
+
+    # Terminate the IO, without marking it as success, this should trigger the
+    # waiting session to be able to start the io
+    a.sql(terminate_buffer_io(buf, "false"))
+
+    # Because the IO was terminated, but not marked as valid, second session
+    # should get the right to start io
+    assert fut.result() is True, "blocking start buffer io, terminating io, not valid"
+
+    # terminate the IO again
+    b.sql(terminate_buffer_io(buf, "false"))
+
+    # same as the above scenario, but mark IO as having succeeded
+    assert a.sql(start_buffer_io(buf, "true")) is True, (
+        "blocking buffer io w/ success: first start buffer io"
+    )
+
+    # start io in a different session, will block
+    fut = wait_block(node, b, start_buffer_io(buf, "true"), "BufferIo")
+
+    # Terminate the IO, marking it as success
+    a.sql(terminate_buffer_io(buf, "true"))
+
+    # Because the IO was terminated, and marked as valid, second session should
+    # complete but not need io
+    assert fut.result() is False, "blocking start buffer io, terminating io, valid"
+
+    # buffer is valid now, make it invalid again
+    a.sql("SELECT buffer_create_toy('tbl_ok', 1)")
+
+    # Verify behavior for temporary tables
+    #
+    # Can't unfortunately share the code with the normal table case, there are
+    # too many behavioral differences.
+
+    # create a buffer we can play around with
+    a.sql("CREATE TEMPORARY TABLE tmp_ok(data int not null)")
+    a.sql("INSERT INTO tmp_ok SELECT generate_series(1, 10000)")
+    buf = a.sql("SELECT buffer_create_toy('tmp_ok', 3)")
+
+    # check that one backend can perform StartLocalBufferIO
+    assert a.sql(start_buffer_io(buf, "false")) is True, "first StartLocalBufferIO"
+
+    # Because local buffers don't use IO_IN_PROGRESS, a second
+    # StartLocalBufferIO succeeds as well. This test mostly serves as a
+    # documentation of that fact. If we had actually started IO, it'd be
+    # different.
+    assert a.sql(start_buffer_io(buf, "false")) is True, (
+        "second StartLocalBufferIO succeeds, same session"
+    )
+
+    # Terminate the IO again, without marking it as a success
+    a.sql(terminate_buffer_io(buf, "false"))
+    assert a.sql(start_buffer_io(buf, "false")) is True, (
+        "StartLocalBufferIO after not marking valid succeeds, same session"
+    )
+
+    # Terminate the IO again, marking it as a success
+    a.sql(terminate_buffer_io(buf, "true"))
+
+    # Now another StartLocalBufferIO should fail, this time because the buffer
+    # is already valid.
+    assert a.sql(start_buffer_io(buf, "true")) is False, (
+        "StartLocalBufferIO after marking valid fails"
+    )
+
+    # The remaining tests don't make sense for temp tables, as they are
+    # concerned with multiple sessions interacting with each other.
+
+
+def test_read_buffers(node):
+    """Tests for StartReadBuffers()."""
+    a = node.connect()
+    b = node.connect()
+    a.sql("CREATE TEMPORARY TABLE tmp_ok(data int not null)")
+    a.sql("INSERT INTO tmp_ok SELECT generate_series(1, 5000)")
+
+    cols = "blockoff, blocknum, io_reqd, nblocks"
+    # io_reqd masked by foreign IO, for the in-progress cases.
+    cols_nf = "blockoff, blocknum, io_reqd and not foreign_io, nblocks"
+
+    def read_buffers(table, start, n, c=cols):
+        # simplify_result=False so a one-row result still comes back as a list:
+        # otherwise the shape of each expected value below would silently encode
+        # how many rows it expects.
+        # c is a column list, so it stays interpolated; the relation and block
+        # numbers are values.
+        return a.sql(
+            f"SELECT {c} FROM read_buffers($1, $2, $3)",
+            table,
+            start,
+            n,
+            simplify_result=False,
+        )
+
+    for table in ("tbl_ok", "tmp_ok"):
+        # check that consecutive misses are combined into one read
+        a.sql("SELECT evict_rel($1)", table)
+        assert read_buffers(table, 0, 2) == [(0, 0, True, 2)], (
+            f"{table}: read buffers, combine, block 0-1"
+        )
+        # but if we do it again, i.e. it's in the buffer pool, there will be
+        # two operations
+        assert read_buffers(table, 0, 2) == [
+            (0, 0, False, 1),
+            (1, 1, False, 1),
+        ], f"{table}: read buffers, doesn't combine hits, block 0-1"
+        # Check that a larger read interrupted by a hit works
+        assert read_buffers(table, 3, 1) == [(0, 3, True, 1)], (
+            f"{table}: read buffers, prep, block 3"
+        )
+        assert read_buffers(table, 2, 4) == [
+            (0, 2, True, 1),
+            (1, 3, False, 1),
+            (2, 4, True, 2),
+        ], f"{table}: read buffers, interrupted by hit on 3, block 2-5"
+
+        # Verify that a read with an initial buffer hit works
+        a.sql("SELECT evict_rel($1)", table)
+        assert read_buffers(table, 0, 1) == [(0, 0, True, 1)], (
+            f"{table}: read buffers, miss, block 0"
+        )
+        assert read_buffers(table, 0, 1) == [(0, 0, False, 1)], (
+            f"{table}: read buffers, hit, block 0"
+        )
+        assert read_buffers(table, 1, 1) == [(0, 1, True, 1)], (
+            f"{table}: read buffers, miss, block 1"
+        )
+        assert read_buffers(table, 1, 1) == [(0, 1, False, 1)], (
+            f"{table}: read buffers, hit, block 1"
+        )
+        assert read_buffers(table, 0, 2) == [
+            (0, 0, False, 1),
+            (1, 1, False, 1),
+        ], f"{table}: read buffers, hit, block 0-1"
+        assert read_buffers(table, 0, 3) == [
+            (0, 0, False, 1),
+            (1, 1, False, 1),
+            (2, 2, True, 1),
+        ], f"{table}: read buffers, hit 0-1, miss 2"
+
+        # Verify that a read with an initial miss and trailing buffer hit(s) works
+        a.sql("SELECT invalidate_rel_block($1, 0)", table)
+        assert read_buffers(table, 0, 3) == [
+            (0, 0, True, 1),
+            (1, 1, False, 1),
+            (2, 2, False, 1),
+        ], f"{table}: read buffers, miss 0, hit 1-2"
+        a.sql("SELECT invalidate_rel_block($1, 1)", table)
+        a.sql("SELECT invalidate_rel_block($1, 2)", table)
+        a.sql("SELECT * FROM read_buffers($1, 3, 2)", table)
+        assert read_buffers(table, 1, 4) == [
+            (0, 1, True, 2),
+            (2, 3, False, 1),
+            (3, 4, False, 1),
+        ], f"{table}: read buffers, miss 1-2, hit 3-4"
+
+        # Verify that we aren't doing reads larger than
+        # io_combine_limit. That's just enforced in read_buffers() function,
+        # but kinda still worth testing.
+        a.sql("SELECT evict_rel($1)", table)
+        a.sql("SET io_combine_limit=3")
+        assert read_buffers(table, 1, 5) == [
+            (0, 1, True, 3),
+            (3, 4, True, 2),
+        ], f"{table}: read buffers, io_combine_limit has effect"
+        a.sql("RESET io_combine_limit")
+
+        # Test encountering buffer IO we started in the first block of the
+        # range.
+        #
+        # Depending on how quick the IO we start completes, the IO might be
+        # completed or we "join" the foreign IO. To hide that variability, the
+        # query below treats a foreign IO as not having needed to do IO.
+        a.sql("SELECT evict_rel($1)", table)
+        a.sql("SELECT read_rel_block_ll($1, 1, wait_complete=>false)", table)
+        assert read_buffers(table, 1, 3, cols_nf) == [
+            (0, 1, False, 1),
+            (1, 2, True, 2),
+        ], f"{table}: read buffers, in-progress 1, read 1-3"
+        # Test in-progress IO in the middle block of the range
+        a.sql("SELECT evict_rel($1)", table)
+        a.sql("SELECT read_rel_block_ll($1, 2, wait_complete=>false)", table)
+        assert read_buffers(table, 1, 3, cols_nf) == [
+            (0, 1, True, 1),
+            (1, 2, False, 1),
+            (2, 3, True, 1),
+        ], f"{table}: read buffers, in-progress 2, read 1-3"
+        # Test in-progress IO on the last block of the range
+        a.sql("SELECT evict_rel($1)", table)
+        a.sql("SELECT read_rel_block_ll($1, 3, wait_complete=>false)", table)
+        assert read_buffers(table, 1, 3, cols_nf) == [
+            (0, 1, True, 2),
+            (2, 3, False, 1),
+        ], f"{table}: read buffers, in-progress 3, read 1-3"
+
+    # Test start buffer IO will split IO if there's IO in progress. We can't
+    # observe this with sync, as that does not start the IO operation in
+    # StartReadBuffers().
+    table = "tbl_ok"
+    fcols = "blockoff, blocknum, io_reqd, foreign_io, nblocks"
+    if node.sql("SHOW io_method") != "sync":
+        # Because no IO wref was assigned, block 3 should not report foreign IO
+        a.sql("SELECT evict_rel($1)", table)
+        buf = b.sql("SELECT buffer_create_toy($1, 3)", table)
+        b.sql(start_buffer_io(buf, "true"))
+        fut = wait_block(
+            node,
+            a,
+            f"SELECT {fcols} FROM read_buffers($1, 1, 5)",
+            "BufferIo",
+            params=(table,),
+            simplify_result=False,
+        )
+        b.sql(terminate_buffer_io(buf, "false"))
+        assert fut.result() == [
+            (0, 1, True, False, 2),
+            (2, 3, True, False, 3),
+        ], "IO was split due to concurrent failed IO"
+
+        # Same as before, except the concurrent IO succeeds this time
+        a.sql("SELECT evict_rel($1)", table)
+        buf = b.sql("SELECT buffer_create_toy($1, 3)", table)
+        b.sql(start_buffer_io(buf, "true"))
+        fut = wait_block(
+            node,
+            a,
+            f"SELECT {fcols} FROM read_buffers($1, 1, 5)",
+            "BufferIo",
+            params=(table,),
+            simplify_result=False,
+        )
+        b.sql(terminate_buffer_io(buf, "true"))
+        assert fut.result() == [
+            (0, 1, True, False, 2),
+            (2, 3, False, False, 1),
+            (3, 4, True, False, 2),
+        ], "IO was split due to concurrent successful IO"
+
+
+@pytest.mark.parametrize("persistency", ["normal", "temporary"])
+def test_zero(node, persistency):
+    """Behavior of ZERO_ON_ERROR and zero_damaged_pages."""
+    a = node.connect()
+    b = node.connect()
+
+    kind = "" if persistency == "normal" else persistency
+    a.sql(f"CREATE {kind} TABLE tbl_zero(id int) WITH (AUTOVACUUM_ENABLED = false)")
+    a.sql("INSERT INTO tbl_zero SELECT generate_series(1, 10000)")
+
+    a.sql("SELECT modify_rel_block('tbl_zero', 0, corrupt_header=>true)")
+
+    # A page validity error is reported,
+    with pytest.raises(LibpqError, match=r"invalid page in block 0 of relation"):
+        a.sql("SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)")
+    # ... or zeroed with a warning under zero_on_error.
+    with pytest.warns(
+        PostgresWarning,
+        match=r"invalid page in block 0 of relation .*; zeroing out page",
+    ):
+        a.sql("SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>true)")
+
+    # Once fixed, the block reads cleanly.
+    a.sql("SELECT modify_rel_block('tbl_zero', 0, zero=>true)")
+    with no_messages():
+        a.sql("SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)")
+
+    # The correct block number is reported for a different block.
+    a.sql("SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true)")
+    with pytest.warns(
+        PostgresWarning,
+        match=r"invalid page in block 3 of relation .*; zeroing out page",
+    ):
+        a.sql("SELECT read_rel_block_ll('tbl_zero', 3, zero_on_error=>true)")
+
+    # One read reporting multiple invalid blocks.
+    a.sql("SELECT modify_rel_block('tbl_zero', 2, corrupt_header=>true)")
+    a.sql("SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true)")
+    with pytest.raises(
+        LibpqError, match=r"2 invalid pages among blocks 1\.\.4 of relation"
+    ) as err:
+        a.sql(
+            "SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)"
+        )
+    assert err.value.detail == "Block 2 held the first invalid page."
+    assert err.value.hint == "See server log for the other 1 invalid block(s)."
+
+    # zeroed via the ZERO_ON_ERROR flag ...
+    with pytest.warns(
+        PostgresWarning,
+        match=r"zeroing out 2 invalid pages among blocks 1\.\.4 of relation",
+    ) as caught:
+        a.sql(
+            "SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>true)"
+        )
+    warning = caught.pop(PostgresWarning).message
+    assert warning.detail == "Block 2 held the first zeroed page."
+    assert warning.hint == "See server log for the other 1 zeroed block(s)."
+
+    # ... and via zero_damaged_pages.
+    a.sql("BEGIN")
+    a.sql("SET LOCAL zero_damaged_pages = true")
+    with pytest.warns(
+        PostgresWarning,
+        match=r"zeroing out 2 invalid pages among blocks 1\.\.4 of relation",
+    ) as caught:
+        a.sql(
+            "SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)"
+        )
+    warning = caught.pop(PostgresWarning).message
+    assert warning.detail == "Block 2 held the first zeroed page."
+    assert warning.hint == "See server log for the other 1 zeroed block(s)."
+    a.sql("COMMIT")
+
+    # bufmgr IO detects page validity errors.
+    a.sql(
+        "SELECT invalidate_rel_block('tbl_zero', g.i) FROM generate_series(0, 15) g(i)"
+    )
+    a.sql("SELECT modify_rel_block('tbl_zero', 3, zero=>true)")
+    with pytest.raises(LibpqError, match=r"invalid page in block 2 of relation"):
+        a.sql("SELECT count(*) FROM tbl_zero")
+    # ... and zeroes them with zero_damaged_pages.
+    a.sql("BEGIN")
+    a.sql("SET LOCAL zero_damaged_pages = true")
+    with pytest.warns(PostgresWarning, match=r"invalid page in block 2 of relation"):
+        a.sql("SELECT count(*) FROM tbl_zero")
+    a.sql("COMMIT")
+
+    # A page validity error in an IO that session B completes must not be
+    # logged visibly to B. Needs cross-session access, so non-temp only.
+    if persistency != "temporary":
+        a.sql("SELECT modify_rel_block('tbl_zero', 1, corrupt_header=>true)")
+        a.sql(
+            "SELECT read_rel_block_ll('tbl_zero', 1, wait_complete=>false, "
+            "zero_on_error=>true)"
+        )
+        with no_messages():
+            assert b.sql("SELECT count(*) > 0 FROM tbl_zero"), (
+                "page validity error is not logged to the completing session"
+            )
+
+    a.sql("DROP TABLE tbl_zero")
+
+
+def test_checksum(node):
+    """Checksum failures are detected and reported in the stats."""
+
+    node.sql("CREATE TABLE tbl_normal(id int) WITH (AUTOVACUUM_ENABLED = false)")
+    node.sql("INSERT INTO tbl_normal SELECT generate_series(1, 5000)")
+    node.sql("SELECT modify_rel_block('tbl_normal', 3, corrupt_checksum=>true)")
+    node.sql(
+        "CREATE TEMPORARY TABLE tbl_temp(id int) WITH (AUTOVACUUM_ENABLED = false)"
+    )
+    node.sql("INSERT INTO tbl_temp SELECT generate_series(1, 5000)")
+    node.sql("SELECT modify_rel_block('tbl_temp', 3, corrupt_checksum=>true)")
+    node.sql("SELECT modify_rel_block('tbl_temp', 4, corrupt_checksum=>true)")
+
+    # A shared rel with invalid pages: pg_shseclabel isn't accessed by default.
+    node.sql("SELECT grow_rel('pg_shseclabel', 4)")
+    node.sql("SELECT modify_rel_block('pg_shseclabel', 2, corrupt_checksum=>true)")
+    node.sql("SELECT modify_rel_block('pg_shseclabel', 3, corrupt_checksum=>true)")
+
+    # normal rel
+    before = checksum_count(node, "postgres")
+    with pytest.raises(
+        LibpqError, match=r'invalid page in block 3 of relation "base/\d+/\d+"'
+    ):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_normal', 3, nblocks=>1, zero_on_error=>false)"
+        )
+    assert_checksum_increased(node, before, "postgres")
+
+    # temp rel
+    before = checksum_count(node, "postgres")
+    with pytest.raises(
+        LibpqError, match=r'invalid page in block 4 of relation "base/\d+/t\d+_\d+"'
+    ):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_temp', 4, nblocks=>2, zero_on_error=>false)"
+        )
+    assert_checksum_increased(node, before, "postgres")
+
+    # shared rel
+    before = checksum_count(node, None)
+    with pytest.raises(
+        LibpqError,
+        match=r'2 invalid pages among blocks 2\.\.3 of relation "global/\d+"',
+    ) as err:
+        node.sql(
+            "SELECT read_rel_block_ll('pg_shseclabel', 2, nblocks=>2, zero_on_error=>false)"
+        )
+    assert err.value.detail == "Block 2 held the first invalid page."
+    assert err.value.hint == "See server log for the other 1 invalid block(s)."
+    assert_checksum_increased(node, before, None)
+
+    # restore sanity
+    node.sql("SELECT modify_rel_block('pg_shseclabel', 1, zero=>true)")
+    node.sql("DROP TABLE tbl_normal")
+
+
+def test_ignore_checksum(node):
+    """ignore_checksum_failure handling, including multi-block reads."""
+
+    node.sql("CREATE TABLE tbl_cs_fail(id int) WITH (AUTOVACUUM_ENABLED = false)")
+    node.sql("INSERT INTO tbl_cs_fail SELECT generate_series(1, 10000)")
+    count_sql = "SELECT count(*) FROM tbl_cs_fail"
+    invalidate = "SELECT invalidate_rel_block('tbl_cs_fail', g.i) FROM generate_series(0, 6) g(i)"
+    expect = node.sql(count_sql)
+
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 1, corrupt_checksum=>true)")
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 5, corrupt_checksum=>true)")
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 6, corrupt_checksum=>true)")
+
+    # off: a wrong checksum errors.
+    node.sql(invalidate)
+    with pytest.raises(LibpqError, match=r"invalid page in block"):
+        node.sql(count_sql)
+
+    # on: it is ignored with a warning.
+    node.sql("SET ignore_checksum_failure=on")
+    node.sql(invalidate)
+    with pytest.warns(
+        PostgresWarning, match=r"ignoring (checksum failure|\d checksum failures)"
+    ):
+        assert node.sql(count_sql) == expect, (
+            "checksum failure ignored with ignore_checksum_failure=on"
+        )
+
+    # ignore in a multi-block read still surfaces a real invalid page as ERROR.
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 2, zero=>true)")
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true)")
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 4, corrupt_header=>true)")
+    offset = node.current_log_position()
+    with pytest.warns(PostgresWarning, match=r"ignoring checksum failure in block 3"):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false)"
+        )
+    node.wait_for_log(r"LOG:  ignoring checksum failure", offset)
+    with pytest.raises(
+        LibpqError, match=r'invalid page in block 4 of relation "base/\d+/\d+"'
+    ):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_cs_fail', 2, nblocks=>3, zero_on_error=>false)"
+        )
+
+    # multi-block read with different problems in different blocks, zeroed.
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 1, zero=>true)")
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 2, corrupt_checksum=>true)")
+    node.sql(
+        "SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header=>true)"
+    )
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 4, corrupt_header=>true)")
+    node.sql("SELECT modify_rel_block('tbl_cs_fail', 5, corrupt_header=>true)")
+    offset = node.current_log_position()
+    with pytest.warns(
+        PostgresWarning,
+        match=r"zeroing 3 page\(s\) and ignoring 2 checksum failure\(s\) "
+        r"among blocks 1\.\.5 of relation",
+    ):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_cs_fail', 1, nblocks=>5, zero_on_error=>true)"
+        )
+    node.wait_for_log(r"LOG:  ignoring checksum failure in block 2", offset)
+    node.wait_for_log(
+        r'LOG:  invalid page in block 3 of relation "base.*"; zeroing out page', offset
+    )
+    node.wait_for_log(
+        r'LOG:  invalid page in block 4 of relation "base.*"; zeroing out page', offset
+    )
+    node.wait_for_log(
+        r'LOG:  invalid page in block 5 of relation "base.*"; zeroing out page', offset
+    )
+
+    # both an invalid header and an invalid checksum in one block
+    node.sql(
+        "SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header=>true)"
+    )
+    with pytest.raises(LibpqError, match=r'invalid page in block 3 of relation "'):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false)"
+        )
+    with pytest.warns(
+        PostgresWarning,
+        match=r'invalid page in block 3 of relation "base/.*"; zeroing out page',
+    ):
+        node.sql(
+            "SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>true)"
+        )
+
+
+def test_checksum_createdb(node):
+    """Checksum handling when creating a database from one with an invalid
+    block (also a minimal cross-database IO check)."""
+    node.sql("CREATE DATABASE regression_createdb_source")
+    src = node.connect(dbname="regression_createdb_source")
+    src.sql("CREATE EXTENSION test_aio")
+    src.sql(
+        "CREATE TABLE tbl_cs_fail(data int not null) WITH (AUTOVACUUM_ENABLED = false)"
+    )
+    src.sql("INSERT INTO tbl_cs_fail SELECT generate_series(1, 1000)")
+    src.sql("SELECT modify_rel_block('tbl_cs_fail', 1, corrupt_checksum=>true)")
+    # Closed explicitly, not left to per-test cleanup: CREATE DATABASE below
+    # refuses to copy a template that still has sessions connected to it.
+    src.close()
+
+    createdb = (
+        "CREATE DATABASE regression_createdb_target "
+        "TEMPLATE regression_createdb_source STRATEGY wal_log"
+    )
+
+    # An invalid source block fails the create and is accounted for.
+    before = checksum_count(node, "regression_createdb_source")
+    with pytest.raises(
+        LibpqError, match=r'invalid page in block 1 of relation "base/\d+/\d+"'
+    ):
+        node.sql(createdb)
+    assert_checksum_increased(node, before, "regression_createdb_source")
+
+    # Once the source is fixed, the create succeeds.
+    src = node.connect(dbname="regression_createdb_source")
+    src.sql("SELECT modify_rel_block('tbl_cs_fail', 1, zero=>true)")
+    src.close()
+
+    with no_messages():
+        node.sql(createdb)
+
+
+def test_read_buffers_inject(node):
+    """StartReadBuffers() recognizing another backend's in-progress IO as
+    foreign IO, using injection points to hold an IO in its completion hook."""
+    skip_unless_injection_points()
+    a = node.connect()
+    b = node.connect()
+    c = node.connect()
+    table = "tbl_ok"
+    sync = node.sql("SHOW io_method") == "sync"
+    fcols = "blockoff, blocknum, io_reqd, foreign_io, nblocks"
+
+    def configure_wait(blockno):
+        """B: Trigger wait in the next AIO read for the given block."""
+        b.sql(
+            "SELECT inj_io_completion_wait(pid=>pg_backend_pid(), "
+            f"relfilenode=>pg_relation_filenode('{table}'), blockno=>{blockno})"
+        )
+
+    # Test if a read buffers encounters AIO in progress by another backend, it
+    # recognizes that other IO as a foreign IO.
+    a.sql("SELECT evict_rel($1)", table)
+    configure_wait(1)
+    b_fut = wait_block_any_backend(
+        node,
+        b,
+        "SELECT read_rel_block_ll($1, blockno=>1, nblocks=>1)",
+        "completion_wait",
+        params=(table,),
+    )
+    a_fut = wait_block(
+        node,
+        a,
+        f"SELECT {fcols} FROM read_buffers($1, 1, 4)",
+        "AioIoCompletion",
+        params=(table,),
+        simplify_result=False,
+    )
+    # C: Release B from completion hook
+    # C: Release B from completion hook
+    c.sql("SELECT inj_io_completion_continue()")
+    b_fut.result()
+    if sync:
+        # sync doesn't issue IO below StartReadBuffers(): one combined read.
+        assert a_fut.result() == [(0, 1, True, False, 4)], (
+            "read 1-3, blocked on in-progress 1, see expected result"
+        )
+    else:
+        # a foreign IO covering block 1, plus one covering blocks 2-4.
+        assert a_fut.result() == [
+            (0, 1, True, True, 1),
+            (1, 2, True, False, 3),
+        ], "read 1-3, blocked on in-progress 1, see expected result"
+
+    # Test if a read buffers encounters AIO in progress by another backend, it
+    # recognizes that other IO as a foreign IO. This time we encounter the
+    # foreign IO multiple times.
+    a.sql("SELECT evict_rel($1)", table)
+    configure_wait(3)
+    b_fut = wait_block_any_backend(
+        node,
+        b,
+        "SELECT read_rel_block_ll($1, blockno=>2, nblocks=>2)",
+        "completion_wait",
+        params=(table,),
+    )
+    a_fut = wait_block(
+        node,
+        a,
+        f"SELECT {fcols} FROM read_buffers($1, 0, 4)",
+        "AioIoCompletion",
+        params=(table,),
+        simplify_result=False,
+    )
+    c.sql("SELECT inj_io_completion_continue()")
+    b_fut.result()
+    if sync:
+        assert a_fut.result() == [(0, 0, True, False, 4)], (
+            "read 0-3, blocked on in-progress 2+3, see expected result"
+        )
+    else:
+        # one IO for blocks 0-1, then foreign IOs for blocks 2 and 3.
+        assert a_fut.result() == [
+            (0, 0, True, False, 2),
+            (2, 2, True, True, 1),
+            (3, 3, True, True, 1),
+        ], "read 0-3, blocked on in-progress 2+3, see expected result"
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
deleted file mode 100644
index 63cadd64c15..00000000000
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ /dev/null
@@ -1,1902 +0,0 @@
-# Copyright (c) 2025-2026, PostgreSQL Global Development Group
-
-use strict;
-use warnings FATAL => 'all';
-
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-
-use FindBin;
-use lib $FindBin::RealBin;
-
-use TestAio;
-
-my @methods = TestAio::supported_io_methods();
-my %nodes;
-
-
-###
-# Create and configure one instance for each io_method
-###
-
-foreach my $method (@methods)
-{
-	my $node = PostgreSQL::Test::Cluster->new($method);
-
-	$nodes{$method} = $node;
-	$node->init();
-	$node->append_conf('postgresql.conf', "io_method=$method");
-	TestAio::configure($node);
-}
-
-# Just to have one test not use the default auto-tuning
-$nodes{'sync'}->append_conf(
-	'postgresql.conf', qq(
- io_max_concurrency=4
-));
-
-
-###
-# Execute the tests for each io_method
-###
-
-foreach my $method (@methods)
-{
-	my $node = $nodes{$method};
-
-	$node->start();
-	test_io_method($method, $node);
-	$node->stop();
-}
-
-done_testing();
-
-
-###
-# Test Helpers
-###
-
-
-sub psql_like
-{
-	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my $io_method = shift;
-	my $psql = shift;
-	my $name = shift;
-	my $sql = shift;
-	my $expected_stdout = shift;
-	my $expected_stderr = shift;
-	my ($cmdret, $output);
-
-	($output, $cmdret) = $psql->query($sql);
-
-	like($output, $expected_stdout, "$io_method: $name: expected stdout");
-	like($psql->{stderr}, $expected_stderr,
-		"$io_method: $name: expected stderr");
-	$psql->{stderr} = '';
-
-	return $output;
-}
-
-# Issue query, wait for the specified wait event to be reached. If
-# wait_current_session is true, we will wait for the event in the current
-# session, otherwise we'll wait for any session.
-sub query_wait_block
-{
-	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my $io_method = shift;
-	my $node = shift;
-	my $psql = shift;
-	my $name = shift;
-	my $sql = shift;
-	my $waitfor = shift;
-	my $wait_current_session = shift;
-
-	my $pid = $psql->query_safe('SELECT pg_backend_pid()');
-
-	$psql->{stdin} .= qq($sql;\n);
-	$psql->{run}->pump_nb();
-	note "issued sql: $sql;\n";
-	ok(1, "$io_method: $name: issued sql");
-
-	my $waitquery;
-	if ($wait_current_session)
-	{
-		$waitquery =
-		  qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid);
-	}
-	else
-	{
-		$waitquery =
-		  qq(SELECT wait_event FROM pg_stat_activity WHERE wait_event = '$waitfor');
-	}
-
-	note "polling for completion with $waitquery";
-	$node->poll_query_until('postgres', $waitquery, $waitfor);
-	ok(1, "$io_method: $name: observed $waitfor wait event");
-}
-
-# Returns count of checksum failures for the specified database or for shared
-# relations, if $datname is undefined.
-sub checksum_failures
-{
-	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my $psql = shift;
-	my $datname = shift;
-	my $checksum_count;
-	my $checksum_last_failure;
-
-	if (defined $datname)
-	{
-		$checksum_count = $psql->query_safe(
-			qq(
-SELECT checksum_failures FROM pg_stat_database WHERE datname = '$datname';
-));
-		$checksum_last_failure = $psql->query_safe(
-			qq(
-SELECT checksum_last_failure FROM pg_stat_database WHERE datname = '$datname';
-));
-	}
-	else
-	{
-		$checksum_count = $psql->query_safe(
-			qq(
-SELECT checksum_failures FROM pg_stat_database WHERE datname IS NULL;
-));
-		$checksum_last_failure = $psql->query_safe(
-			qq(
-SELECT checksum_last_failure FROM pg_stat_database WHERE datname IS NULL;
-));
-	}
-
-	return $checksum_count, $checksum_last_failure;
-}
-
-###
-# Sub-tests
-###
-
-# Sanity checks for the IO handle API
-sub test_handle
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	# leak warning: implicit xact
-	psql_like(
-		$io_method,
-		$psql,
-		"handle_get() leak in implicit xact",
-		qq(SELECT handle_get()),
-		qr/^$/,
-		qr/leaked AIO handle/,
-		"$io_method: leaky handle_get() warns");
-
-	# leak warning: explicit xact
-	psql_like(
-		$io_method, $psql,
-		"handle_get() leak in explicit xact",
-		qq(BEGIN; SELECT handle_get(); COMMIT),
-		qr/^$/, qr/leaked AIO handle/);
-
-
-	# leak warning: explicit xact, rollback
-	psql_like(
-		$io_method,
-		$psql,
-		"handle_get() leak in explicit xact, rollback",
-		qq(BEGIN; SELECT handle_get(); ROLLBACK;),
-		qr/^$/,
-		qr/leaked AIO handle/);
-
-	# leak warning: subtrans
-	psql_like(
-		$io_method,
-		$psql,
-		"handle_get() leak in subxact",
-		qq(BEGIN; SAVEPOINT foo; SELECT handle_get(); COMMIT;),
-		qr/^$/,
-		qr/leaked AIO handle/);
-
-	# leak warning + error: released in different command (thus resowner)
-	psql_like(
-		$io_method,
-		$psql,
-		"handle_release() in different command",
-		qq(BEGIN; SELECT handle_get(); SELECT handle_release_last(); COMMIT;),
-		qr/^$/,
-		qr/leaked AIO handle.*release in unexpected state/ms);
-
-	# no leak, release in same command
-	psql_like(
-		$io_method,
-		$psql,
-		"handle_release() in same command",
-		qq(BEGIN; SELECT handle_get() UNION ALL SELECT handle_release_last(); COMMIT;),
-		qr/^$/,
-		qr/^$/);
-
-	# normal handle use
-	psql_like($io_method, $psql, "handle_get_release()",
-		qq(SELECT handle_get_release()),
-		qr/^$/, qr/^$/);
-
-	# should error out, API violation
-	psql_like(
-		$io_method,
-		$psql,
-		"handle_get_twice()",
-		qq(SELECT handle_get_twice()),
-		qr/^$/,
-		qr/ERROR:  API violation: Only one IO can be handed out$/);
-
-	# recover after error in implicit xact
-	psql_like(
-		$io_method,
-		$psql,
-		"handle error recovery in implicit xact",
-		qq(SELECT handle_get_and_error(); SELECT 'ok', handle_get_release()),
-		qr/^|ok$/,
-		qr/ERROR.*as you command/);
-
-	# recover after error in implicit xact
-	psql_like(
-		$io_method,
-		$psql,
-		"handle error recovery in explicit xact",
-		qq(BEGIN; SELECT handle_get_and_error(); SELECT handle_get_release(), 'ok'; COMMIT;),
-		qr/^|ok$/,
-		qr/ERROR.*as you command/);
-
-	# recover after error in subtrans
-	psql_like(
-		$io_method,
-		$psql,
-		"handle error recovery in explicit subxact",
-		qq(BEGIN; SAVEPOINT foo; SELECT handle_get_and_error(); ROLLBACK TO SAVEPOINT foo; SELECT handle_get_release(); ROLLBACK;),
-		qr/^|ok$/,
-		qr/ERROR.*as you command/);
-
-	$psql->quit();
-}
-
-# Sanity checks for the batchmode API
-sub test_batchmode
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	# In a build with RELCACHE_FORCE_RELEASE and CATCACHE_FORCE_RELEASE, just
-	# using SELECT batch_start() causes spurious test failures, because the
-	# lookup of the type information when printing the result tuple also
-	# starts a batch. The easiest way around is to not print a result tuple.
-	my $batch_start_sql = qq(SELECT WHERE batch_start() IS NULL);
-
-	# leak warning & recovery: implicit xact
-	psql_like(
-		$io_method,
-		$psql,
-		"batch_start() leak & cleanup in implicit xact",
-		$batch_start_sql,
-		qr/^$/,
-		qr/open AIO batch at end/,
-		"$io_method: leaky batch_start() warns");
-
-	# leak warning & recovery: explicit xact
-	psql_like(
-		$io_method,
-		$psql,
-		"batch_start() leak & cleanup in explicit xact",
-		qq(BEGIN; $batch_start_sql; COMMIT;),
-		qr/^$/,
-		qr/open AIO batch at end/,
-		"$io_method: leaky batch_start() warns");
-
-
-	# leak warning & recovery: explicit xact, rollback
-	#
-	# XXX: This doesn't fail right now, due to not getting a chance to do
-	# something at transaction command commit. That's not a correctness issue,
-	# it just means it's a bit harder to find buggy code.
-	#psql_like($io_method, $psql,
-	#		  "batch_start() leak & cleanup after abort",
-	#		  qq(BEGIN; $batch_start_sql; ROLLBACK;),
-	#		  qr/^$/,
-	#		  qr/open AIO batch at end/, "$io_method: leaky batch_start() warns");
-
-	# no warning, batch closed in same command
-	psql_like(
-		$io_method,
-		$psql,
-		"batch_start(), batch_end() works",
-		qq($batch_start_sql UNION ALL SELECT WHERE batch_end() IS NULL),
-		qr/^$/,
-		qr/^$/,
-		"$io_method: batch_start(), batch_end()");
-
-	$psql->quit();
-}
-
-# Test that simple cases of invalid pages are reported
-sub test_io_error
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	$psql->query_safe(
-		qq(
-CREATE TEMPORARY TABLE tmp_corr(data int not null);
-INSERT INTO tmp_corr SELECT generate_series(1, 10000);
-SELECT modify_rel_block('tmp_corr', 1, corrupt_header=>true);
-));
-
-	foreach my $tblname (qw(tbl_corr tmp_corr))
-	{
-		my $invalid_page_re =
-		  $tblname eq 'tbl_corr'
-		  ? qr/invalid page in block 1 of relation "base\/\d+\/\d+/
-		  : qr/invalid page in block 1 of relation "base\/\d+\/t\d+_\d+/;
-
-		# verify the error is reported in custom C code
-		psql_like(
-			$io_method,
-			$psql,
-			"read_rel_block_ll() of $tblname page",
-			qq(SELECT read_rel_block_ll('$tblname', 1)),
-			qr/^$/,
-			$invalid_page_re);
-
-		# verify the error is reported for bufmgr reads, seq scan
-		psql_like(
-			$io_method, $psql,
-			"sequential scan of $tblname block fails",
-			qq(SELECT count(*) FROM $tblname),
-			qr/^$/, $invalid_page_re);
-
-		# verify the error is reported for bufmgr reads, tid scan
-		psql_like(
-			$io_method,
-			$psql,
-			"tid scan of $tblname block fails",
-			qq(SELECT count(*) FROM $tblname WHERE ctid = '(1, 1)'),
-			qr/^$/,
-			$invalid_page_re);
-	}
-
-	$psql->quit();
-}
-
-# Test interplay between StartBufferIO and TerminateBufferIO
-sub test_startwait_io
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-
-	my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
-	my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
-
-
-	### Verify behavior for normal tables
-
-	# create a buffer we can play around with
-	my $buf_id = psql_like(
-		$io_method, $psql_a,
-		"creation of toy buffer succeeds",
-		qq(SELECT buffer_create_toy('tbl_ok', 1)),
-		qr/^\d+$/, qr/^$/);
-
-	# check that one backend can perform StartBufferIO
-	psql_like(
-		$io_method,
-		$psql_a,
-		"first StartBufferIO",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);),
-		qr/^t$/,
-		qr/^$/);
-
-	# but not twice on the same buffer (non-waiting)
-	psql_like(
-		$io_method,
-		$psql_a,
-		"second StartBufferIO fails, same session",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);),
-		qr/^f$/,
-		qr/^$/);
-	psql_like(
-		$io_method,
-		$psql_b,
-		"second StartBufferIO fails, other session",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);),
-		qr/^f$/,
-		qr/^$/);
-
-	# start io in a different session, will block
-	query_wait_block(
-		$io_method,
-		$node,
-		$psql_b,
-		"blocking start buffer io",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);),
-		"BufferIo",
-		1);
-
-	# Terminate the IO, without marking it as success, this should trigger the
-	# waiting session to be able to start the io
-	psql_like(
-		$io_method,
-		$psql_a,
-		"blocking start buffer io, terminating io, not valid",
-		qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, release_aio=>false)),
-		qr/^$/,
-		qr/^$/);
-
-
-	# Because the IO was terminated, but not marked as valid, second session should get the right to start io
-	pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stdout}, qr/t/);
-	ok(1, "$io_method: blocking start buffer io, can start io");
-
-	# terminate the IO again
-	$psql_b->query_safe(
-		qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, release_aio=>false);)
-	);
-
-
-	# same as the above scenario, but mark IO as having succeeded
-	psql_like(
-		$io_method,
-		$psql_a,
-		"blocking buffer io w/ success: first start buffer io",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);),
-		qr/^t$/,
-		qr/^$/);
-
-	# start io in a different session, will block
-	query_wait_block(
-		$io_method,
-		$node,
-		$psql_b,
-		"blocking start buffer io",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);),
-		"BufferIo",
-		1);
-
-	# Terminate the IO, marking it as success
-	psql_like(
-		$io_method,
-		$psql_a,
-		"blocking start buffer io, terminating io, valid",
-		qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>true, io_error=>false, release_aio=>false)),
-		qr/^$/,
-		qr/^$/);
-
-	# Because the IO was terminated, and marked as valid, second session should complete but not need io
-	pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stdout}, qr/f/);
-	ok(1, "$io_method: blocking start buffer io, no need to start io");
-
-	# buffer is valid now, make it invalid again
-	$psql_a->query_safe(qq(SELECT buffer_create_toy('tbl_ok', 1);));
-
-
-	### Verify behavior for temporary tables
-
-	# Can't unfortunately share the code with the normal table case, there are
-	# too many behavioral differences.
-
-	# create a buffer we can play around with
-	$psql_a->query_safe(
-		qq(
-CREATE TEMPORARY TABLE tmp_ok(data int not null);
-INSERT INTO tmp_ok SELECT generate_series(1, 10000);
-));
-	$buf_id = $psql_a->query_safe(qq(SELECT buffer_create_toy('tmp_ok', 3);));
-
-	# check that one backend can perform StartLocalBufferIO
-	psql_like(
-		$io_method,
-		$psql_a,
-		"first StartLocalBufferIO",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);),
-		qr/^t$/,
-		qr/^$/);
-
-	# Because local buffers don't use IO_IN_PROGRESS, a second StartLocalBufferIO
-	# succeeds as well. This test mostly serves as a documentation of that
-	# fact. If we had actually started IO, it'd be different.
-	psql_like(
-		$io_method,
-		$psql_a,
-		"second StartLocalBufferIO succeeds, same session",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);),
-		qr/^t$/,
-		qr/^$/);
-
-	# Terminate the IO again, without marking it as a success
-	$psql_a->query_safe(
-		qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, release_aio=>false);)
-	);
-	psql_like(
-		$io_method,
-		$psql_a,
-		"StartLocalBufferIO after not marking valid succeeds, same session",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);),
-		qr/^t$/,
-		qr/^$/);
-
-	# Terminate the IO again, marking it as a success
-	$psql_a->query_safe(
-		qq(SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>true, io_error=>false, release_aio=>false);)
-	);
-
-	# Now another StartLocalBufferIO should fail, this time because the buffer
-	# is already valid.
-	psql_like(
-		$io_method,
-		$psql_a,
-		"StartLocalBufferIO after marking valid fails",
-		qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);),
-		qr/^f$/,
-		qr/^$/);
-
-	$psql_a->quit();
-	$psql_b->quit();
-}
-
-# Test that if the backend issuing a read doesn't wait for the IO's
-# completion, another backend can complete the IO
-sub test_complete_foreign
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-
-	my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
-	my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
-
-	# Issue IO without waiting for completion, then sleep
-	$psql_a->query_safe(
-		qq(SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false);));
-
-	# Check that another backend can read the relevant block
-	psql_like(
-		$io_method,
-		$psql_b,
-		"completing read started by sleeping backend",
-		qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1),
-		qr/^1$/,
-		qr/^$/);
-
-	# Issue IO without waiting for completion, then exit.
-	$psql_a->query_safe(
-		qq(SELECT read_rel_block_ll('tbl_ok', 1, wait_complete=>false);));
-	$psql_a->reconnect_and_clear();
-
-	# Check that another backend can read the relevant block. This verifies
-	# that the exiting backend left the AIO in a sane state.
-	psql_like(
-		$io_method,
-		$psql_b,
-		"read buffer started by exited backend",
-		qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1),
-		qr/^1$/,
-		qr/^$/);
-
-	# Read a tbl_corr block, then sleep. The other session will retry the IO
-	# and also fail. The easiest thing to verify that seems to be to check
-	# that both are in the log.
-	my $log_location = -s $node->logfile;
-	$psql_a->query_safe(
-		qq(SELECT read_rel_block_ll('tbl_corr', 1, wait_complete=>false);));
-
-	psql_like(
-		$io_method,
-		$psql_b,
-		"completing read of tbl_corr block started by other backend",
-		qq(SELECT count(*) FROM tbl_corr WHERE ctid = '(1,1)' LIMIT 1),
-		qr/^$/,
-		qr/invalid page in block/);
-
-	# The log message issued for the read_rel_block_ll() should be logged as a LOG
-	$node->wait_for_log(qr/LOG[^\n]+invalid page in/, $log_location);
-	ok(1,
-		"$io_method: completing read of tbl_corr block started by other backend: LOG message for background read"
-	);
-
-	# But for the SELECT, it should be an ERROR
-	$log_location =
-	  $node->wait_for_log(qr/ERROR[^\n]+invalid page in/, $log_location);
-	ok(1,
-		"$io_method: completing read of tbl_corr block started by other backend: ERROR message for foreground read"
-	);
-
-	$psql_a->quit();
-	$psql_b->quit();
-}
-
-# Test that we deal correctly with FDs being closed while IO is in progress
-sub test_close_fd
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	psql_like(
-		$io_method,
-		$psql,
-		"close all FDs after read, waiting for results",
-		qq(
-			SELECT read_rel_block_ll('tbl_ok', 1,
-				wait_complete=>true,
-				batchmode_enter=>true,
-				smgrreleaseall=>true,
-				batchmode_exit=>true
-			);),
-		qr/^$/,
-		qr/^$/);
-
-	psql_like(
-		$io_method,
-		$psql,
-		"close all FDs after read, no waiting",
-		qq(
-			SELECT read_rel_block_ll('tbl_ok', 1,
-				wait_complete=>false,
-				batchmode_enter=>true,
-				smgrreleaseall=>true,
-				batchmode_exit=>true
-			);),
-		qr/^$/,
-		qr/^$/);
-
-	# Check that another backend can read the relevant block
-	psql_like(
-		$io_method,
-		$psql,
-		"close all FDs after read, no waiting, query works",
-		qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(1,1)' LIMIT 1),
-		qr/^1$/,
-		qr/^$/);
-
-	$psql->quit();
-}
-
-# Tests using injection points. Mostly to exercise hard IO errors that are
-# hard to trigger without using injection points.
-sub test_inject
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	# injected what we'd expect
-	$psql->query_safe(qq(SELECT inj_io_short_read_attach(8192);));
-	$psql->query_safe(qq(SELECT invalidate_rel_block('tbl_ok', 2);));
-	psql_like(
-		$io_method, $psql,
-		"injection point not triggering failure",
-		qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(2, 1)'),
-		qr/^1$/, qr/^$/);
-
-	# injected a read shorter than a single block, expecting error
-	$psql->query_safe(qq(SELECT inj_io_short_read_attach(17);));
-	$psql->query_safe(qq(SELECT invalidate_rel_block('tbl_ok', 2);));
-	psql_like(
-		$io_method,
-		$psql,
-		"single block short read fails",
-		qq(SELECT count(*) FROM tbl_ok WHERE ctid = '(2, 1)'),
-		qr/^$/,
-		qr/ERROR:.*could not read blocks 2\.\.2 in file "base\/.*": read only 0 of 8192 bytes/
-	);
-
-	# shorten multi-block read to a single block, should retry
-	my $inval_query = qq(SELECT invalidate_rel_block('tbl_ok', 0);
-SELECT invalidate_rel_block('tbl_ok', 1);
-SELECT invalidate_rel_block('tbl_ok', 2);
-SELECT invalidate_rel_block('tbl_ok', 3);
-/* gap */
-SELECT invalidate_rel_block('tbl_ok', 5);
-SELECT invalidate_rel_block('tbl_ok', 6);
-SELECT invalidate_rel_block('tbl_ok', 7);
-SELECT invalidate_rel_block('tbl_ok', 8););
-
-	$psql->query_safe($inval_query);
-	$psql->query_safe(qq(SELECT inj_io_short_read_attach(8192);));
-	psql_like(
-		$io_method, $psql,
-		"multi block short read (1 block) is retried",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^10000$/, qr/^$/);
-
-	# shorten multi-block read to two blocks, should retry
-	$psql->query_safe($inval_query);
-	$psql->query_safe(qq(SELECT inj_io_short_read_attach(8192*2);));
-
-	psql_like(
-		$io_method, $psql,
-		"multi block short read (2 blocks) is retried",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^10000$/, qr/^$/);
-
-	# verify that page verification errors are detected even as part of a
-	# shortened multi-block read (tbl_corr, block 1 is corrupted)
-	$psql->query_safe(
-		qq(
-SELECT invalidate_rel_block('tbl_corr', 0);
-SELECT invalidate_rel_block('tbl_corr', 1);
-SELECT invalidate_rel_block('tbl_corr', 2);
-SELECT inj_io_short_read_attach(8192);
-	));
-
-	psql_like(
-		$io_method,
-		$psql,
-		"shortened multi-block read detects invalid page",
-		qq(SELECT count(*) FROM tbl_corr WHERE ctid < '(2, 1)'),
-		qr/^$/,
-		qr/ERROR:.*invalid page in block 1 of relation "base\/.*/);
-
-	# trigger a hard error, should error out
-	$psql->query_safe(
-		qq(
-SELECT inj_io_short_read_attach(-errno_from_string('EIO'));
-SELECT invalidate_rel_block('tbl_ok', 2);
-	));
-
-	psql_like(
-		$io_method,
-		$psql,
-		"first hard IO error is reported",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^$/,
-		qr!ERROR:.*could not read blocks 2\.\.2 in file "base/.*": (?:I/O|Input/output) error!
-	);
-
-	psql_like(
-		$io_method,
-		$psql,
-		"second hard IO error is reported",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^$/,
-		qr!ERROR:.*could not read blocks 2\.\.2 in file "base/.*": (?:I/O|Input/output) error!
-	);
-
-	$psql->query_safe(qq(SELECT inj_io_short_read_detach()));
-
-	# now the IO should be ok.
-	psql_like(
-		$io_method, $psql,
-		"recovers after hard error",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^10000$/, qr/^$/);
-
-	# trigger a different hard error, should error out
-	$psql->query_safe(
-		qq(
-SELECT inj_io_short_read_attach(-errno_from_string('EROFS'));
-SELECT invalidate_rel_block('tbl_ok', 2);
-	));
-	psql_like(
-		$io_method,
-		$psql,
-		"different hard IO error is reported",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^$/,
-		qr/ERROR:.*could not read blocks 2\.\.2 in file \"base\/.*\": Read-only file system/
-	);
-	$psql->query_safe(qq(SELECT inj_io_short_read_detach()));
-
-	$psql->quit();
-}
-
-# Tests using injection points, only for io_method=worker.
-#
-# io_method=worker has the special case of needing to reopen files. That can
-# in theory fail, because the file could be gone. That's a hard path to test
-# for real, so we use an injection point to trigger it.
-sub test_inject_worker
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	# trigger a failure to reopen, should error out, but should recover
-	$psql->query_safe(
-		qq(
-SELECT inj_io_reopen_attach();
-SELECT invalidate_rel_block('tbl_ok', 1);
-	));
-
-	psql_like(
-		$io_method,
-		$psql,
-		"failure to open: detected",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^$/,
-		qr/ERROR:.*could not read blocks 1\.\.1 in file "base\/.*": No such file or directory/
-	);
-
-	$psql->query_safe(qq(SELECT inj_io_reopen_detach();));
-
-	# check that we indeed recover
-	psql_like(
-		$io_method, $psql,
-		"failure to open: recovers",
-		qq(SELECT count(*) FROM tbl_ok),
-		qr/^10000$/, qr/^$/);
-
-	$psql->quit();
-}
-
-# Verify that we handle a relation getting removed (due to a rollback or a
-# DROP TABLE) while IO is ongoing for that table.
-sub test_invalidate
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	foreach my $persistency (qw(normal unlogged temporary))
-	{
-		my $sql_persistency = $persistency eq 'normal' ? '' : $persistency;
-		my $tblname = $persistency . '_transactional';
-
-		my $create_sql = qq(
-CREATE $sql_persistency TABLE $tblname (id int not null, data text not null) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO $tblname(id, data) SELECT generate_series(1, 10000) as id, repeat('a', 200);
-);
-
-		# Verify that outstanding read IO does not cause problems with
-		# AbortTransaction -> smgrDoPendingDeletes -> smgrdounlinkall -> ...
-		# -> Invalidate[Local]Buffer.
-		$psql->query_safe("BEGIN; $create_sql;");
-		$psql->query_safe(
-			qq(
-SELECT read_rel_block_ll('$tblname', 1, wait_complete=>false);
-));
-		psql_like(
-			$io_method,
-			$psql,
-			"rollback of newly created $persistency table with outstanding IO",
-			qq(ROLLBACK),
-			qr/^$/,
-			qr/^$/);
-
-		# Verify that outstanding read IO does not cause problems with
-		# CommitTransaction -> smgrDoPendingDeletes -> smgrdounlinkall -> ...
-		# -> Invalidate[Local]Buffer.
-		$psql->query_safe("BEGIN; $create_sql; COMMIT;");
-		$psql->query_safe(
-			qq(
-BEGIN;
-SELECT read_rel_block_ll('$tblname', 1, wait_complete=>false);
-));
-
-		psql_like(
-			$io_method, $psql,
-			"drop $persistency table with outstanding IO",
-			qq(DROP TABLE $tblname),
-			qr/^$/, qr/^$/);
-
-		psql_like($io_method, $psql,
-			"commit after drop $persistency table with outstanding IO",
-			qq(COMMIT), qr/^$/, qr/^$/);
-	}
-
-	$psql->quit();
-}
-
-# Test behavior related to ZERO_ON_ERROR and zero_damaged_pages
-sub test_zero
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
-	my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
-
-	foreach my $persistency (qw(normal temporary))
-	{
-		my $sql_persistency = $persistency eq 'normal' ? '' : $persistency;
-
-		$psql_a->query_safe(
-			qq(
-CREATE $sql_persistency TABLE tbl_zero(id int) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_zero SELECT generate_series(1, 10000);
-));
-
-		$psql_a->query_safe(
-			qq(
-SELECT modify_rel_block('tbl_zero', 0, corrupt_header=>true);
-));
-
-		# Check that page validity errors are detected
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test reading of invalid block 0",
-			qq(
-SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: ERROR:  invalid page in block 0 of relation "base\/.*\/.*$/
-		);
-
-		# Check that page validity errors are zeroed
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test zeroing of invalid block 0",
-			qq(
-SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>true)),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: WARNING:  invalid page in block 0 of relation "base\/.*\/.*"; zeroing out page$/
-		);
-
-		# And that once the corruption is fixed, we can read again
-		$psql_a->query(
-			qq(
-SELECT modify_rel_block('tbl_zero', 0, zero=>true);
-));
-		$psql_a->{stderr} = '';
-
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test re-read of block 0",
-			qq(
-SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)),
-			qr/^$/,
-			qr/^$/);
-
-		# Check a page validity error in another block, to ensure we report
-		# the correct block number
-		$psql_a->query_safe(
-			qq(
-SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true);
-));
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test zeroing of invalid block 3",
-			qq(SELECT read_rel_block_ll('tbl_zero', 3, zero_on_error=>true);),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: WARNING:  invalid page in block 3 of relation "base\/.*\/.*"; zeroing out page$/
-		);
-
-
-		# Check one read reporting multiple invalid blocks
-		$psql_a->query_safe(
-			qq(
-SELECT modify_rel_block('tbl_zero', 2, corrupt_header=>true);
-SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true);
-));
-		# First test error
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test reading of invalid block 2,3 in larger read",
-			qq(SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: ERROR:  2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL:  Block 2 held the first invalid page\.\nHINT:[^\n]+$/
-		);
-
-		# Then test zeroing via ZERO_ON_ERROR flag
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test zeroing of invalid block 2,3 in larger read, ZERO_ON_ERROR",
-			qq(SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>true)),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: WARNING:  zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL:  Block 2 held the first zeroed page\.\nHINT:[^\n]+$/
-		);
-
-		# Then test zeroing via zero_damaged_pages
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: test zeroing of invalid block 2,3 in larger read, zero_damaged_pages",
-			qq(
-BEGIN;
-SET LOCAL zero_damaged_pages = true;
-SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)
-COMMIT;
-),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: WARNING:  zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL:  Block 2 held the first zeroed page\.\nHINT:[^\n]+$/
-		);
-
-		$psql_a->query_safe(qq(COMMIT));
-
-
-		# Verify that bufmgr.c IO detects page validity errors
-		$psql_a->query(
-			qq(
-SELECT invalidate_rel_block('tbl_zero', g.i)
-FROM generate_series(0, 15) g(i);
-SELECT modify_rel_block('tbl_zero', 3, zero=>true);
-));
-		$psql_a->{stderr} = '';
-
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: verify reading zero_damaged_pages=off",
-			qq(
-SELECT count(*) FROM tbl_zero),
-			qr/^$/,
-			qr/^psql:<stdin>:\d+: ERROR:  invalid page in block 2 of relation "base\/.*\/.*$/
-		);
-
-		# Verify that bufmgr.c IO zeroes out pages with page validity errors
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: verify zero_damaged_pages=on",
-			qq(
-BEGIN;
-SET LOCAL zero_damaged_pages = true;
-SELECT count(*) FROM tbl_zero;
-COMMIT;
-),
-			qr/^\d+$/,
-			qr/^psql:<stdin>:\d+: WARNING:  invalid page in block 2 of relation "base\/.*\/.*$/
-		);
-
-		# Check that warnings/errors about page validity in an IO started by
-		# session A that session B might complete aren't logged visibly to
-		# session B.
-		#
-		# This will only ever trigger for io_method's like io_uring, that can
-		# complete IO's in a client backend. But it doesn't seem worth
-		# restricting to that.
-		#
-		# This requires cross-session access to the same relation, hence the
-		# restriction to non-temporary table.
-		if ($sql_persistency ne 'temporary')
-		{
-			# Create a corruption and then read the block without waiting for
-			# completion.
-			$psql_a->query(
-				qq(
-SELECT modify_rel_block('tbl_zero', 1, corrupt_header=>true);
-SELECT read_rel_block_ll('tbl_zero', 1, wait_complete=>false, zero_on_error=>true)
-));
-
-			psql_like(
-				$io_method,
-				$psql_b,
-				"$persistency: test completing read by other session doesn't generate warning",
-				qq(SELECT count(*) > 0 FROM tbl_zero;),
-				qr/^t$/,
-				qr/^$/);
-		}
-
-		# Clean up
-		$psql_a->query_safe(
-			qq(
-DROP TABLE tbl_zero;
-));
-	}
-
-	$psql_a->{stderr} = '';
-
-	$psql_a->quit();
-	$psql_b->quit();
-}
-
-# Test that we detect checksum failures and report them
-sub test_checksum
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
-
-	$psql_a->query_safe(
-		qq(
-CREATE TABLE tbl_normal(id int) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_normal SELECT generate_series(1, 5000);
-SELECT modify_rel_block('tbl_normal', 3, corrupt_checksum=>true);
-
-CREATE TEMPORARY TABLE tbl_temp(id int) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_temp SELECT generate_series(1, 5000);
-SELECT modify_rel_block('tbl_temp', 3, corrupt_checksum=>true);
-SELECT modify_rel_block('tbl_temp', 4, corrupt_checksum=>true);
-));
-
-	# To be able to test checksum failures on shared rels we need a shared rel
-	# with invalid pages - which is a bit scary. pg_shseclabel seems like a
-	# good bet, as it's not accessed in a default configuration.
-	$psql_a->query_safe(
-		qq(
-SELECT grow_rel('pg_shseclabel', 4);
-SELECT modify_rel_block('pg_shseclabel', 2, corrupt_checksum=>true);
-SELECT modify_rel_block('pg_shseclabel', 3, corrupt_checksum=>true);
-));
-
-
-	# Check that page validity errors are detected, checksums stats increase, normal rel
-	my ($cs_count_before, $cs_ts_before) =
-	  checksum_failures($psql_a, 'postgres');
-	psql_like(
-		$io_method,
-		$psql_a,
-		"normal rel: test reading of invalid block 3",
-		qq(
-SELECT read_rel_block_ll('tbl_normal', 3, nblocks=>1, zero_on_error=>false);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: ERROR:  invalid page in block 3 of relation "base\/\d+\/\d+"$/
-	);
-
-	my ($cs_count_after, $cs_ts_after) =
-	  checksum_failures($psql_a, 'postgres');
-
-	cmp_ok($cs_count_before + 1,
-		'<=', $cs_count_after,
-		"$io_method: normal rel: checksum count increased");
-	cmp_ok($cs_ts_after, 'ne', '',
-		"$io_method: normal rel: checksum timestamp is not null");
-
-
-	# Check that page validity errors are detected, checksums stats increase, temp rel
-	($cs_count_after, $cs_ts_after) = checksum_failures($psql_a, 'postgres');
-	psql_like(
-		$io_method,
-		$psql_a,
-		"temp rel: test reading of invalid block 4, valid block 5",
-		qq(
-SELECT read_rel_block_ll('tbl_temp', 4, nblocks=>2, zero_on_error=>false);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: ERROR:  invalid page in block 4 of relation "base\/\d+\/t\d+_\d+"$/
-	);
-
-	($cs_count_after, $cs_ts_after) = checksum_failures($psql_a, 'postgres');
-
-	cmp_ok($cs_count_before + 1,
-		'<=', $cs_count_after,
-		"$io_method: temp rel: checksum count increased");
-	cmp_ok($cs_ts_after, 'ne', '',
-		"$io_method: temp rel: checksum timestamp is not null");
-
-
-	# Check that page validity errors are detected, checksums stats increase, shared rel
-	($cs_count_before, $cs_ts_after) = checksum_failures($psql_a);
-	psql_like(
-		$io_method,
-		$psql_a,
-		"shared rel: reading of invalid blocks 2+3",
-		qq(
-SELECT read_rel_block_ll('pg_shseclabel', 2, nblocks=>2, zero_on_error=>false);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: ERROR:  2 invalid pages among blocks 2..3 of relation "global\/\d+"\nDETAIL:  Block 2 held the first invalid page\.\nHINT:[^\n]+$/
-	);
-
-	($cs_count_after, $cs_ts_after) = checksum_failures($psql_a);
-
-	cmp_ok($cs_count_before + 1,
-		'<=', $cs_count_after,
-		"$io_method: shared rel: checksum count increased");
-	cmp_ok($cs_ts_after, 'ne', '',
-		"$io_method: shared rel: checksum timestamp is not null");
-
-
-	# and restore sanity
-	$psql_a->query(
-		qq(
-SELECT modify_rel_block('pg_shseclabel', 1, zero=>true);
-DROP TABLE tbl_normal;
-));
-	$psql_a->{stderr} = '';
-
-	$psql_a->quit();
-}
-
-# Verify checksum handling when creating database from a database with an
-# invalid block. This also serves as a minimal check that cross-database IO is
-# handled reasonably.
-sub test_checksum_createdb
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	$node->safe_psql('postgres',
-		'CREATE DATABASE regression_createdb_source');
-
-	$node->safe_psql(
-		'regression_createdb_source', qq(
-CREATE EXTENSION test_aio;
-CREATE TABLE tbl_cs_fail(data int not null) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_cs_fail SELECT generate_series(1, 1000);
-SELECT modify_rel_block('tbl_cs_fail', 1, corrupt_checksum=>true);
-));
-
-	my $createdb_sql = qq(
-CREATE DATABASE regression_createdb_target
-TEMPLATE regression_createdb_source
-STRATEGY wal_log;
-);
-
-	# Verify that CREATE DATABASE of an invalid database fails and is
-	# accounted for accurately.
-	#
-	# Note: On windows additional WARNING messages might be printed, due to
-	# "some useless files may be left behind" warnings. While we probably
-	# should prevent those from occurring, they're independent of AIO, so we
-	# shouldn't fail because of them here.
-	my ($cs_count_before, $cs_ts_before) =
-	  checksum_failures($psql, 'regression_createdb_source');
-	psql_like(
-		$io_method,
-		$psql,
-		"create database w/ wal strategy, invalid source",
-		$createdb_sql,
-		qr/^$/,
-		qr/psql:<stdin>:\d+: ERROR:  invalid page in block 1 of relation "base\/\d+\/\d+"$/
-	);
-	my ($cs_count_after, $cs_ts_after) =
-	  checksum_failures($psql, 'regression_createdb_source');
-	cmp_ok($cs_count_before + 1, '<=', $cs_count_after,
-		"$io_method: create database w/ wal strategy, invalid source: checksum count increased"
-	);
-
-	# Verify that CREATE DATABASE of the fixed database succeeds.
-	$node->safe_psql(
-		'regression_createdb_source', qq(
-SELECT modify_rel_block('tbl_cs_fail', 1, zero=>true);
-));
-	psql_like($io_method, $psql,
-		"create database w/ wal strategy, valid source",
-		$createdb_sql, qr/^$/, qr/^$/);
-
-	$psql->quit();
-}
-
-# Test that we detect checksum failures and report them
-#
-# In several places we make sure that the server log actually contains
-# individual information for each block involved in the IO.
-sub test_ignore_checksum
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql = $node->background_psql('postgres', on_error_stop => 0);
-
-	# Test setup
-	$psql->query_safe(
-		qq(
-CREATE TABLE tbl_cs_fail(id int) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_cs_fail SELECT generate_series(1, 10000);
-));
-
-	my $count_sql = "SELECT count(*) FROM tbl_cs_fail";
-	my $invalidate_sql = qq(
-SELECT invalidate_rel_block('tbl_cs_fail', g.i)
-FROM generate_series(0, 6) g(i);
-);
-
-	my $expect = $psql->query_safe($count_sql);
-
-
-	# Very basic tests for ignore_checksum_failure=off / on
-
-	$psql->query_safe(
-		qq(
-SELECT modify_rel_block('tbl_cs_fail', 1, corrupt_checksum=>true);
-SELECT modify_rel_block('tbl_cs_fail', 5, corrupt_checksum=>true);
-SELECT modify_rel_block('tbl_cs_fail', 6, corrupt_checksum=>true);
-));
-
-	$psql->query_safe($invalidate_sql);
-	psql_like(
-		$io_method,
-		$psql,
-		"reading block w/ wrong checksum with ignore_checksum_failure=off fails",
-		$count_sql,
-		qr/^$/,
-		qr/ERROR:  invalid page in block/);
-
-	$psql->query_safe("SET ignore_checksum_failure=on");
-
-	$psql->query_safe($invalidate_sql);
-	psql_like(
-		$io_method,
-		$psql,
-		"reading block w/ wrong checksum with ignore_checksum_failure=off succeeds",
-		$count_sql,
-		qr/^$expect$/,
-		qr/WARNING:  ignoring (checksum failure|\d checksum failures)/);
-
-
-	# Verify that ignore_checksum_failure=off works in multi-block reads
-
-	$psql->query_safe(
-		qq(
-SELECT modify_rel_block('tbl_cs_fail', 2, zero=>true);
-SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true);
-SELECT modify_rel_block('tbl_cs_fail', 4, corrupt_header=>true);
-));
-
-	my $log_location = -s $node->logfile;
-	psql_like(
-		$io_method,
-		$psql,
-		"test reading of checksum failed block 3, with ignore",
-		qq(
-SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: WARNING:  ignoring checksum failure in block 3/
-	);
-
-	# Check that the log contains a LOG message about the failure
-	$log_location =
-	  $node->wait_for_log(qr/LOG:  ignoring checksum failure/, $log_location);
-
-	# check that we error
-	psql_like(
-		$io_method,
-		$psql,
-		"test reading of valid block 2, checksum failed 3, invalid 4, zero=false with ignore",
-		qq(
-SELECT read_rel_block_ll('tbl_cs_fail', 2, nblocks=>3, zero_on_error=>false);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: ERROR:  invalid page in block 4 of relation "base\/\d+\/\d+"$/
-	);
-
-	# Test multi-block read with different problems in different blocks
-	$psql->query(
-		qq(
-SELECT modify_rel_block('tbl_cs_fail', 1, zero=>true);
-SELECT modify_rel_block('tbl_cs_fail', 2, corrupt_checksum=>true);
-SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header=>true);
-SELECT modify_rel_block('tbl_cs_fail', 4, corrupt_header=>true);
-SELECT modify_rel_block('tbl_cs_fail', 5, corrupt_header=>true);
-));
-	$psql->{stderr} = '';
-
-	$log_location = -s $node->logfile;
-	psql_like(
-		$io_method,
-		$psql,
-		"test reading of valid block 1, checksum failed 2, 3, invalid 3-5, zero=true",
-		qq(
-SELECT read_rel_block_ll('tbl_cs_fail', 1, nblocks=>5, zero_on_error=>true);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: WARNING:  zeroing 3 page\(s\) and ignoring 2 checksum failure\(s\) among blocks 1..5 of relation "/
-	);
-
-
-	# Unfortunately have to scan the whole log since determining $log_location
-	# above in each of the tests, as wait_for_log() returns the size of the
-	# file.
-
-	$node->wait_for_log(qr/LOG:  ignoring checksum failure in block 2/,
-		$log_location);
-	ok(1, "$io_method: found information about checksum failure in block 2");
-
-	$node->wait_for_log(
-		qr/LOG:  invalid page in block 3 of relation "base.*"; zeroing out page/,
-		$log_location);
-	ok(1, "$io_method: found information about invalid page in block 3");
-
-	$node->wait_for_log(
-		qr/LOG:  invalid page in block 4 of relation "base.*"; zeroing out page/,
-		$log_location);
-	ok(1, "$io_method: found information about checksum failure in block 4");
-
-	$node->wait_for_log(
-		qr/LOG:  invalid page in block 5 of relation "base.*"; zeroing out page/,
-		$log_location);
-	ok(1, "$io_method: found information about checksum failure in block 5");
-
-
-	# Reading a page with both an invalid header and an invalid checksum
-	$psql->query(
-		qq(
-SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header=>true);
-));
-	$psql->{stderr} = '';
-
-	psql_like(
-		$io_method,
-		$psql,
-		"test reading of block with both invalid header and invalid checksum, zero=false",
-		qq(
-SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: ERROR:  invalid page in block 3 of relation "/);
-
-	psql_like(
-		$io_method,
-		$psql,
-		"test reading of block 3 with both invalid header and invalid checksum, zero=true",
-		qq(
-SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>true);),
-		qr/^$/,
-		qr/^psql:<stdin>:\d+: WARNING:  invalid page in block 3 of relation "base\/.*"; zeroing out page/
-	);
-
-
-	$psql->quit();
-}
-
-
-# Tests for StartReadBuffers()
-sub test_read_buffers
-{
-	my $io_method = shift;
-	my $node = shift;
-	my ($ret, $output);
-	my $table;
-
-	my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
-	my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
-
-	$psql_a->query_safe(
-		qq(
-CREATE TEMPORARY TABLE tmp_ok(data int not null);
-INSERT INTO tmp_ok SELECT generate_series(1, 5000);
-));
-
-	foreach my $persistency (qw(normal temporary))
-	{
-		$table = $persistency eq 'normal' ? 'tbl_ok' : 'tmp_ok';
-
-		# check that consecutive misses are combined into one read
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, combine, block 0-1",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|,
-			qr/^0\|0\|t\|2$/,
-			qr/^$/);
-
-		# but if we do it again, i.e. it's in the buffer pool, there will be
-		# two operations
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, doesn't combine hits, block 0-1",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|,
-			qr/^0\|0\|f\|1\n1\|1\|f\|1$/,
-			qr/^$/);
-
-		# Check that a larger read interrupted by a hit works
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, prep, block 3",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 3, 1)|,
-			qr/^0\|3\|t\|1$/,
-			qr/^$/);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, interrupted by hit on 3, block 2-5",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 2, 4)|,
-			qr/^0\|2\|t\|1\n1\|3\|f\|1\n2\|4\|t\|2$/,
-			qr/^$/);
-
-
-		# Verify that a read with an initial buffer hit works
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, miss, block 0",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 1)|,
-			qr/^0\|0\|t\|1$/,
-			qr/^$/);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, hit, block 0",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 1)|,
-			qr/^0\|0\|f\|1$/,
-			qr/^$/);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, miss, block 1",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 1)|,
-			qr/^0\|1\|t\|1$/,
-			qr/^$/);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, hit, block 1",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 1)|,
-			qr/^0\|1\|f\|1$/,
-			qr/^$/);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, hit, block 0-1",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|,
-			qr/^0\|0\|f\|1\n1\|1\|f\|1$/,
-			qr/^$/);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, hit 0-1, miss 2",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 3)|,
-			qr/^0\|0\|f\|1\n1\|1\|f\|1\n2\|2\|t\|1$/,
-			qr/^$/);
-
-		# Verify that a read with an initial miss and trailing buffer hit(s) works
-		$psql_a->query_safe(qq|SELECT invalidate_rel_block('$table', 0)|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, miss 0, hit 1-2",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 3)|,
-			qr/^0\|0\|t\|1\n1\|1\|f\|1\n2\|2\|f\|1$/,
-			qr/^$/);
-		$psql_a->query_safe(qq|SELECT invalidate_rel_block('$table', 1)|);
-		$psql_a->query_safe(qq|SELECT invalidate_rel_block('$table', 2)|);
-		$psql_a->query_safe(qq|SELECT * FROM read_buffers('$table', 3, 2)|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, miss 1-2, hit 3-4",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 4)|,
-			qr/^0\|1\|t\|2\n2\|3\|f\|1\n3\|4\|f\|1$/,
-			qr/^$/);
-
-		# Verify that we aren't doing reads larger than
-		# io_combine_limit. That's just enforced in read_buffers() function,
-		# but kinda still worth testing.
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		$psql_a->query_safe(qq|SET io_combine_limit=3|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, io_combine_limit has effect",
-			qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 5)|,
-			qr/^0\|1\|t\|3\n3\|4\|t\|2$/,
-			qr/^$/);
-		$psql_a->query_safe(qq|RESET io_combine_limit|);
-
-
-		# Test encountering buffer IO we started in the first block of the
-		# range.
-		#
-		# Depending on how quick the IO we start completes, the IO might be
-		# completed or we "join" the foreign IO. To hide that variability, the
-		# query below treats a foreign IO as not having needed to do IO.
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		$psql_a->query_safe(
-			qq|SELECT read_rel_block_ll('$table', 1, wait_complete=>false)|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, in-progress 1, read 1-3",
-			qq|SELECT blockoff, blocknum, io_reqd and not foreign_io, nblocks FROM read_buffers('$table', 1, 3)|,
-			qr/^0\|1\|f\|1\n1\|2\|t\|2$/,
-			qr/^$/);
-
-		# Test in-progress IO in the middle block of the range
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		$psql_a->query_safe(
-			qq|SELECT read_rel_block_ll('$table', 2, wait_complete=>false)|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, in-progress 2, read 1-3",
-			qq|SELECT blockoff, blocknum, io_reqd and not foreign_io, nblocks FROM read_buffers('$table', 1, 3)|,
-			qr/^0\|1\|t\|1\n1\|2\|f\|1\n2\|3\|t\|1$/,
-			qr/^$/);
-
-		# Test in-progress IO on the last block of the range
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		$psql_a->query_safe(
-			qq|SELECT read_rel_block_ll('$table', 3, wait_complete=>false)|);
-		psql_like(
-			$io_method,
-			$psql_a,
-			"$persistency: read buffers, in-progress 3, read 1-3",
-			qq|SELECT blockoff, blocknum, io_reqd and not foreign_io, nblocks FROM read_buffers('$table', 1, 3)|,
-			qr/^0\|1\|t\|2\n2\|3\|f\|1$/,
-			qr/^$/);
-	}
-
-	# The remaining tests don't make sense for temp tables, as they are
-	# concerned with multiple sessions interacting with each other.
-	$table = 'tbl_ok';
-	my $persistency = 'normal';
-
-	# Test start buffer IO will split IO if there's IO in progress. We can't
-	# observe this with sync, as that does not start the IO operation in
-	# StartReadBuffers().
-	if ($io_method ne 'sync')
-	{
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-
-		my $buf_id =
-		  $psql_b->query_safe(qq|SELECT buffer_create_toy('$table', 3)|);
-		$psql_b->query_safe(
-			qq|SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true)|
-		);
-
-		query_wait_block(
-			$io_method,
-			$node,
-			$psql_a,
-			"$persistency: read buffers blocks waiting for concurrent IO",
-			qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM read_buffers('$table', 1, 5);\n|,
-			"BufferIo",
-			1);
-		$psql_b->query_safe(
-			qq|SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, release_aio=>false)|
-		);
-		# Because no IO wref was assigned, block 3 should not report foreign IO
-		pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
-			qr/0\|1\|t\|f\|2\n2\|3\|t\|f\|3/);
-		ok(1,
-			"$io_method: $persistency: IO was split due to concurrent failed IO"
-		);
-
-		# Same as before, except the concurrent IO succeeds this time
-		$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-		$buf_id =
-		  $psql_b->query_safe(qq|SELECT buffer_create_toy('$table', 3)|);
-		$psql_b->query_safe(
-			qq|SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true)|
-		);
-
-		query_wait_block(
-			$io_method,
-			$node,
-			$psql_a,
-			"$persistency: read buffers blocks waiting for concurrent IO",
-			qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM read_buffers('$table', 1, 5);\n|,
-			"BufferIo",
-			1);
-		$psql_b->query_safe(
-			qq|SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>true, io_error=>false, release_aio=>false)|
-		);
-		# Because no IO wref was assigned, block 3 should not report foreign IO
-		pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
-			qr/0\|1\|t\|f\|2\n2\|3\|f\|f\|1\n3\|4\|t\|f\|2/);
-		ok(1,
-			"$io_method: $persistency: IO was split due to concurrent successful IO"
-		);
-	}
-
-	$psql_a->quit();
-	$psql_b->quit();
-}
-
-
-# Tests for StartReadBuffers() that depend on injection point support
-sub test_read_buffers_inject
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
-	my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
-	my $psql_c = $node->background_psql('postgres', on_error_stop => 0);
-
-	my $expected;
-
-	# We can't easily test waiting for foreign IOs on temporary tables, as the
-	# waiting in the completion hook will just stall the backend. For worker
-	# that is because temporary table IO is executed synchronously, for
-	# io_uring the completion will be executed in the same process, but due to
-	# temporary tables not being shared, we can't do the wait in another
-	# backend.
-	my $table = 'tbl_ok';
-	my $persistency = 'normal';
-
-
-	###
-	# Test if a read buffers encounters AIO in progress by another backend, it
-	# recognizes that other IO as a foreign IO.
-	###
-	$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-
-	# B: Trigger wait in the next AIO read for block 1.
-	$psql_b->query_safe(
-		qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(),
-		   relfilenode=>pg_relation_filenode('$table'),
-		   blockno=>1);/);
-	ok(1,
-		"$io_method: $persistency: configure wait in completion of block 1");
-
-	# B: Read block 1 and wait for the completion hook to be reached (which could
-	# be in B itself or in an IO worker)
-	query_wait_block(
-		$io_method,
-		$node,
-		$psql_b,
-		"$persistency: wait in completion of block 1",
-		qq|SELECT read_rel_block_ll('$table', blockno=>1, nblocks=>1)|,
-		'completion_wait',
-		0);
-
-	# A: Start read, wait until we're waiting for IO completion
-	query_wait_block(
-		$io_method,
-		$node,
-		$psql_a,
-		"$persistency: read 1-4, blocked on in-progress 1",
-		qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM read_buffers('$table', 1, 4)|,
-		"AioIoCompletion",
-		1);
-
-	# C: Release B from completion hook
-	$psql_c->query_safe(qq|SELECT inj_io_completion_continue()|);
-	ok(1, "$io_method: $persistency: continued completion of block 1");
-
-	# A: Check that we recognized the foreign IO wait, if possible
-	#
-	# Due to sync mode not actually issuing IO below StartReadBuffers(), we
-	# can't observe encountering foreign IO. It still seems worth exercising these
-	# paths however.
-	if ($io_method ne 'sync')
-	{
-		# A foreign IO covering block 1, and one IO covering blocks 2-4.
-		$expected = qr/0\|1\|t\|t\|1\n1\|2\|t\|f\|3/;
-	}
-	else
-	{
-		# One IO covering everything, as that's what StartReadBuffers() will
-		# return for something with misses in sync mode.
-		$expected = qr/0\|1\|t\|f\|4/;
-	}
-	pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
-		$expected);
-	ok(1,
-		"$io_method: $persistency: read 1-3, blocked on in-progress 1, see expected result"
-	);
-	$psql_a->{stdout} = '';
-
-
-	###
-	# Test if a read buffers encounters AIO in progress by another backend, it
-	# recognizes that other IO as a foreign IO. This time we encounter the
-	# foreign IO multiple times.
-	###
-	$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
-
-	# B: Trigger wait in the next AIO read for block 3.
-	$psql_b->query_safe(
-		qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(),
-		   relfilenode=>pg_relation_filenode('$table'),
-		   blockno=>3);/);
-	ok(1,
-		"$io_method: $persistency: configure wait in completion of block 3");
-
-	# B: Read block 2-3 and wait for the completion hook to be reached (which
-	# could be in B itself or in an IO worker)
-	query_wait_block(
-		$io_method,
-		$node,
-		$psql_b,
-		"$persistency: wait in completion of block 2+3",
-		qq|SELECT read_rel_block_ll('$table', blockno=>2, nblocks=>2)|,
-		'completion_wait',
-		0);
-
-	# A: Start read, wait until we're waiting for IO completion
-	#
-	# Note that we need to defer waiting for IO until the end of
-	# read_buffers(), to be able to see that the IO on 3 is still in progress.
-	query_wait_block(
-		$io_method,
-		$node,
-		$psql_a,
-		"$persistency: read 0-3, blocked on in-progress 2+3",
-		qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM
-read_buffers('$table', 0, 4)|,
-		"AioIoCompletion", 1);
-
-	# C: Release B from completion hook
-	$psql_c->query_safe(qq|SELECT inj_io_completion_continue()|);
-	ok(1, "$io_method: $persistency: continued completion of block 2+3");
-
-	# A: Check that we recognized the foreign IO wait, if possible
-	#
-	# See comment further up about sync mode.
-	if ($io_method ne 'sync')
-	{
-		# One IO covering blocks 0-1, A foreign IO covering block 2, and a
-		# foreign IO covering block 3 (same wref as for block 2).
-		$expected = qr/0\|0\|t\|f\|2\n2\|2\|t\|t\|1\n3\|3\|t\|t\|1/;
-	}
-	else
-	{
-		# One IO covering everything, as that's what StartReadBuffers() will
-		# return for something with misses in sync mode.
-		$expected = qr/0\|0\|t\|f\|4/;
-	}
-	pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
-		$expected);
-	ok(1,
-		"$io_method: $persistency: read 0-3, blocked on in-progress 2+3, see expected result"
-	);
-	$psql_a->{stdout} = '';
-
-
-	$psql_a->quit();
-	$psql_b->quit();
-	$psql_c->quit();
-}
-
-# Run all tests that for the specified node / io_method
-sub test_io_method
-{
-	my $io_method = shift;
-	my $node = shift;
-
-	is($node->safe_psql('postgres', 'SHOW io_method'),
-		$io_method, "$io_method: io_method set correctly");
-
-	$node->safe_psql(
-		'postgres', qq(
-CREATE EXTENSION test_aio;
-CREATE TABLE tbl_corr(data int not null) WITH (AUTOVACUUM_ENABLED = false);
-CREATE TABLE tbl_ok(data int not null) WITH (AUTOVACUUM_ENABLED = false);
-
-INSERT INTO tbl_corr SELECT generate_series(1, 10000);
-INSERT INTO tbl_ok SELECT generate_series(1, 10000);
-SELECT grow_rel('tbl_corr', 16);
-SELECT grow_rel('tbl_ok', 16);
-
-SELECT modify_rel_block('tbl_corr', 1, corrupt_header=>true);
-CHECKPOINT;
-));
-
-	test_handle($io_method, $node);
-	test_io_error($io_method, $node);
-	test_batchmode($io_method, $node);
-	test_startwait_io($io_method, $node);
-	test_complete_foreign($io_method, $node);
-	test_close_fd($io_method, $node);
-	test_invalidate($io_method, $node);
-	test_zero($io_method, $node);
-	test_checksum($io_method, $node);
-	test_ignore_checksum($io_method, $node);
-	test_checksum_createdb($io_method, $node);
-	test_read_buffers($io_method, $node);
-
-	# generic injection tests
-  SKIP:
-	{
-		skip 'Injection points not supported by this build', 1
-		  unless $ENV{enable_injection_points} eq 'yes';
-		test_inject($io_method, $node);
-		test_read_buffers_inject($io_method, $node);
-	}
-
-	# worker specific injection tests
-	if ($io_method eq 'worker')
-	{
-	  SKIP:
-		{
-			skip 'Injection points not supported by this build', 1
-			  unless $ENV{enable_injection_points} eq 'yes';
-
-			test_inject_worker($io_method, $node);
-		}
-	}
-}
-- 
2.54.0

