From acb641037186b254567d2b352340772f455d2c17 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Sat, 29 Aug 2026 13:34:07 +0000
Subject: [PATCH 2/4] Add test_getopt_long module

TAP tests for the port getopt_long(). The harness compiles
src/port/getopt_long.c in directly since libpgport uses the system
implementation on most platforms. A second copy linked without it runs
the same cases against the system getopt_long() for comparison.
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_getopt_long/Makefile    |  39 ++
 src/test/modules/test_getopt_long/README      |  18 +
 src/test/modules/test_getopt_long/meson.build |  48 +++
 .../test_getopt_long/t/001_getopt_long.pl     | 372 ++++++++++++++++++
 .../test_getopt_long/test_getopt_long.c       | 149 +++++++
 7 files changed, 628 insertions(+)
 create mode 100644 src/test/modules/test_getopt_long/Makefile
 create mode 100644 src/test/modules/test_getopt_long/README
 create mode 100644 src/test/modules/test_getopt_long/meson.build
 create mode 100644 src/test/modules/test_getopt_long/t/001_getopt_long.pl
 create mode 100644 src/test/modules/test_getopt_long/test_getopt_long.c

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index bb88b3058ed..c85e8aff866 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -31,6 +31,7 @@ SUBDIRS = \
 		  test_dsm_registry \
 		  test_escape \
 		  test_extensions \
+		  test_getopt_long \
 		  test_ginpostinglist \
 		  test_int128 \
 		  test_integerset \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index ce09e00531d..3af2b1473e2 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -32,6 +32,7 @@ subdir('test_dsa')
 subdir('test_dsm_registry')
 subdir('test_escape')
 subdir('test_extensions')
+subdir('test_getopt_long')
 subdir('test_ginpostinglist')
 subdir('test_int128')
 subdir('test_integerset')
diff --git a/src/test/modules/test_getopt_long/Makefile b/src/test/modules/test_getopt_long/Makefile
new file mode 100644
index 00000000000..eb53b2c2231
--- /dev/null
+++ b/src/test/modules/test_getopt_long/Makefile
@@ -0,0 +1,39 @@
+# src/test/modules/test_getopt_long/Makefile
+
+PGFILEDESC = "standalone getopt_long tester"
+PGAPPICON = win32
+
+TAP_TESTS = 1
+
+OBJS = test_getopt_long.o $(WIN32RES)
+
+# getopt_long.o is not in OBJS, or --with-llvm would look for a
+# getopt_long.c here to build getopt_long.bc from.
+EXTRA_CLEAN = test_getopt_long$(X) test_getopt_long_system$(X) getopt_long.o
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_getopt_long
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+all: test_getopt_long$(X) test_getopt_long_system$(X)
+
+%.o: $(top_srcdir)/$(subdir)/%.c
+
+# The port getopt_long() is compiled in directly rather than taken from
+# libpgport, which uses the system implementation where one exists.
+getopt_long.o: $(top_srcdir)/src/port/getopt_long.c
+	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
+test_getopt_long$(X): test_getopt_long.o getopt_long.o $(WIN32RES)
+	$(CC) $(CFLAGS) $^ $(PG_LIBS_INTERNAL) $(LDFLAGS) $(LDFLAGS_EX) $(PG_LIBS) $(LIBS) -o $@
+
+# linked against the system getopt_long(), where there is one
+test_getopt_long_system$(X): test_getopt_long.o $(WIN32RES)
+	$(CC) $(CFLAGS) $^ $(PG_LIBS_INTERNAL) $(LDFLAGS) $(LDFLAGS_EX) $(PG_LIBS) $(LIBS) -o $@
diff --git a/src/test/modules/test_getopt_long/README b/src/test/modules/test_getopt_long/README
new file mode 100644
index 00000000000..f1fffcdfc90
--- /dev/null
+++ b/src/test/modules/test_getopt_long/README
@@ -0,0 +1,18 @@
+Module `test_getopt_long`
+=========================
+
+This module tests the getopt_long() implementation in src/port/getopt_long.c.
+Most platforms provide their own getopt_long() and libpgport does not include
+the port version there, so the test program compiles the port source in
+directly and thus exercises it everywhere. `test_getopt_long_system` is the
+same program without it, so the tests also run against the system
+getopt_long() and check the two agree.
+
+`test_getopt_long OPTSTRING LONGOPTS [ARG ...]` parses ARGs with the given
+short option string and long option list and prints one line per
+getopt_long() return, followed by a line listing the remaining non-option
+arguments. See the header comment in test_getopt_long.c for the format. The
+TAP test compares that output against the expected sequence for a range of
+argument layouts: attached and separate values, optional arguments in first,
+middle and last position, missing arguments, "--", a bare "-", and
+non-option reordering.
diff --git a/src/test/modules/test_getopt_long/meson.build b/src/test/modules/test_getopt_long/meson.build
new file mode 100644
index 00000000000..aa2e533b119
--- /dev/null
+++ b/src/test/modules/test_getopt_long/meson.build
@@ -0,0 +1,48 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# The port getopt_long() is compiled in directly rather than taken from
+# libpgport, which uses the system implementation where one exists.
+test_getopt_long_sources = files(
+  'test_getopt_long.c',
+  '../../../port/getopt_long.c',
+)
+
+if host_system == 'windows'
+  test_getopt_long_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_getopt_long',
+    '--FILEDESC', 'standalone getopt_long tester',
+  ])
+endif
+
+test_getopt_long = executable('test_getopt_long',
+  test_getopt_long_sources,
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+# Same program linked against the system getopt_long(), where there is one,
+# to check the port version behaves the same.
+test_getopt_long_system = executable('test_getopt_long_system',
+  files('test_getopt_long.c'),
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+tests += {
+  'name': 'test_getopt_long',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_getopt_long.pl',
+    ],
+    'deps': [
+      test_getopt_long,
+      test_getopt_long_system,
+    ],
+  },
+}
diff --git a/src/test/modules/test_getopt_long/t/001_getopt_long.pl b/src/test/modules/test_getopt_long/t/001_getopt_long.pl
new file mode 100644
index 00000000000..85c49d7d678
--- /dev/null
+++ b/src/test/modules/test_getopt_long/t/001_getopt_long.pl
@@ -0,0 +1,372 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test the port implementation of getopt_long().
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $longopts = 'alpha,beta:,gamma::,*verbose,alphabet';
+
+# Each case is a paragraph: a "== title" line, optional "optstring:" and
+# "args:" lines (default "ab:" and none), then the expected output lines.
+# An optional final "stderr: REGEX" line gives the expected error message;
+# without it stderr must be empty.  The system getopt_long() words messages
+# differently, so the regexes accept both forms.
+my $cases = <<'EOF';
+== no arguments
+--
+
+== only non-options
+args: x y
+-- x y
+
+== short no arg
+args: -a
+-a
+--
+
+== short attached arg
+args: -bfoo
+-b=foo
+--
+
+== short separate arg
+args: -b foo
+-b=foo
+--
+
+== short bundled
+args: -ab foo
+-a
+-b=foo
+--
+
+== short bundled, attached arg
+args: -abfoo
+-a
+-b=foo
+--
+
+== short arg looks like option
+args: -b -a
+-b=-a
+--
+
+== short missing arg
+args: -b
+?
+--
+stderr: requires an argument -- '?b'?
+
+== short missing arg, silent
+optstring: :ab:
+args: -b
+:
+--
+
+== short missing arg at end of bundle
+args: -ab
+-a
+?
+--
+stderr: requires an argument -- '?b'?
+
+== short unknown
+args: -x
+?
+--
+stderr: (illegal|invalid) option -- '?x'?
+
+== short unknown, silent
+optstring: :ab:
+args: -x
+?
+--
+
+== short unknown in bundle
+args: -axa foo
+-a
+?
+-a
+-- foo
+stderr: (illegal|invalid) option -- '?x'?
+
+== long no arg
+args: --alpha
+--alpha
+--
+
+== long no arg then non-option
+args: --alpha foo
+--alpha
+-- foo
+
+== long no arg, longer name also defined
+args: --alphabet
+--alphabet
+--
+
+== long unknown
+args: --nope
+?
+--
+stderr: illegal option -- nope|unrecognized option [`']--nope'
+
+== long unknown, silent
+optstring: :ab:
+args: --nope
+?
+--
+
+== long unknown then option
+args: --nope -a
+?
+-a
+--
+stderr: illegal option -- nope|unrecognized option [`']--nope'
+
+== long required, equals
+args: --beta=foo
+--beta=foo
+--
+
+== long required, separate
+args: --beta foo
+--beta=foo
+--
+
+== long required, empty
+args: --beta=
+--beta=
+--
+
+== long required, value contains equals
+args: --beta=a=b
+--beta=a=b
+--
+
+== long required, double dash as value
+args: --beta --
+--beta=--
+--
+
+== long required, separate then more
+args: --beta foo -a bar
+--beta=foo
+-a
+-- bar
+
+== long required, value looks like option
+args: --beta --alpha
+--beta=--alpha
+--
+
+== long required, missing
+args: --beta
+?
+--
+stderr: requires an argument -- beta|[`']--beta' requires an argument
+
+== long required, missing, silent
+optstring: :ab:
+args: --beta
+:
+--
+
+== long required, missing after others
+args: -a --beta
+-a
+?
+--
+stderr: requires an argument -- beta|[`']--beta' requires an argument
+
+== long optional, alone
+args: --gamma
+--gamma
+--
+
+== long optional, alone, silent
+optstring: :ab:
+args: --gamma
+--gamma
+--
+
+== long optional, equals
+args: --gamma=foo
+--gamma=foo
+--
+
+== long optional, empty
+args: --gamma=
+--gamma=
+--
+
+== long optional, separate not consumed
+args: --gamma foo
+--gamma
+-- foo
+
+== long optional, separate not consumed, silent
+optstring: :ab:
+args: --gamma foo
+--gamma
+-- foo
+
+== long optional, first
+args: --gamma -a foo
+--gamma
+-a
+-- foo
+
+== long optional, first with equals
+args: --gamma=x -a foo
+--gamma=x
+-a
+-- foo
+
+== long optional, middle
+args: -a --gamma -b foo
+-a
+--gamma
+-b=foo
+--
+
+== long optional, middle before non-option
+args: -a --gamma foo -b bar
+-a
+--gamma
+-b=bar
+-- foo
+
+== long optional between non-options
+args: foo --gamma bar -a
+--gamma
+-a
+-- foo bar
+
+== long optional, last
+args: -a -b foo --gamma
+-a
+-b=foo
+--gamma
+--
+
+== long optional, last, silent
+optstring: :ab:
+args: -a --gamma
+-a
+--gamma
+--
+
+== long optional followed by long
+args: --gamma --alpha
+--gamma
+--alpha
+--
+
+== long optional followed by long required
+args: --gamma --beta foo
+--gamma
+--beta=foo
+--
+
+== long optional followed by lone dash
+args: --gamma -
+--gamma
+-- -
+
+== long optional repeated
+args: --gamma --gamma=1 --gamma
+--gamma
+--gamma=1
+--gamma
+--
+
+== long optional then double dash
+args: --gamma -- foo
+--gamma
+-- foo
+
+== long flag
+args: --verbose
+flag:--verbose
+--
+
+== long flag among others
+args: -a --verbose --gamma
+-a
+flag:--verbose
+--gamma
+--
+
+== double dash ends options
+args: -a -- -b foo
+-a
+-- -b foo
+
+== double dash first
+args: -- -a
+-- -a
+
+== double dash last
+args: -a --
+-a
+--
+
+== double dash repeated
+args: -a -- -- foo
+-a
+-- -- foo
+
+== non-options reordered to end
+args: foo -a bar -b baz qux
+-a
+-b=baz
+-- foo bar qux
+
+== lone dash is a non-option
+args: -a - -b x
+-a
+-b=x
+-- -
+
+== non-options then double dash
+args: foo -a -- -b bar
+-a
+-- foo -b bar
+EOF
+
+# test_getopt_long_system uses the system getopt_long() where there is one;
+# its outputs must match, only the error message wording differs.
+foreach my $exe ('test_getopt_long', 'test_getopt_long_system')
+{
+	foreach my $case (split /\n\n/, $cases)
+	{
+		my @lines = split /\n/, $case;
+		my ($title) = shift(@lines) =~ /^== (.*)/;
+		my $optstring = 'ab:';
+		$optstring = $1 if $lines[0] =~ /^optstring: (.*)/ and shift @lines;
+		my @args;
+		@args = split ' ', $1 if $lines[0] =~ /^args: (.*)/ and shift @lines;
+		my $stderr_re;
+		$stderr_re = $1 if $lines[-1] =~ /^stderr: (.*)/ and pop @lines;
+
+		my ($stdout, $stderr) =
+		  run_command([ $exe, $optstring, $longopts, @args ]);
+		is($stdout, join("\n", @lines), "$exe $title: output");
+		if (defined $stderr_re)
+		{
+			like($stderr, qr/$stderr_re/, "$exe $title: stderr");
+		}
+		else
+		{
+			is($stderr, '', "$exe $title: no stderr");
+		}
+	}
+}
+
+done_testing();
diff --git a/src/test/modules/test_getopt_long/test_getopt_long.c b/src/test/modules/test_getopt_long/test_getopt_long.c
new file mode 100644
index 00000000000..9027e55eb52
--- /dev/null
+++ b/src/test/modules/test_getopt_long/test_getopt_long.c
@@ -0,0 +1,149 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_getopt_long.c
+ *    Test program for the src/port implementation of getopt_long()
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/modules/test_getopt_long/test_getopt_long.c
+ *
+ * Usage: test_getopt_long OPTSTRING LONGOPTS [ARG ...]
+ *
+ * LONGOPTS is a comma separated list of long option names, each optionally
+ * followed by ":" (required argument) or "::" (optional argument), and
+ * optionally prefixed with "*" to make getopt_long() set a flag variable
+ * instead of returning a value.  The remaining ARGs are parsed with
+ * getopt_long() and each return is printed on its own line:
+ *
+ *   -x            short option x
+ *   -x=VALUE      short option x with argument VALUE
+ *   --name        long option name
+ *   --name=VALUE  long option name with argument VALUE
+ *   flag:--name   long option name, delivered via its flag pointer
+ *   ?             BADCH (unknown option or missing argument)
+ *   :             BADARG (missing argument, optstring starts with ':')
+ *
+ * After getopt_long() returns -1, a final line "--" lists the remaining
+ * (non-option) arguments, space separated.
+ *
+ * src/port/getopt_long.c is compiled into this program directly so the
+ * port implementation is tested even on platforms where libpgport would
+ * normally use the system's getopt_long().  test_getopt_long_system is the
+ * same program linked without it, for comparison against the system one.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "getopt_long.h"
+
+#define MAX_LONGOPTS 32
+
+/* long options without a flag return this plus their index */
+#define LONGOPT_BASE 1000
+
+static struct option longopts[MAX_LONGOPTS + 1];
+static char *longnames[MAX_LONGOPTS];
+static int	flagval;
+
+static void
+parse_longopts(char *spec)
+{
+	int			n = 0;
+	char	   *tok;
+
+	for (tok = strtok(spec, ","); tok != NULL; tok = strtok(NULL, ","))
+	{
+		struct option *opt = &longopts[n];
+		char	   *colon;
+
+		if (n >= MAX_LONGOPTS)
+		{
+			fprintf(stderr, "too many long options\n");
+			exit(1);
+		}
+
+		if (tok[0] == '*')
+		{
+			tok++;
+			opt->flag = &flagval;
+			opt->val = n + 1;
+		}
+		else
+		{
+			opt->flag = NULL;
+			opt->val = LONGOPT_BASE + n;
+		}
+
+		colon = strchr(tok, ':');
+		if (colon == NULL)
+			opt->has_arg = no_argument;
+		else if (colon[1] == ':')
+			opt->has_arg = optional_argument;
+		else
+			opt->has_arg = required_argument;
+		if (colon != NULL)
+			*colon = '\0';
+
+		longnames[n] = tok;
+		opt->name = tok;
+		n++;
+	}
+
+	longopts[n].name = NULL;
+}
+
+int
+main(int argc, char **argv)
+{
+	const char *optstring;
+	char	  **args;
+	int			nargs;
+	int			c;
+
+	if (argc < 3)
+	{
+		fprintf(stderr, "Usage: %s OPTSTRING LONGOPTS [ARG ...]\n", argv[0]);
+		exit(1);
+	}
+
+	optstring = argv[1];
+	parse_longopts(argv[2]);
+
+	/* build the argv that getopt_long() will see, and may reorder */
+	nargs = argc - 2;
+	args = palloc((nargs + 1) * sizeof(char *));
+	args[0] = argv[0];
+	for (int i = 1; i < nargs; i++)
+		args[i] = argv[i + 2];
+	args[nargs] = NULL;
+
+	while ((c = getopt_long(nargs, args, optstring, longopts, NULL)) != -1)
+	{
+		if (c == 0)
+		{
+			printf("flag:--%s\n", longnames[flagval - 1]);
+			continue;
+		}
+
+		if (c >= LONGOPT_BASE)
+			printf("--%s", longnames[c - LONGOPT_BASE]);
+		else if (c == '?' || c == ':')
+			printf("%c", c);
+		else
+			printf("-%c", c);
+
+		if (optarg != NULL)
+			printf("=%s", optarg);
+		printf("\n");
+	}
+
+	printf("--");
+	for (int i = optind; i < nargs; i++)
+		printf(" %s", args[i]);
+	printf("\n");
+
+	return 0;
+}
-- 
2.17.1

