From 16b7927775007b6dcbb40a8d86d4f7c9dd842cb5 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Wed, 5 Nov 2025 15:54:11 +0300
Subject: [PATCH v1 2/2] Convert killtuples isolation test to TAP test

This patch converts isolation test to TAP test for exercising the
recovery path. There are no implementation changes except covering redo
delete case for the gist index by re-inserting to the table.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/aKJsWedftW7UX1WM%40paquier.xyz
---
 src/test/modules/index/.gitignore             |   4 -
 src/test/modules/index/Makefile               |   3 +-
 .../modules/index/expected/killtuples.out     | 355 ------------------
 src/test/modules/index/meson.build            |   6 +-
 src/test/modules/index/specs/killtuples.spec  | 127 -------
 src/test/modules/index/t/001_killtuples.pl    | 343 +++++++++++++++++
 6 files changed, 347 insertions(+), 491 deletions(-)
 delete mode 100644 src/test/modules/index/expected/killtuples.out
 delete mode 100644 src/test/modules/index/specs/killtuples.spec
 create mode 100644 src/test/modules/index/t/001_killtuples.pl

diff --git a/src/test/modules/index/.gitignore b/src/test/modules/index/.gitignore
index b4903eba657..716e17f5a2a 100644
--- a/src/test/modules/index/.gitignore
+++ b/src/test/modules/index/.gitignore
@@ -1,6 +1,2 @@
 # Generated subdirectories
-/log/
-/results/
-/output_iso/
 /tmp_check/
-/tmp_check_iso/
diff --git a/src/test/modules/index/Makefile b/src/test/modules/index/Makefile
index 29047044ede..5dc97453cfa 100644
--- a/src/test/modules/index/Makefile
+++ b/src/test/modules/index/Makefile
@@ -1,8 +1,7 @@
 # src/test/modules/index/Makefile
 
 EXTRA_INSTALL = contrib/btree_gin contrib/btree_gist
-
-ISOLATION = killtuples
+TAP_TESTS = 1
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
diff --git a/src/test/modules/index/expected/killtuples.out b/src/test/modules/index/expected/killtuples.out
deleted file mode 100644
index be7ddd756ef..00000000000
--- a/src/test/modules/index/expected/killtuples.out
+++ /dev/null
@@ -1,355 +0,0 @@
-Parsed test spec with 1 sessions
-
-starting permutation: create_table fill_500 create_btree flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table
-step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null);
-step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i);
-step create_btree: CREATE INDEX kill_prior_tuple_btree ON kill_prior_tuple USING btree (key);
-step flush: SELECT FROM pg_stat_force_next_flush();
-step disable_seq: SET enable_seqscan = false;
-step disable_bitmap: SET enable_bitmapscan = false;
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                            
---------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                               
-  Index Searches: 1                                                                   
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                            
---------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                               
-  Index Searches: 1                                                                   
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step delete: DELETE FROM kill_prior_tuple;
-step flush: SELECT FROM pg_stat_force_next_flush();
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                            
---------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                               
-  Index Searches: 1                                                                   
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                            
---------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                               
-  Index Searches: 1                                                                   
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                0
-(1 row)
-
-step drop_table: DROP TABLE IF EXISTS kill_prior_tuple;
-
-starting permutation: create_table fill_500 create_ext_btree_gist create_gist flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table drop_ext_btree_gist
-step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null);
-step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i);
-step create_ext_btree_gist: CREATE EXTENSION btree_gist;
-step create_gist: CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key);
-step flush: SELECT FROM pg_stat_force_next_flush();
-step disable_seq: SET enable_seqscan = false;
-step disable_bitmap: SET enable_bitmapscan = false;
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step delete: DELETE FROM kill_prior_tuple;
-step flush: SELECT FROM pg_stat_force_next_flush();
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                0
-(1 row)
-
-step drop_table: DROP TABLE IF EXISTS kill_prior_tuple;
-step drop_ext_btree_gist: DROP EXTENSION btree_gist;
-
-starting permutation: create_table fill_10 create_ext_btree_gist create_gist flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table drop_ext_btree_gist
-step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null);
-step fill_10: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 10) g(i);
-step create_ext_btree_gist: CREATE EXTENSION btree_gist;
-step create_gist: CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key);
-step flush: SELECT FROM pg_stat_force_next_flush();
-step disable_seq: SET enable_seqscan = false;
-step disable_bitmap: SET enable_bitmapscan = false;
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step delete: DELETE FROM kill_prior_tuple;
-step flush: SELECT FROM pg_stat_force_next_flush();
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step drop_table: DROP TABLE IF EXISTS kill_prior_tuple;
-step drop_ext_btree_gist: DROP EXTENSION btree_gist;
-
-starting permutation: create_table fill_500 create_hash flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table
-step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null);
-step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i);
-step create_hash: CREATE INDEX kill_prior_tuple_hash ON kill_prior_tuple USING hash (key);
-step flush: SELECT FROM pg_stat_force_next_flush();
-step disable_seq: SET enable_seqscan = false;
-step disable_bitmap: SET enable_bitmapscan = false;
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=1.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step delete: DELETE FROM kill_prior_tuple;
-step flush: SELECT FROM pg_stat_force_next_flush();
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                           
--------------------------------------------------------------------------------------
-Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=0.00 loops=1)
-  Index Cond: (key = 1)                                                              
-  Index Searches: 1                                                                  
-(3 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                0
-(1 row)
-
-step drop_table: DROP TABLE IF EXISTS kill_prior_tuple;
-
-starting permutation: create_table fill_500 create_ext_btree_gin create_gin flush disable_seq delete flush measure access flush result measure access flush result drop_table drop_ext_btree_gin
-step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null);
-step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i);
-step create_ext_btree_gin: CREATE EXTENSION btree_gin;
-step create_gin: CREATE INDEX kill_prior_tuple_gin ON kill_prior_tuple USING gin (key);
-step flush: SELECT FROM pg_stat_force_next_flush();
-step disable_seq: SET enable_seqscan = false;
-step delete: DELETE FROM kill_prior_tuple;
-step flush: SELECT FROM pg_stat_force_next_flush();
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                
---------------------------------------------------------------------------
-Bitmap Heap Scan on kill_prior_tuple (actual rows=0.00 loops=1)           
-  Recheck Cond: (key = 1)                                                 
-  Heap Blocks: exact=1                                                    
-  ->  Bitmap Index Scan on kill_prior_tuple_gin (actual rows=1.00 loops=1)
-        Index Cond: (key = 1)                                             
-        Index Searches: 1                                                 
-(6 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
-step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
-QUERY PLAN                                                                
---------------------------------------------------------------------------
-Bitmap Heap Scan on kill_prior_tuple (actual rows=0.00 loops=1)           
-  Recheck Cond: (key = 1)                                                 
-  Heap Blocks: exact=1                                                    
-  ->  Bitmap Index Scan on kill_prior_tuple_gin (actual rows=1.00 loops=1)
-        Index Cond: (key = 1)                                             
-        Index Searches: 1                                                 
-(6 rows)
-
-step flush: SELECT FROM pg_stat_force_next_flush();
-step result: SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
-new_heap_accesses
------------------
-                1
-(1 row)
-
-step drop_table: DROP TABLE IF EXISTS kill_prior_tuple;
-step drop_ext_btree_gin: DROP EXTENSION btree_gin;
diff --git a/src/test/modules/index/meson.build b/src/test/modules/index/meson.build
index 15f3734567a..87b6318dcae 100644
--- a/src/test/modules/index/meson.build
+++ b/src/test/modules/index/meson.build
@@ -4,9 +4,9 @@ tests += {
   'name': 'index',
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
-  'isolation': {
-    'specs': [
-      'killtuples',
+  'tap': {
+    'tests': [
+      't/001_killtuples.pl',
     ],
   },
 }
diff --git a/src/test/modules/index/specs/killtuples.spec b/src/test/modules/index/specs/killtuples.spec
deleted file mode 100644
index 77fe8c689a7..00000000000
--- a/src/test/modules/index/specs/killtuples.spec
+++ /dev/null
@@ -1,127 +0,0 @@
-# Basic testing of killtuples / kill_prior_tuples / all_dead testing
-# for various index AMs
-#
-# This tests just enough to ensure that the kill* routines are actually
-# executed and does something approximately reasonable. It's *not* sufficient
-# testing for adding killitems support to a new AM!
-#
-# This doesn't really need to be an isolation test, it could be written as a
-# regular regression test. However, writing it as an isolation test ends up a
-# *lot* less verbose.
-
-setup
-{
-    CREATE TABLE counter(heap_accesses int);
-    INSERT INTO counter(heap_accesses) VALUES (0);
-}
-
-teardown
-{
-    DROP TABLE counter;
-}
-
-session s1
-# to ensure GUCs are reset
-setup { RESET ALL; }
-
-step disable_seq { SET enable_seqscan = false; }
-
-step disable_bitmap { SET enable_bitmapscan = false; }
-
-# use a temporary table to make sure no other session can interfere with
-# visibility determinations
-step create_table { CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); }
-
-step fill_10 { INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 10) g(i); }
-
-step fill_500 { INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i); }
-
-# column-less select to make output easier to read
-step flush { SELECT FROM pg_stat_force_next_flush(); }
-
-step measure { UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); }
-
-step result { SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses AS new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; }
-
-step access { EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; }
-
-step delete { DELETE FROM kill_prior_tuple; }
-
-step drop_table { DROP TABLE IF EXISTS kill_prior_tuple; }
-
-### steps for testing btree indexes ###
-step create_btree { CREATE INDEX kill_prior_tuple_btree ON kill_prior_tuple USING btree (key); }
-
-### steps for testing gist indexes ###
-# Creating the extensions takes time, so we don't want to do so when testing
-# other AMs
-step create_ext_btree_gist { CREATE EXTENSION btree_gist; }
-step drop_ext_btree_gist { DROP EXTENSION btree_gist; }
-step create_gist { CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key); }
-
-### steps for testing gin indexes ###
-# See create_ext_btree_gist
-step create_ext_btree_gin { CREATE EXTENSION btree_gin; }
-step drop_ext_btree_gin { DROP EXTENSION btree_gin; }
-step create_gin { CREATE INDEX kill_prior_tuple_gin ON kill_prior_tuple USING gin (key); }
-
-### steps for testing hash indexes ###
-step create_hash { CREATE INDEX kill_prior_tuple_hash ON kill_prior_tuple USING hash (key); }
-
-
-# test killtuples with btree index
-permutation
-  create_table fill_500 create_btree flush
-  disable_seq disable_bitmap
-  # show each access to non-deleted tuple increments heap_blks_*
-  measure access flush result
-  measure access flush result
-  delete flush
-  # first access after accessing deleted tuple still needs to access heap
-  measure access flush result
-  # but after kill_prior_tuple did its thing, we shouldn't access heap anymore
-  measure access flush result
-  drop_table
-
-# Same as first permutation, except testing gist
-permutation
-  create_table fill_500 create_ext_btree_gist create_gist flush
-  disable_seq disable_bitmap
-  measure access flush result
-  measure access flush result
-  delete flush
-  measure access flush result
-  measure access flush result
-  drop_table drop_ext_btree_gist
-
-# Test gist, but with fewer rows - shows that killitems doesn't work anymore!
-permutation
-  create_table fill_10 create_ext_btree_gist create_gist flush
-  disable_seq disable_bitmap
-  measure access flush result
-  measure access flush result
-  delete flush
-  measure access flush result
-  measure access flush result
-  drop_table drop_ext_btree_gist
-
-# Same as first permutation, except testing hash
-permutation
-  create_table fill_500 create_hash flush
-  disable_seq disable_bitmap
-  measure access flush result
-  measure access flush result
-  delete flush
-  measure access flush result
-  measure access flush result
-  drop_table
-
-# # Similar to first permutation, except that gin does not have killtuples support
-permutation
-  create_table fill_500 create_ext_btree_gin create_gin flush
-  disable_seq
-  delete flush
-  measure access flush result
-  # will still fetch from heap
-  measure access flush result
-  drop_table drop_ext_btree_gin
diff --git a/src/test/modules/index/t/001_killtuples.pl b/src/test/modules/index/t/001_killtuples.pl
new file mode 100644
index 00000000000..eda138a0b32
--- /dev/null
+++ b/src/test/modules/index/t/001_killtuples.pl
@@ -0,0 +1,343 @@
+
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Basic testing of killtuples / kill_prior_tuples / all_dead testing
+# for various index AMs along with the WAL replay functions.
+#
+# This tests just enough to ensure that the kill* routines are actually
+# executed and does something approximately reasonable. It's *not* sufficient
+# testing for adding killitems support to a new AM!
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my ($primary, $psql_primary, $standby, $psql_standby) = start_instances();
+create_extensions();
+disable_seq();
+set_bitmap('false');
+
+test_btree_index();
+test_gist_index();
+test_gist_index_fewer_rows();
+test_hash_index();
+
+set_bitmap('true');
+test_gin_index();
+
+stop_instances();
+
+# test killtuples with btree index
+sub test_btree_index
+{
+	my $test_name = 'btree';
+	prepare_table();
+	create_btree();
+
+	insert_table_n_rows(500);
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 1);
+
+	delete_table();
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 0);
+
+	drop_table();
+	check_wal_is_replayed($test_name);
+}
+
+# Same as btree_index, except testing gist
+sub test_gist_index
+{
+	my $test_name = 'gist';
+	prepare_table();
+	create_btree_gist();
+
+	insert_table_n_rows(500);
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 1);
+
+	delete_table();
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 0);
+
+	# Re-insert to test redo delete case
+	insert_table_n_rows(250);
+
+	drop_table();
+	check_wal_is_replayed($test_name);
+}
+
+# Same as gist, but with fewer rows - shows that killitems doesn't work anymore!
+sub test_gist_index_fewer_rows
+{
+	my $test_name = 'gist_but_fewer_rows';
+	prepare_table();
+	create_btree_gist();
+
+	insert_table_n_rows(10);
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 1);
+
+	delete_table();
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 1);
+
+	drop_table();
+	check_wal_is_replayed($test_name);
+}
+
+# Same as btree_index, except testing hash
+sub test_hash_index
+{
+	my $test_name = 'hash';
+	prepare_table();
+	create_hash();
+
+	insert_table_n_rows(500);
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 1);
+
+	delete_table();
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 0);
+
+	drop_table();
+	check_wal_is_replayed($test_name);
+}
+
+# Similar to btree_index, except that gin does not have killtuples support
+sub test_gin_index
+{
+	my $test_name = 'gin';
+	prepare_table();
+	create_btree_gin();
+
+	insert_table_n_rows(500);
+
+	delete_table();
+
+	do_checks($test_name, 1);
+	do_checks($test_name, 1);
+
+	drop_table();
+	check_wal_is_replayed($test_name);
+}
+
+# Create btree index
+sub create_btree
+{
+	$psql_primary->query_safe(
+		q(
+CREATE INDEX kill_prior_tuple_btree ON kill_prior_tuple USING btree (key);
+));
+}
+
+# Create btree_gist extension and index
+sub create_btree_gist
+{
+	$psql_primary->query_safe(
+		q(
+CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key);
+));
+}
+
+# Create btree_gin extension and index
+sub create_btree_gin
+{
+	$psql_primary->query_safe(
+		q(
+CREATE INDEX kill_prior_tuple_gin ON kill_prior_tuple USING gin (key);
+    ));
+}
+
+# Create hash index
+sub create_hash
+{
+	$psql_primary->query_safe(
+		q(
+CREATE INDEX kill_prior_tuple_hash ON kill_prior_tuple USING hash (key);
+));
+}
+
+### General helper functions
+sub start_instances
+{
+	my $backup_name = 'backup';
+	my $primary;
+	my $psql_primary;
+	my $standby;
+	my $psql_standby;
+
+	$primary = PostgreSQL::Test::Cluster->new("primary");
+	$primary->init(allows_streaming => 1);
+	$primary->start;
+	$psql_primary = $primary->background_psql('postgres');
+	$primary->backup($backup_name);
+
+	$standby = PostgreSQL::Test::Cluster->new("standby");
+	$standby->init_from_backup($primary, $backup_name, has_streaming => 1);
+	$standby->start;
+	$psql_standby = $standby->background_psql('postgres');
+
+	return $primary, $psql_primary, $standby, $psql_standby;
+}
+
+sub create_extensions
+{
+	$psql_primary->query_safe(
+		q(
+CREATE EXTENSION btree_gist;
+CREATE EXTENSION btree_gin;
+));
+}
+
+sub prepare_table
+{
+	$psql_primary->query_safe(
+		q(
+CREATE TABLE counter(heap_accesses int);
+INSERT INTO counter(heap_accesses) VALUES (0);
+CREATE TABLE kill_prior_tuple(key int not null, cat text not null);
+));
+}
+
+sub insert_table_n_rows
+{
+	my $n_rows = shift;
+
+	$psql_primary->query_safe(
+		qq(
+INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, $n_rows) g(i);
+));
+	flush_statistics();
+}
+
+sub stop_instances
+{
+	$psql_standby->quit;
+	$standby->stop;
+
+	$psql_primary->quit;
+	$primary->stop;
+}
+
+sub drop_table
+{
+	$psql_primary->query_safe(
+		q(
+DROP TABLE IF EXISTS kill_prior_tuple;
+DROP TABLE IF EXISTS counter;
+));
+}
+
+sub delete_table
+{
+	$psql_primary->query_safe(q(DELETE FROM kill_prior_tuple;));
+	flush_statistics();
+}
+
+sub disable_seq
+{
+	$psql_primary->query_safe(
+		q(
+SET enable_seqscan = false;
+SELECT pg_reload_conf();
+));
+}
+
+sub set_bitmap
+{
+	my $val = shift;
+
+	$psql_primary->query_safe(
+		qq(
+SET enable_bitmapscan = $val;
+SELECT pg_reload_conf();
+));
+}
+
+sub flush_statistics
+{
+	$psql_primary->query_safe(q(SELECT FROM pg_stat_force_next_flush();));
+}
+
+sub update_heap_accesses
+{
+	$psql_primary->query_safe(
+		q(
+UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple');
+));
+}
+
+sub check_index_searches
+{
+	my $test_name = shift;
+	my $result;
+
+	$result = $psql_primary->query_safe(
+		q(
+EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1;
+));
+
+	like(
+		$result,
+		qr/Index Searches: 1/,
+		"$test_name: Number of 'Index Searches'should be equal to 1");
+}
+
+sub check_new_heap_accesses
+{
+	my ($test_name, $new_heap_accesses) = @_;
+	my $result;
+
+	$result = $psql_primary->query_safe(
+		q(
+SELECT heap_blks_read + heap_blks_hit - counter.heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple';
+));
+
+	cmp_ok($result, '==', $new_heap_accesses,
+		"$test_name: Number of 'new_heap_accesses' should be equal to $new_heap_accesses"
+	);
+}
+
+sub do_checks
+{
+	my ($test_name, $new_heap_accesses) = @_;
+
+	# First update number of heap accesses
+	update_heap_accesses();
+	# Then check number of index searches in the EXPLAIN output
+	check_index_searches($test_name);
+	# Then force flush statistics
+	flush_statistics();
+	# Then check number of *new* heap accesses, it should be equal to
+	# $new_heap_accesses
+	check_new_heap_accesses($test_name, $new_heap_accesses);
+}
+
+sub check_wal_is_replayed
+{
+	my $test_name = shift;
+
+	my $current_lsn =
+	  $psql_primary->query_safe("SELECT pg_current_wal_lsn();");
+	my $caughtup_query =
+	  "SELECT '$current_lsn'::pg_lsn <= pg_last_wal_replay_lsn()";
+
+	$standby->poll_query_until('postgres', $caughtup_query)
+	  or die "$test_name: Timed out while waiting for standby to catch up";
+}
+
+done_testing();
-- 
2.51.0

