While working on the "Combine Prune and Freeze records emitted by vacuum" patch [1], I wished we would have an easier way to test pruning. There's a lot of logic with following HOT chains etc., and it's very hard to construct all those scenarios just by INSERT/UPDATE/DELETE commands. In principle though, pruning should be very amenable for good test coverage. The input is one heap page and some parameters, the output is one heap page and a few other fields that are already packaged neatly in the PruneFreezeResult struct.

Back then, I started to work on a little tool for that to verify the correctness of pruning refactoring, but I never got around to polish it or write proper repeatable tests with it. I did use it for some ad hoc testing, though.

I don't know when I'll find the time to polish it, so here is the very rough work-in-progress version I've got now.

One thing I used this for was to test that we still handle HEAP_MOVED_IN/OFF correctly. Yes, it still works. But what surprised me is that when a HEAP_MOVED_IN tuple is frozen, we replace xvac with FrozenTransactondId, and leave the HEAP_MOVED_IN flag in place. I assumed that we would clear the HEAP_MOVED_IN flag instead.

[1] https://www.postgresql.org/message-id/CAAKRu_azf-zH%3DDgVbquZ3tFWjMY1w5pO8m-TXJaMdri8z3933g%40mail.gmail.com

--
Heikki Linnakangas
Neon (https://neon.tech)
From 9dcd528dd42f689e207d808bee388fb233b2e25e Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@iki.fi>
Date: Sun, 14 Apr 2024 22:50:10 +0300
Subject: [PATCH 1/2] Expose conflict_xid to caller, for tests

---
 src/backend/access/heap/pruneheap.c | 20 +++++++++++---------
 src/include/access/heapam.h         |  5 +++++
 2 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index d2eecaf7ebc..8f4d17f7d08 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -366,6 +366,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 	bool		do_hint;
 	bool		hint_bit_fpi;
 	int64		fpi_before = pgWalUsage.wal_fpi;
+	TransactionId conflict_xid = InvalidTransactionId;
 
 	/* Copy parameters to prstate */
 	prstate.vistest = vistest;
@@ -794,7 +795,6 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 		/*
 		 * Emit a WAL XLOG_HEAP2_PRUNE_FREEZE record showing what we did
 		 */
-		if (RelationNeedsWAL(relation))
 		{
 			/*
 			 * The snapshotConflictHorizon for the whole record should be the
@@ -807,7 +807,6 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 			 * record will freeze will conflict.
 			 */
 			TransactionId frz_conflict_horizon = InvalidTransactionId;
-			TransactionId conflict_xid;
 
 			/*
 			 * We can use the visibility_cutoff_xid as our cutoff for
@@ -832,13 +831,14 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 			else
 				conflict_xid = prstate.latest_xid_removed;
 
-			log_heap_prune_and_freeze(relation, buffer,
-									  conflict_xid,
-									  true, reason,
-									  prstate.frozen, prstate.nfrozen,
-									  prstate.redirected, prstate.nredirected,
-									  prstate.nowdead, prstate.ndead,
-									  prstate.nowunused, prstate.nunused);
+			if (RelationNeedsWAL(relation))
+				log_heap_prune_and_freeze(relation, buffer,
+										  conflict_xid,
+										  true, reason,
+										  prstate.frozen, prstate.nfrozen,
+										  prstate.redirected, prstate.nredirected,
+										  prstate.nowdead, prstate.ndead,
+										  prstate.nowunused, prstate.nunused);
 		}
 	}
 
@@ -876,6 +876,8 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 
 	presult->hastup = prstate.hastup;
 
+	presult->conflict_xid = conflict_xid;
+
 	/*
 	 * For callers planning to update the visibility map, the conflict horizon
 	 * for that record must be the newest xmin on the page.  However, if the
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 735662dc9df..934b5bfbe50 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -255,6 +255,11 @@ typedef struct PruneFreezeResult
 	 */
 	bool		hastup;
 
+	/*
+	 * Recovery conflict XID, if any. This is returned just for white-box tests.
+	 */
+	TransactionId conflict_xid;
+
 	/*
 	 * LP_DEAD items on the page after pruning.  Includes existing LP_DEAD
 	 * items.
-- 
2.39.2

From 9af964cfddf8634e796af5facfd2476401f8ef78 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@iki.fi>
Date: Sun, 14 Apr 2024 23:29:35 +0300
Subject: [PATCH 2/2] XXX: Add test_heapam

heappage_craft.c contains SQL-callable functions for constructing a
heap page from scratch, with the exact line pointers, XIDs, flags etc.
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_heapam/Makefile         |  24 ++
 .../test_heapam/expected/move_in_out.out      |  69 ++++
 .../modules/test_heapam/expected/pruning.out  | 317 ++++++++++++++++++
 src/test/modules/test_heapam/heappage_craft.c | 271 +++++++++++++++
 src/test/modules/test_heapam/meson.build      |  35 ++
 .../modules/test_heapam/sql/move_in_out.sql   |  37 ++
 src/test/modules/test_heapam/sql/pruning.sql  |  76 +++++
 .../modules/test_heapam/test_heapam--1.0.sql  |  42 +++
 src/test/modules/test_heapam/test_heapam.c    | 114 +++++++
 .../modules/test_heapam/test_heapam.control   |   4 +
 12 files changed, 991 insertions(+)
 create mode 100644 src/test/modules/test_heapam/Makefile
 create mode 100644 src/test/modules/test_heapam/expected/move_in_out.out
 create mode 100644 src/test/modules/test_heapam/expected/pruning.out
 create mode 100644 src/test/modules/test_heapam/heappage_craft.c
 create mode 100644 src/test/modules/test_heapam/meson.build
 create mode 100644 src/test/modules/test_heapam/sql/move_in_out.sql
 create mode 100644 src/test/modules/test_heapam/sql/pruning.sql
 create mode 100644 src/test/modules/test_heapam/test_heapam--1.0.sql
 create mode 100644 src/test/modules/test_heapam/test_heapam.c
 create mode 100644 src/test/modules/test_heapam/test_heapam.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 256799f520a..fa9e1c471b3 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -21,6 +21,7 @@ SUBDIRS = \
 		  test_dsm_registry \
 		  test_extensions \
 		  test_ginpostinglist \
+		  test_heapam \
 		  test_integerset \
 		  test_json_parser \
 		  test_lfind \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index d8fe059d236..946d1157a6f 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_dsa')
 subdir('test_dsm_registry')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
+subdir('test_heapam')
 subdir('test_integerset')
 subdir('test_json_parser')
 subdir('test_lfind')
diff --git a/src/test/modules/test_heapam/Makefile b/src/test/modules/test_heapam/Makefile
new file mode 100644
index 00000000000..1ba90fef276
--- /dev/null
+++ b/src/test/modules/test_heapam/Makefile
@@ -0,0 +1,24 @@
+# src/test/modules/test_heapam/Makefile
+
+MODULE_big = test_heapam
+OBJS = \
+	$(WIN32RES) \
+	heappage_craft.o \
+	test_heapam.o
+PGFILEDESC = "test_heapam - test code for heap AM"
+
+EXTENSION = test_heapam
+DATA = test_heapam--1.0.sql
+
+REGRESS = move_in_out pruning
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_heapam
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_heapam/expected/move_in_out.out b/src/test/modules/test_heapam/expected/move_in_out.out
new file mode 100644
index 00000000000..70090caac3d
--- /dev/null
+++ b/src/test/modules/test_heapam/expected/move_in_out.out
@@ -0,0 +1,69 @@
+CREATE EXTENSION pageinspect;
+CREATE EXTENSION test_heapam;
+CREATE TEMP TABLE prunetest (data text);
+select pg_current_xact_id() as committed_xid1
+\gset
+create temporary view dump_items as
+  SELECT lp, case lp_flags
+    when 0 then 'LP_UNUSED'
+    when 1 then 'LP_NORMAL'
+    when 2 then 'LP_REDIRECT to ' || lp_off
+    when 3 then 'LP_DEAD' END as lp_flags,
+    t_xmin, t_xmax, t_field3,
+  (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*,
+  convert_from(substring(t_data, 2), 'utf8') as data
+  FROM heap_page_items(get_raw_page('prunetest', 0));
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+--				lp    xmin  xmax   xvac/cid           ctid        flags                              data
+select heappage_craft_add_tuple('1',  '2',  '0',   :'committed_xid1', '(0, 1)',   array['HEAP_MOVED_OFF']::text[],   'normal');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('2',  '2',  '0',   :'committed_xid1', '(0, 2)',   array['HEAP_MOVED_IN']::text[],    'normal');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select * from dump_items;
+ lp | lp_flags  | t_xmin | t_xmax | t_field3 |             raw_flags             | combined_flags |  data  
+----+-----------+--------+--------+----------+-----------------------------------+----------------+--------
+  1 | LP_NORMAL |      2 |      0 |      743 | {HEAP_HASVARWIDTH,HEAP_MOVED_OFF} | {}             | normal
+  2 | LP_NORMAL |      2 |      0 |      743 | {HEAP_HASVARWIDTH,HEAP_MOVED_IN}  | {}             | normal
+(2 rows)
+
+select xmin, xmax, ctid, * from prunetest;
+ xmin | xmax | ctid  |  data  
+------+------+-------+--------
+    2 |    0 | (0,2) | normal
+(1 row)
+
+select * from dump_items;
+ lp | lp_flags  | t_xmin | t_xmax | t_field3 |                               raw_flags                                | combined_flags |  data  
+----+-----------+--------+--------+----------+------------------------------------------------------------------------+----------------+--------
+  1 | LP_NORMAL |      2 |      0 |      743 | {HEAP_HASVARWIDTH,HEAP_XMIN_INVALID,HEAP_MOVED_OFF}                    | {}             | normal
+  2 | LP_NORMAL |      2 |      0 |      743 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMAX_INVALID,HEAP_MOVED_IN} | {}             | normal
+(2 rows)
+
+--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0));
+vacuum freeze prunetest;
+select * from dump_items;
+ lp | lp_flags  | t_xmin | t_xmax | t_field3 |                               raw_flags                                | combined_flags |  data  
+----+-----------+--------+--------+----------+------------------------------------------------------------------------+----------------+--------
+  1 | LP_UNUSED |        |        |          |                                                                        |                | 
+  2 | LP_NORMAL |      2 |      0 |        2 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMAX_INVALID,HEAP_MOVED_IN} | {}             | normal
+(2 rows)
+
diff --git a/src/test/modules/test_heapam/expected/pruning.out b/src/test/modules/test_heapam/expected/pruning.out
new file mode 100644
index 00000000000..56f7254332c
--- /dev/null
+++ b/src/test/modules/test_heapam/expected/pruning.out
@@ -0,0 +1,317 @@
+CREATE EXTENSION pageinspect;
+ERROR:  extension "pageinspect" already exists
+CREATE EXTENSION test_heapam;
+ERROR:  extension "test_heapam" already exists
+CREATE TABLE prunetest (data text) WITH (autovacuum_enabled=false);
+select pg_current_xact_id() as committed_xid
+\gset
+begin;
+select pg_current_xact_id() as aborted_xid;
+ aborted_xid 
+-------------
+         749
+(1 row)
+
+\gset
+rollback;
+create temporary view dump_items as
+  SELECT lp, case when lp_flags = 2 then lp_off else null end as redirect_off, t_xmin, t_xmax,
+  (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*,
+  convert_from(substring(t_data, 2), 'utf8') as data
+  FROM heap_page_items(get_raw_page('prunetest', 0));
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+select heappage_craft_add_lp_unused('1');
+ heappage_craft_add_lp_unused 
+------------------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select * from dump_items;
+ lp | redirect_off | t_xmin | t_xmax |     raw_flags      | combined_flags |  data  
+----+--------------+--------+--------+--------------------+----------------+--------
+  1 |              |    748 |      0 | {HEAP_HASVARWIDTH} | {}             | normal
+(1 row)
+
+--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0));
+SELECT heappage_prune_and_freeze('prunetest', 0);
+NOTICE:  prune results:
+  ndeleted: 0
+  nnewlpdead: 0
+  nfrozen: 1
+  live_tuples: 1
+  recently_dead_tuples: 0
+  all_visible: 1
+  all_frozen: 1
+  vm_conflict_horizon: 0
+  hastup: 1
+  conflict_xid: 748
+  deadoffsets: []
+  new_relfrozen_xid: 752
+  new_relmin_mxid: 1
+
+ heappage_prune_and_freeze 
+---------------------------
+ 
+(1 row)
+
+select * from dump_items;
+ lp | redirect_off | t_xmin | t_xmax |                                 raw_flags                                  |   combined_flags   |  data  
+----+--------------+--------+--------+----------------------------------------------------------------------------+--------------------+--------
+  1 |              |    748 |      0 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMIN_INVALID,HEAP_XMAX_INVALID} | {HEAP_XMIN_FROZEN} | normal
+(1 row)
+
+-- Page has two LP_DEAD items, one LP_UNUSED, nothing else.
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+select heappage_craft_add_lp_dead('1');
+ heappage_craft_add_lp_dead 
+----------------------------
+ 
+(1 row)
+
+select heappage_craft_add_lp_unused('2');
+ heappage_craft_add_lp_unused 
+------------------------------
+ 
+(1 row)
+
+select heappage_craft_add_lp_dead('3');
+ heappage_craft_add_lp_dead 
+----------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select heappage_prune_and_freeze('prunetest', 0);
+NOTICE:  prune results:
+  ndeleted: 0
+  nnewlpdead: 0
+  nfrozen: 0
+  live_tuples: 0
+  recently_dead_tuples: 0
+  all_visible: 0
+  all_frozen: 0
+  vm_conflict_horizon: 0
+  hastup: 0
+  conflict_xid: 0
+  deadoffsets: [3, 1]
+  new_relfrozen_xid: 754
+  new_relmin_mxid: 1
+
+ heappage_prune_and_freeze 
+---------------------------
+ 
+(1 row)
+
+-- One aborted item, nothing else. 
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('1', :'aborted_xid', '0', '0', '(0, 1)', array[]::text[], 'aborted');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select heappage_prune_and_freeze('prunetest', 0);
+NOTICE:  prune results:
+  ndeleted: 1
+  nnewlpdead: 1
+  nfrozen: 0
+  live_tuples: 0
+  recently_dead_tuples: 0
+  all_visible: 0
+  all_frozen: 0
+  vm_conflict_horizon: 0
+  hastup: 0
+  conflict_xid: 0
+  deadoffsets: [1]
+  new_relfrozen_xid: 756
+  new_relmin_mxid: 1
+
+ heappage_prune_and_freeze 
+---------------------------
+ 
+(1 row)
+
+-- One already-frozen item, nothing else. 
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('1', '2', '0', '0', '(0, 1)', array[]::text[], 'normal');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select heappage_prune_and_freeze('prunetest', 0);
+NOTICE:  prune results:
+  ndeleted: 0
+  nnewlpdead: 0
+  nfrozen: 0
+  live_tuples: 1
+  recently_dead_tuples: 0
+  all_visible: 1
+  all_frozen: 1
+  vm_conflict_horizon: 0
+  hastup: 1
+  conflict_xid: 0
+  deadoffsets: []
+  new_relfrozen_xid: 758
+  new_relmin_mxid: 1
+
+ heappage_prune_and_freeze 
+---------------------------
+ 
+(1 row)
+
+-- One committed item, nothing else.
+select pg_current_xact_id() as committed_xid
+\gset
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select heappage_prune_and_freeze('prunetest', 0);
+NOTICE:  prune results:
+  ndeleted: 0
+  nnewlpdead: 0
+  nfrozen: 1
+  live_tuples: 1
+  recently_dead_tuples: 0
+  all_visible: 1
+  all_frozen: 1
+  vm_conflict_horizon: 0
+  hastup: 1
+  conflict_xid: 759
+  deadoffsets: []
+  new_relfrozen_xid: 761
+  new_relmin_mxid: 1
+
+ heappage_prune_and_freeze 
+---------------------------
+ 
+(1 row)
+
+-- One visible item, one deleted item
+select pg_current_xact_id() as committed_xid1
+\gset
+select pg_current_xact_id() as committed_xid2
+\gset
+select heappage_craft_new();
+ heappage_craft_new 
+--------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('1', :'committed_xid1', '0', '0', '(0, 1)', array[]::text[], 'normal');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_add_tuple('2', '2', :'committed_xid2', '0', '(0, 1)', array[]::text[], 'deleted');
+ heappage_craft_add_tuple 
+--------------------------
+ 
+(1 row)
+
+select heappage_craft_install('prunetest'::regclass, 0);
+ heappage_craft_install 
+------------------------
+ 
+(1 row)
+
+select * from dump_items;
+ lp | redirect_off | t_xmin | t_xmax |     raw_flags      | combined_flags |  data   
+----+--------------+--------+--------+--------------------+----------------+---------
+  1 |              |    762 |      0 | {HEAP_HASVARWIDTH} | {}             | normal
+  2 |              |      2 |    763 | {HEAP_HASVARWIDTH} | {}             | deleted
+(2 rows)
+
+select heappage_prune_and_freeze('prunetest', 0);
+NOTICE:  prune results:
+  ndeleted: 1
+  nnewlpdead: 1
+  nfrozen: 1
+  live_tuples: 1
+  recently_dead_tuples: 0
+  all_visible: 0
+  all_frozen: 0
+  vm_conflict_horizon: 762
+  hastup: 1
+  conflict_xid: 763
+  deadoffsets: [2]
+  new_relfrozen_xid: 765
+  new_relmin_mxid: 1
+
+ heappage_prune_and_freeze 
+---------------------------
+ 
+(1 row)
+
+select * from dump_items;
+ lp | redirect_off | t_xmin | t_xmax |                                 raw_flags                                  |   combined_flags   |  data  
+----+--------------+--------+--------+----------------------------------------------------------------------------+--------------------+--------
+  1 |              |    762 |      0 | {HEAP_HASVARWIDTH,HEAP_XMIN_COMMITTED,HEAP_XMIN_INVALID,HEAP_XMAX_INVALID} | {HEAP_XMIN_FROZEN} | normal
+  2 |              |        |        |                                                                            |                    | 
+(2 rows)
+
diff --git a/src/test/modules/test_heapam/heappage_craft.c b/src/test/modules/test_heapam/heappage_craft.c
new file mode 100644
index 00000000000..78f99a47829
--- /dev/null
+++ b/src/test/modules/test_heapam/heappage_craft.c
@@ -0,0 +1,271 @@
+/*--------------------------------------------------------------------------
+ *
+ * heappage_craft.c
+ *		Functions to craft heap pages with specific contents
+ *
+ * Copyright (c) 2022-2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_heapam/heappage_craft.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "storage/bufmgr.h"
+#include "storage/bufpage.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+
+/* Temporary Page image we're currently crafting */
+static Page crafted_page = NULL;
+
+static OffsetNumber heappage_craft_add_line_pointer(OffsetNumber offnum, ItemIdData itemid);
+
+/* Initialize a new empty page. Must be called before the other functions */
+PG_FUNCTION_INFO_V1(heappage_craft_new);
+Datum
+heappage_craft_new(PG_FUNCTION_ARGS)
+{
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	if (crafted_page == NULL)
+		crafted_page = MemoryContextAlloc(TopMemoryContext, BLCKSZ);
+	PageInit(crafted_page, BLCKSZ, 0);
+
+	PG_RETURN_VOID();
+}
+
+/* Install the currently crafted page into a table */
+PG_FUNCTION_INFO_V1(heappage_craft_install);
+Datum
+heappage_craft_install(PG_FUNCTION_ARGS)
+{
+	Oid			table_oid = PG_GETARG_OID(0);
+	BlockNumber	blkno = PG_GETARG_UINT32(1);
+	Relation	rel;
+	BlockNumber numblocks;
+	Buffer		buf;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	rel = table_open(table_oid, AccessExclusiveLock);
+
+	numblocks = RelationGetNumberOfBlocksInFork(rel, MAIN_FORKNUM);
+	if (blkno == numblocks)
+	{
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, P_NEW, RBM_ZERO_AND_LOCK, NULL);
+	}
+	else
+	{
+		if (blkno >= RelationGetNumberOfBlocksInFork(rel, MAIN_FORKNUM))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("block number %u is out of range for relation \"%s\"",
+							blkno, RelationGetRelationName(rel))));
+		buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL);
+	}
+
+	memcpy(BufferGetPage(buf), crafted_page, BLCKSZ);
+
+	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+	ReleaseBuffer(buf);
+
+	table_close(rel, AccessExclusiveLock);
+
+	PG_RETURN_VOID();
+}
+
+static void
+flags_to_infomask(ArrayType *flags, uint16 *infomask, uint16 *infomask2)
+{
+	Datum	   *elems;
+	int			nelems;
+
+	deconstruct_array(flags, TEXTOID, -1, false, TYPALIGN_INT,
+					  &elems, NULL, &nelems);
+	for (int i = 0; i < nelems; i++)
+	{
+		char		*flag = TextDatumGetCString(elems[i]);
+
+		if (strcmp(flag, "HEAP_HASNULL") == 0)
+			(*infomask) |= HEAP_HASNULL;
+		else if (strcmp(flag, "HEAP_HASVARWIDTH") == 0)
+			(*infomask) |= HEAP_HASVARWIDTH;
+		else if (strcmp(flag, "HEAP_HASEXTERNAL") == 0)
+			(*infomask) |= HEAP_HASEXTERNAL;
+		else if (strcmp(flag, "HEAP_HASOID_OLD") == 0)
+			(*infomask) |= HEAP_HASOID_OLD;
+		else if (strcmp(flag, "HEAP_XMAX_KEYSHR_LOCK") == 0)
+			(*infomask) |= HEAP_XMAX_KEYSHR_LOCK;
+		else if (strcmp(flag, "HEAP_COMBOCID") == 0)
+			(*infomask) |= HEAP_COMBOCID;
+		else if (strcmp(flag, "HEAP_XMAX_EXCL_LOCK") == 0)
+			(*infomask) |= HEAP_XMAX_EXCL_LOCK;
+		else if (strcmp(flag, "HEAP_XMAX_LOCK_ONLY") == 0)
+			(*infomask) |= HEAP_XMAX_LOCK_ONLY;
+		else if (strcmp(flag, "HEAP_XMIN_COMMITTED") == 0)
+			(*infomask) |= HEAP_XMIN_COMMITTED;
+		else if (strcmp(flag, "HEAP_XMIN_INVALID") == 0)
+			(*infomask) |= HEAP_XMIN_INVALID;
+		else if (strcmp(flag, "HEAP_XMAX_COMMITTED") == 0)
+			(*infomask) |= HEAP_XMAX_COMMITTED;
+		else if (strcmp(flag, "HEAP_XMAX_INVALID") == 0)
+			(*infomask) |= HEAP_XMAX_INVALID;
+		else if (strcmp(flag, "HEAP_XMAX_IS_MULTI") == 0)
+			(*infomask) |= HEAP_XMAX_IS_MULTI;
+		else if (strcmp(flag, "HEAP_UPDATED") == 0)
+			(*infomask) |= HEAP_UPDATED;
+		else if (strcmp(flag, "HEAP_MOVED_OFF") == 0)
+			(*infomask) |= HEAP_MOVED_OFF;
+		else if (strcmp(flag, "HEAP_MOVED_IN") == 0)
+			(*infomask) |= HEAP_MOVED_IN;
+
+		else if (strcmp(flag, " HEAP_KEYS_UPDATED") == 0)
+			(*infomask2) |=  HEAP_KEYS_UPDATED;
+		else if (strcmp(flag, " HEAP_HOT_UPDATED") == 0)
+			(*infomask2) |=  HEAP_HOT_UPDATED;
+		else if (strcmp(flag, " HEAP_ONLY_TUPLE") == 0)
+			(*infomask2) |=  HEAP_ONLY_TUPLE;
+		else
+			elog(ERROR, "unknown heap tuple flag %s", flag);
+
+		pfree(flag);
+	}
+}
+
+/* Add a tuple to page being crafted */
+PG_FUNCTION_INFO_V1(heappage_craft_add_tuple);
+Datum
+heappage_craft_add_tuple(PG_FUNCTION_ARGS)
+{
+	OffsetNumber offnum = PG_GETARG_UINT16(0);
+	TransactionId xmin = PG_GETARG_TRANSACTIONID(1);
+	TransactionId xmax = PG_GETARG_TRANSACTIONID(2);
+	CommandId cid = PG_GETARG_UINT32(3);
+	ItemPointer ctid = PG_GETARG_ITEMPOINTER(4);
+	ArrayType *infoflags = PG_GETARG_ARRAYTYPE_P(5);
+	Datum		data = PG_GETARG_DATUM(6);
+	bool		isnull = false;
+	HeapTuple	htup;
+	TupleDesc	tupdesc;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	tupdesc = CreateTemplateTupleDesc(1);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "data",
+					   TEXTOID, -1, 0);
+	tupdesc = BlessTupleDesc(tupdesc);
+
+	htup = heap_form_tuple(tupdesc, &data, &isnull);
+
+	htup->t_data->t_choice.t_heap.t_xmin = xmin;
+	htup->t_data->t_choice.t_heap.t_xmax = xmax;
+	htup->t_data->t_choice.t_heap.t_field3.t_cid = cid;
+	htup->t_data->t_ctid = *ctid;
+	flags_to_infomask(infoflags,
+					  &htup->t_data->t_infomask,
+					  &htup->t_data->t_infomask2);
+	
+	if (PageAddItemExtended(crafted_page, (Item) htup->t_data, htup->t_len, offnum,
+							PAI_OVERWRITE | PAI_IS_HEAP) == InvalidOffsetNumber)
+		elog(ERROR, "failed to add tuple");
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(heappage_craft_add_lp_unused);
+Datum
+heappage_craft_add_lp_unused(PG_FUNCTION_ARGS)
+{
+	OffsetNumber offnum = PG_GETARG_UINT16(0);
+	ItemIdData iid;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	ItemIdSetUnused(&iid);
+	heappage_craft_add_line_pointer(offnum, iid);
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(heappage_craft_add_lp_redirect);
+Datum
+heappage_craft_add_lp_redirect(PG_FUNCTION_ARGS)
+{
+	OffsetNumber offnum = PG_GETARG_UINT16(0);
+	OffsetNumber redirect_offnum = PG_GETARG_UINT16(1);
+	ItemIdData iid;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	ItemIdSetRedirect(&iid, redirect_offnum);
+	heappage_craft_add_line_pointer(offnum, iid);
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(heappage_craft_add_lp_dead);
+Datum
+heappage_craft_add_lp_dead(PG_FUNCTION_ARGS)
+{
+	OffsetNumber offnum = PG_GETARG_UINT16(0);
+	ItemIdData iid;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	ItemIdSetDead(&iid);
+	heappage_craft_add_line_pointer(offnum, iid);
+
+	PG_RETURN_VOID();
+}
+
+static OffsetNumber
+heappage_craft_add_line_pointer(OffsetNumber offnum, ItemIdData itemid)
+{
+	PageHeader phdr = (PageHeader) crafted_page;
+	OffsetNumber limit;
+	
+	/* Reject placing items beyond the first unused line pointer */
+	limit = OffsetNumberNext(PageGetMaxOffsetNumber(crafted_page));
+	if (offnum > limit)
+	{
+		elog(WARNING, "specified item offset is too large");
+		return InvalidOffsetNumber;
+	}
+	if (offnum == limit)
+	{
+		uint16 new_lower = phdr->pd_lower + sizeof(ItemIdData);
+
+		if (new_lower > phdr->pd_upper)
+			return InvalidOffsetNumber;
+		phdr->pd_lower = new_lower;
+	}
+	phdr->pd_linp[offnum - 1] = itemid;
+
+	return offnum;
+}
diff --git a/src/test/modules/test_heapam/meson.build b/src/test/modules/test_heapam/meson.build
new file mode 100644
index 00000000000..f0fe701bf24
--- /dev/null
+++ b/src/test/modules/test_heapam/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+test_heapam_sources = files(
+  'heappage_craft.c',
+  'test_heapam.c',
+)
+
+if host_system == 'windows'
+  test_heapam_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_heapam',
+    '--FILEDESC', 'test_heapam - test code for heap AM',])
+endif
+
+test_heapam = shared_module('test_heapam',
+  test_heapam_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_heapam
+
+test_install_data += files(
+  'test_heapam.control',
+  'test_heapam--1.0.sql',
+)
+
+tests += {
+  'name': 'test_heapam',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'move_in_out',
+      'pruning',
+    ],
+  },
+}
diff --git a/src/test/modules/test_heapam/sql/move_in_out.sql b/src/test/modules/test_heapam/sql/move_in_out.sql
new file mode 100644
index 00000000000..15a5a9686bb
--- /dev/null
+++ b/src/test/modules/test_heapam/sql/move_in_out.sql
@@ -0,0 +1,37 @@
+CREATE EXTENSION pageinspect;
+CREATE EXTENSION test_heapam;
+
+CREATE TEMP TABLE prunetest (data text);
+
+select pg_current_xact_id() as committed_xid1
+\gset
+
+create temporary view dump_items as
+  SELECT lp, case lp_flags
+    when 0 then 'LP_UNUSED'
+    when 1 then 'LP_NORMAL'
+    when 2 then 'LP_REDIRECT to ' || lp_off
+    when 3 then 'LP_DEAD' END as lp_flags,
+    t_xmin, t_xmax, t_field3,
+  (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*,
+  convert_from(substring(t_data, 2), 'utf8') as data
+  FROM heap_page_items(get_raw_page('prunetest', 0));
+
+select heappage_craft_new();
+
+--				lp    xmin  xmax   xvac/cid           ctid        flags                              data
+select heappage_craft_add_tuple('1',  '2',  '0',   :'committed_xid1', '(0, 1)',   array['HEAP_MOVED_OFF']::text[],   'normal');
+select heappage_craft_add_tuple('2',  '2',  '0',   :'committed_xid1', '(0, 2)',   array['HEAP_MOVED_IN']::text[],    'normal');
+
+select heappage_craft_install('prunetest'::regclass, 0);
+
+select * from dump_items;
+
+select xmin, xmax, ctid, * from prunetest;
+
+select * from dump_items;
+--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0));
+
+vacuum freeze prunetest;
+
+select * from dump_items;
diff --git a/src/test/modules/test_heapam/sql/pruning.sql b/src/test/modules/test_heapam/sql/pruning.sql
new file mode 100644
index 00000000000..360b6c6b052
--- /dev/null
+++ b/src/test/modules/test_heapam/sql/pruning.sql
@@ -0,0 +1,76 @@
+CREATE EXTENSION pageinspect;
+CREATE EXTENSION test_heapam;
+
+CREATE TABLE prunetest (data text) WITH (autovacuum_enabled=false);
+
+select pg_current_xact_id() as committed_xid
+\gset
+
+begin;
+select pg_current_xact_id() as aborted_xid;
+\gset
+rollback;
+
+create temporary view dump_items as
+  SELECT lp, case when lp_flags = 2 then lp_off else null end as redirect_off, t_xmin, t_xmax,
+  (heap_tuple_infomask_flags(t_infomask, t_infomask2)).*,
+  convert_from(substring(t_data, 2), 'utf8') as data
+  FROM heap_page_items(get_raw_page('prunetest', 0));
+
+select heappage_craft_new();
+select heappage_craft_add_lp_unused('1');
+select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal');
+select heappage_craft_install('prunetest'::regclass, 0);
+
+select * from dump_items;
+--SELECT *, (heap_tuple_infomask_flags(t_infomask, t_infomask2)).* FROM heap_page_items(get_raw_page('prunetest', 0));
+
+SELECT heappage_prune_and_freeze('prunetest', 0);
+
+select * from dump_items;
+
+
+-- Page has two LP_DEAD items, one LP_UNUSED, nothing else.
+select heappage_craft_new();
+select heappage_craft_add_lp_dead('1');
+select heappage_craft_add_lp_unused('2');
+select heappage_craft_add_lp_dead('3');
+select heappage_craft_install('prunetest'::regclass, 0);
+
+select heappage_prune_and_freeze('prunetest', 0);
+
+-- One aborted item, nothing else. 
+select heappage_craft_new();
+select heappage_craft_add_tuple('1', :'aborted_xid', '0', '0', '(0, 1)', array[]::text[], 'aborted');
+select heappage_craft_install('prunetest'::regclass, 0);
+select heappage_prune_and_freeze('prunetest', 0);
+
+-- One already-frozen item, nothing else. 
+select heappage_craft_new();
+select heappage_craft_add_tuple('1', '2', '0', '0', '(0, 1)', array[]::text[], 'normal');
+select heappage_craft_install('prunetest'::regclass, 0);
+select heappage_prune_and_freeze('prunetest', 0);
+
+-- One committed item, nothing else.
+select pg_current_xact_id() as committed_xid
+\gset
+select heappage_craft_new();
+select heappage_craft_add_tuple('1', :'committed_xid', '0', '0', '(0, 1)', array[]::text[], 'normal');
+select heappage_craft_install('prunetest'::regclass, 0);
+select heappage_prune_and_freeze('prunetest', 0);
+
+
+-- One visible item, one deleted item
+select pg_current_xact_id() as committed_xid1
+\gset
+
+select pg_current_xact_id() as committed_xid2
+\gset
+
+select heappage_craft_new();
+select heappage_craft_add_tuple('1', :'committed_xid1', '0', '0', '(0, 1)', array[]::text[], 'normal');
+select heappage_craft_add_tuple('2', '2', :'committed_xid2', '0', '(0, 1)', array[]::text[], 'deleted');
+select heappage_craft_install('prunetest'::regclass, 0);
+select * from dump_items;
+select heappage_prune_and_freeze('prunetest', 0);
+select * from dump_items;
diff --git a/src/test/modules/test_heapam/test_heapam--1.0.sql b/src/test/modules/test_heapam/test_heapam--1.0.sql
new file mode 100644
index 00000000000..54141fd93f0
--- /dev/null
+++ b/src/test/modules/test_heapam/test_heapam--1.0.sql
@@ -0,0 +1,42 @@
+/* src/test/modules/test_heapam/test_heapam--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_heapam" to load this file. \quit
+
+
+CREATE FUNCTION heappage_prune_and_freeze(rel regclass, blkno int4)
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+
+CREATE FUNCTION heappage_craft_new()
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION heappage_craft_install(rel regclass, blkno int4)
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION heappage_craft_add_tuple(
+   offnum       int2,
+   xmin         xid,
+   xmax         xid,
+   cid          cid,
+   ctid         tid,
+   infoflags    text[],
+   data         text
+)
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION heappage_craft_add_lp_unused(offnum int2)
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION heappage_craft_add_lp_redirect(offnum int2, redirect_offnum int2)
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION heappage_craft_add_lp_dead(offnum int2)
+	RETURNS void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_heapam/test_heapam.c b/src/test/modules/test_heapam/test_heapam.c
new file mode 100644
index 00000000000..311d39f09bf
--- /dev/null
+++ b/src/test/modules/test_heapam/test_heapam.c
@@ -0,0 +1,114 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_heapam.c
+ *		Heap AM test functinos
+ *
+ * Copyright (c) 2022-2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_heapam/test_heapam.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "commands/vacuum.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "storage/bufmgr.h"
+#include "storage/bufpage.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(heappage_prune_and_freeze);
+Datum
+heappage_prune_and_freeze(PG_FUNCTION_ARGS)
+{
+	Oid			table_oid = PG_GETARG_OID(0);
+	BlockNumber	blkno = PG_GETARG_UINT32(1);
+	Relation	rel;
+	BlockNumber numblocks;
+	Buffer		buf;
+	VacuumParams vacuum_params;
+	struct GlobalVisState *vistest;
+	struct VacuumCutoffs cutoffs;
+	PruneFreezeResult presult;
+	OffsetNumber off_loc;
+	TransactionId new_relfrozen_xid;
+	MultiXactId new_relmin_mxid;
+	StringInfoData sinfo;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use test_heapam functions")));
+
+	rel = table_open(table_oid, AccessExclusiveLock);
+
+	numblocks = RelationGetNumberOfBlocksInFork(rel, MAIN_FORKNUM);
+	if (blkno >= numblocks)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("block number %u is out of range for relation \"%s\"",
+						blkno, RelationGetRelationName(rel))));
+
+	buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, NULL);
+	LockBufferForCleanup(buf);
+
+	memset(&vacuum_params, 0, sizeof(VacuumParams));
+	vacuum_params.options = VACOPT_VACUUM | VACOPT_PROCESS_MAIN | VACOPT_FREEZE;
+	vacuum_params.freeze_min_age = 0;
+	vacuum_params.multixact_freeze_min_age = 0;
+
+	(void) vacuum_get_cutoffs(rel, &vacuum_params, &cutoffs);
+	vistest = GlobalVisTestFor(rel);
+
+	new_relfrozen_xid = cutoffs.OldestXmin;
+	new_relmin_mxid = cutoffs.OldestMxact;
+	heap_page_prune_and_freeze(rel, buf, vistest, HEAP_PAGE_PRUNE_FREEZE,
+							   &cutoffs, &presult,
+							   PRUNE_VACUUM_SCAN, &off_loc,
+							   &new_relfrozen_xid, &new_relmin_mxid);
+
+	initStringInfo(&sinfo);
+	appendStringInfo(&sinfo, "prune results:\n");
+	appendStringInfo(&sinfo, "  ndeleted: %d\n", presult.ndeleted);
+	appendStringInfo(&sinfo, "  nnewlpdead: %d\n", presult.nnewlpdead);
+	appendStringInfo(&sinfo, "  nfrozen: %d\n", presult.nfrozen);
+	appendStringInfo(&sinfo, "  live_tuples: %d\n", presult.live_tuples);
+	appendStringInfo(&sinfo, "  recently_dead_tuples: %d\n", presult.recently_dead_tuples);
+
+	appendStringInfo(&sinfo, "  all_visible: %d\n", presult.all_visible);
+	appendStringInfo(&sinfo, "  all_frozen: %d\n", presult.all_frozen);
+	appendStringInfo(&sinfo, "  vm_conflict_horizon: %u\n", presult.vm_conflict_horizon);
+	appendStringInfo(&sinfo, "  hastup: %d\n", presult.hastup);
+	appendStringInfo(&sinfo, "  conflict_xid: %u\n", presult.conflict_xid);
+
+	appendStringInfoString(&sinfo, "  deadoffsets: [");
+	for (int i = 0; i < presult.lpdead_items; i++)
+	{
+		if (i > 0)
+			appendStringInfoString(&sinfo, ", ");
+		appendStringInfo(&sinfo, "%d", presult.deadoffsets[i]);
+	}
+	appendStringInfoString(&sinfo, "]\n");
+
+	appendStringInfo(&sinfo, "  new_relfrozen_xid: %u\n", new_relfrozen_xid);
+	appendStringInfo(&sinfo, "  new_relmin_mxid: %u\n", new_relmin_mxid);
+
+	LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+	ReleaseBuffer(buf);
+
+	table_close(rel, AccessExclusiveLock);
+
+	elog(NOTICE, "%s", sinfo.data);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_heapam/test_heapam.control b/src/test/modules/test_heapam/test_heapam.control
new file mode 100644
index 00000000000..4dd825e6b6e
--- /dev/null
+++ b/src/test/modules/test_heapam/test_heapam.control
@@ -0,0 +1,4 @@
+comment = 'Test code for heap AM'
+default_version = '1.0'
+module_pathname = '$libdir/test_heapam'
+relocatable = true
-- 
2.39.2

Reply via email to