From 65af82ba5f450f0c073019ebfaa02b4b5337bcae Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Tue, 3 Feb 2026 20:07:53 +0300
Subject: [PATCH] Fix TAP tests for Windows with non-C numeric locales

On Windows with MSVC, strtod() ignores the LC_NUMERIC environment
variable and uses the system's regional settings instead. This causes
TAP tests to fail when passing fractional numbers to psql's \watch
command on systems with non-English locales (e.g., Russian locale uses
',' as the decimal separator).

Add get_decimal_separator() and format_locale_number() methods to
PostgreSQL::Test::Cluster that detect the actual decimal separator by
querying psql with numericlocale enabled, then format numbers
accordingly. The result is cached per node for efficiency.

Update affected tests.

Also adjust pg_test_timing's regex to accept both '.' and ',' in the
"99.9900%" output.

Discussion:
---
 src/bin/pg_test_timing/t/001_basic.pl         |  2 +-
 src/bin/psql/t/001_basic.pl                   | 10 ++--
 src/test/modules/test_misc/t/005_timeouts.pl  |  5 +-
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 46 +++++++++++++++++++
 .../recovery/t/046_checkpoint_logical_slot.pl |  7 +--
 5 files changed, 59 insertions(+), 11 deletions(-)

diff --git a/src/bin/pg_test_timing/t/001_basic.pl b/src/bin/pg_test_timing/t/001_basic.pl
index 430d1ed817f..cb79a43a733 100644
--- a/src/bin/pg_test_timing/t/001_basic.pl
+++ b/src/bin/pg_test_timing/t/001_basic.pl
@@ -46,7 +46,7 @@ like(
 	qr/
 \QTesting timing overhead for 1 second.\E.*
 \QHistogram of timing durations:\E.*
-\QObserved timing durations up to 99.9900%:\E
+\QObserved timing durations up to 99\E[.,]\Q9900%:\E
 /sx,
 	'pg_test_timing: stdout passes sanity check');
 
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 6839f27cbe5..66e42c7b315 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -367,14 +367,15 @@ psql_like(
 	'\copy from with DEFAULT');
 
 # Check \watch
-# Note: the interval value is parsed with locale-aware strtod()
 psql_like(
-	$node, sprintf('SELECT 1 \watch c=3 i=%g', 0.01),
+	$node,
+	sprintf('SELECT 1 \watch c=3 i=%s', $node->format_locale_number(0.01)),
 	qr/1\n1\n1/, '\watch with 3 iterations, interval of 0.01');
 
 # Sub-millisecond wait works, equivalent to 0.
 psql_like(
-	$node, sprintf('SELECT 1 \watch c=3 i=%g', 0.0001),
+	$node,
+	sprintf('SELECT 1 \watch c=3 i=%s', $node->format_locale_number(0.0001)),
 	qr/1\n1\n1/, '\watch with 3 iterations, interval of 0.0001');
 
 # Test zero interval
@@ -403,7 +404,8 @@ psql_like(
 		select now()-backend_start AS howlong
 		from pg_stat_activity
 		where pid = pg_backend_pid()
-	  ) select 123 from x where howlong < '2 seconds' \watch i=%g m=2}, 0.5),
+	  ) select 123 from x where howlong < '2 seconds' \watch i=%s m=2},
+		$node->format_locale_number(0.5)),
 	qr/^123$/,
 	'\watch, 2 minimum rows');
 
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
index c16b7dbf5e6..d9d95e1f19c 100644
--- a/src/test/modules/test_misc/t/005_timeouts.pl
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -46,16 +46,15 @@ my $psql_session = $node->background_psql('postgres');
 
 # The following query will generate a stream of SELECT 1 queries. This is done
 # so to exercise transaction timeout in the presence of short queries.
-# Note: the interval value is parsed with locale-aware strtod()
 $psql_session->query_until(
 	qr/starting_bg_psql/,
 	sprintf(
 		q(\echo starting_bg_psql
 		SET transaction_timeout to '10ms';
 		BEGIN;
-		SELECT 1 \watch %g
+		SELECT 1 \watch %s
 		\q
-), 0.001));
+), $node->format_locale_number(0.001)));
 
 # Wait until the backend enters the timeout injection point. Will get an error
 # here if anything goes wrong.
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..9d23fdf1f05 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2057,6 +2057,52 @@ sub safe_psql
 
 =pod
 
+=item $node->get_decimal_separator()
+
+Detect the locale's decimal separator by querying psql with numericlocale
+enabled. Returns '.' or ',' depending on the locale. The result is cached
+for subsequent calls.
+
+This is useful for formatting numbers to be parsed by psql's locale-aware
+strtod(), such as the interval argument to \watch.
+
+=cut
+
+sub get_decimal_separator
+{
+	my ($self) = @_;
+
+	# Return cached result if available
+	return $self->{_decimal_sep} if defined $self->{_decimal_sep};
+
+	my $result = $self->safe_psql('postgres',
+		"\\pset numericlocale on\n\\pset tuples_only on\nSELECT 0.5");
+	$result =~ s/^0(.)5$/$1/ or $result = '.';
+
+	$self->{_decimal_sep} = $result;
+	return $result;
+}
+
+=pod
+
+=item $node->format_locale_number($num)
+
+Format a number using the locale's decimal separator. This is needed when
+passing numeric arguments to psql commands that use locale-aware parsing,
+such as the interval argument to \watch.
+
+=cut
+
+sub format_locale_number
+{
+	my ($self, $num) = @_;
+	my $sep = $self->get_decimal_separator();
+	(my $str = sprintf("%g", $num)) =~ s/\./$sep/;
+	return $str;
+}
+
+=pod
+
 =item $node->psql($dbname, $sql, %params) => psql_retval
 
 Invoke B<psql> to execute B<$sql> on B<$dbname> and return the return value
diff --git a/src/test/recovery/t/046_checkpoint_logical_slot.pl b/src/test/recovery/t/046_checkpoint_logical_slot.pl
index 1ecb47a8b58..6ee5445fc2f 100644
--- a/src/test/recovery/t/046_checkpoint_logical_slot.pl
+++ b/src/test/recovery/t/046_checkpoint_logical_slot.pl
@@ -55,10 +55,11 @@ $node->safe_psql('postgres', q{checkpoint});
 my $xacts = $node->background_psql('postgres');
 $xacts->query_until(
 	qr/run_xacts/,
-	q(\echo run_xacts
-SELECT 1 \watch 0.1
+	sprintf(
+		q(\echo run_xacts
+SELECT 1 \watch %s
 \q
-));
+), $node->format_locale_number(0.1)));
 
 $node->advance_wal(20);
 
-- 
2.51.2

