[PATCH 1/7] stdio-common: Verify printf format tests with Python rather than AWK
Matt Turner
mattst88@gmail.com
Mon Aug 24 22:16:06 GMT 2026
The formatted printf output tests verify their records against GNU AWK,
relying on it to provide an implementation of format processing that is
independent from ours.
AWK has to run in the bignum mode for the floating-point conversions,
because otherwise it uses the system sprintf(3) internally and we end up
verifying our code against itself. That in turn makes gawk compiled
with MPFR support a requirement for testing the library at all. Beyond
that gawk mishandles a number of cases which the AWK script then has to
undo by hand: the extraneous leading 0 produced for the alternative form
with the octal conversion, the 0 produced where no characters are
expected for the hexadecimal conversions, the missing + and space
characters for a zero value with the precision of zero, and a collection
of sign, flag and field width anomalies for Inf and NaN values. Each
such workaround suppresses whatever we might get wrong in the same
place. The a, A, b, and B conversions cannot be verified at all,
because gawk either does not handle them or produces different output.
Replace the AWK script with an equivalent one written in Python, which
is already a requirement for building the library. Rather than calling
into any formatting routine it computes the reference output directly,
using exact integer and rational arithmetic. Working exactly means the
result does not depend on the range or precision of any host
floating-point type, so the wider types are handled without
arbitrary-precision arithmetic having to be built into the interpreter,
and none of the workarounds listed above are needed: the corner cases
they cover are computed correctly. The same property removes the reason
the a, A, b, and B conversions had to be left out; adding them is left
for the commits that follow. Rendered digits are memoized per value,
without which the exact arithmetic makes the long double conversions
slower than AWK.
As the capability probes only ever detected gawk build options, they go
away along with the unsupported status they could produce, so the f and
F conversions are now always verified rather than silently skipped where
gawk was built without them. Drop the corresponding note on MPFR from
the installation instructions.
Tested on x86_64-linux-gnu, where all 576 results continue to pass.
---
INSTALL | 4 -
manual/install.texi | 4 -
stdio-common/Makefile | 4 +-
stdio-common/tst-printf-format-c.sh | 4 +-
stdio-common/tst-printf-format-char.sh | 4 +-
stdio-common/tst-printf-format-double.sh | 54 +--
stdio-common/tst-printf-format-int.sh | 19 +-
stdio-common/tst-printf-format-ldouble.sh | 54 +--
stdio-common/tst-printf-format-llong.sh | 19 +-
stdio-common/tst-printf-format-long.sh | 19 +-
stdio-common/tst-printf-format-s.sh | 4 +-
stdio-common/tst-printf-format-short.sh | 4 +-
stdio-common/tst-printf-format-uchar.sh | 4 +-
stdio-common/tst-printf-format-uint.sh | 19 +-
stdio-common/tst-printf-format-ullong.sh | 19 +-
stdio-common/tst-printf-format-ulong.sh | 19 +-
stdio-common/tst-printf-format-ushort.sh | 4 +-
stdio-common/tst-printf-format.awk | 130 -------
stdio-common/tst-printf-format.py | 440 ++++++++++++++++++++++
19 files changed, 478 insertions(+), 350 deletions(-)
delete mode 100644 stdio-common/tst-printf-format.awk
create mode 100644 stdio-common/tst-printf-format.py
diff --git ./INSTALL ./INSTALL
index bafae84f75..0f3745d3b2 100644
--- ./INSTALL
+++ ./INSTALL
@@ -522,10 +522,6 @@ build the GNU C Library:
version 5.4.1 is the newest verified to work to build the GNU C
Library.
- Testing the GNU C Library requires 'gawk' to be compiled with
- support for high precision arithmetic via the 'MPFR'
- multiple-precision floating-point computation library.
-
* GNU 'bison' 2.7 or later
'bison' is used to generate the 'yacc' parser code in the 'intl'
diff --git ./manual/install.texi ./manual/install.texi
index fe4a368385..e2f45b8e6f 100644
--- ./manual/install.texi
+++ ./manual/install.texi
@@ -567,10 +567,6 @@ function, which was introduced in version 3.1.2 of @code{gawk}.
As of release time, @code{gawk} version 5.4.1 is the newest verified
to work to build @theglibc{}.
-Testing the GNU C Library requires @code{gawk} to be compiled with
-support for high precision arithmetic via the @code{MPFR}
-multiple-precision floating-point computation library.
-
@item
GNU @code{bison} 2.7 or later
diff --git ./stdio-common/Makefile ./stdio-common/Makefile
index a948bfde8f..537962ad86 100644
--- ./stdio-common/Makefile
+++ ./stdio-common/Makefile
@@ -633,7 +633,7 @@ $(objpfx)tst-printf.out: tst-printf.sh $(objpfx)tst-printf
# We can't split a quoted recipe line, so do it via an auxiliary variable.
make-tst-printf-format-out = \
- AWK='$(AWK)' BASH='$(BASH)' \
+ PYTHON='$(PYTHON)' BASH='$(BASH)' \
$(BASH) $< $@ $(common-objpfx) \
'$(run-program-prefix-before-env) \
$(run-program-env) \
@@ -642,7 +642,7 @@ make-tst-printf-format-out = \
$(run-program-prefix-after-env)'
$(objpfx)tst-printf-format-%.out: \
tst-printf-format.sh $(foreach c,$(convs),tst-printf-format-$(c).sh) \
- $(foreach f,$(xprintf-srcs),$(objpfx)$(f)) tst-printf-format.awk
+ $(foreach f,$(xprintf-srcs),$(objpfx)$(f)) tst-printf-format.py
$(make-tst-printf-format-out) > $@; \
$(evaluate-test)
diff --git ./stdio-common/tst-printf-format-c.sh ./stdio-common/tst-printf-format-c.sh
index 73d28c5607..70a8f59204 100644
--- ./stdio-common/tst-printf-format-c.sh
+++ ./stdio-common/tst-printf-format-c.sh
@@ -23,12 +23,10 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
echo Verifying c
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-c c |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion c output error, first line:\n/") 2>&1 ||
exit 1
diff --git ./stdio-common/tst-printf-format-char.sh ./stdio-common/tst-printf-format-char.sh
index aa4d211126..ac0f273039 100644
--- ./stdio-common/tst-printf-format-char.sh
+++ ./stdio-common/tst-printf-format-char.sh
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
status=0
for f in d i; do
@@ -32,7 +30,7 @@ for f in d i; do
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-char $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-double.sh ./stdio-common/tst-printf-format-double.sh
index 051e4716c5..3201a419ab 100644
--- ./stdio-common/tst-printf-format-double.sh
+++ ./stdio-common/tst-printf-format-double.sh
@@ -24,51 +24,15 @@ format=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-# For floating-point formats we need to use the bignum mode even if the
-# regular mode would do, because GAWK in the latter mode uses sprintf(3)
-# internally to process the conversion requested, so any bug in our code
-# would then be verified against itself, defeating the objective of doing
-# the verification against an independent implementation.
-AWK="env LC_ALL=C ${AWK:-awk} -M"
+status=0
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="-1.79769313486231570814527423731704357e+308"
-val=$(echo "$ref" | $AWK '{ printf "%.35e\n", $1 }' 2>&1) &&
- test "$val" = "$ref" && status=0
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
-
-# Check for any additional conversions that AWK handles conditionally
-# according to its version and/or the environment it has been built in.
-# The 'A' and 'a' conversions are not suitable to use at this point, as
-# output produced by AWK is different apparently due to a subtlety in
-# rounding, so do not try them.
-declare -A conversion_disabled
-ref="-inf"
-for f in f F; do
- conversion_disabled[$f]=true
- val=$(echo "$ref" | $AWK '{ printf "%'$f'\n", $1 }' 2>&1) &&
- test "${val^^}" = "${ref^^}" && unset conversion_disabled[$f]
-done
-
-if test "${conversion_disabled[$format]+set}" = set; then
- echo Unsupported $format
- status=77
-else
- echo Verifying $format
- (set -o pipefail
- ${test_program_prefix} \
- ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-double $format |
- $AWK -f tst-printf-format.awk 2>&1 |
- head -n 1 |
- sed "s/^/Conversion $format output error, first line:\n/") 2>&1 ||
- status=1
-fi
+echo Verifying $format
+(set -o pipefail
+ ${test_program_prefix} \
+ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-double $format |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
+ head -n 1 |
+ sed "s/^/Conversion $format output error, first line:\n/") 2>&1 ||
+ status=1
exit $status
diff --git ./stdio-common/tst-printf-format-int.sh ./stdio-common/tst-printf-format-int.sh
index e9dcbedc04..983cd9013f 100644
--- ./stdio-common/tst-printf-format-int.sh
+++ ./stdio-common/tst-printf-format-int.sh
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="-2147483648"
-for AWK in "$AWK -M" "$AWK"; do
- val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue
- test "$val" = "$ref" && status=0 && break
-done
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
+status=0
for f in d i; do
echo Verifying $f
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-int $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-ldouble.sh ./stdio-common/tst-printf-format-ldouble.sh
index 7ee097ac0a..2be2a592e0 100644
--- ./stdio-common/tst-printf-format-ldouble.sh
+++ ./stdio-common/tst-printf-format-ldouble.sh
@@ -24,51 +24,15 @@ format=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-# For floating-point formats we need to use the bignum mode even if the
-# regular mode would do, because GAWK in the latter mode uses sprintf(3)
-# internally to process the conversion requested, so any bug in our code
-# would then be verified against itself, defeating the objective of doing
-# the verification against an independent implementation.
-AWK="env LC_ALL=C ${AWK:-awk} -M"
+status=0
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="-1.18973149535723176508575932662800702e+4932"
-val=$(echo "$ref" | $AWK '{ PREC=113; printf "%.35e\n", $1 }' 2>&1) &&
- test "$val" = "$ref" && status=0
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
-
-# Check for any additional conversions that AWK handles conditionally
-# according to its version and/or the environment it has been built in.
-# The 'A' and 'a' conversions are not suitable to use at this point, as
-# output produced by AWK is different apparently due to a subtlety in
-# rounding, so do not try them.
-declare -A conversion_disabled
-ref="-inf"
-for f in f F; do
- conversion_disabled[$f]=true
- val=$(echo "$ref" | $AWK '{ printf "%'$f'\n", $1 }' 2>&1) &&
- test "${val^^}" = "${ref^^}" && unset conversion_disabled[$f]
-done
-
-if test "${conversion_disabled[$format]+set}" = set; then
- echo Unsupported $format
- status=77
-else
- echo Verifying $format
- (set -o pipefail
- ${test_program_prefix} \
- ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ldouble $format |
- $AWK -f tst-printf-format.awk 2>&1 |
- head -n 1 |
- sed "s/^/Conversion $format output error, first line:\n/") 2>&1 ||
- status=1
-fi
+echo Verifying $format
+(set -o pipefail
+ ${test_program_prefix} \
+ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ldouble $format |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
+ head -n 1 |
+ sed "s/^/Conversion $format output error, first line:\n/") 2>&1 ||
+ status=1
exit $status
diff --git ./stdio-common/tst-printf-format-llong.sh ./stdio-common/tst-printf-format-llong.sh
index 98a79660cc..d90af6f881 100644
--- ./stdio-common/tst-printf-format-llong.sh
+++ ./stdio-common/tst-printf-format-llong.sh
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="9223372036854775807"
-for AWK in "$AWK -M" "$AWK"; do
- val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue
- test "$val" = "$ref" && status=0 && break
-done
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
+status=0
for f in d i; do
echo Verifying $f
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-llong $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-long.sh ./stdio-common/tst-printf-format-long.sh
index 89ac3302b8..c00f899aa4 100644
--- ./stdio-common/tst-printf-format-long.sh
+++ ./stdio-common/tst-printf-format-long.sh
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="9223372036854775807"
-for AWK in "$AWK -M" "$AWK"; do
- val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue
- test "$val" = "$ref" && status=0 && break
-done
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
+status=0
for f in d i; do
echo Verifying $f
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-long $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-s.sh ./stdio-common/tst-printf-format-s.sh
index 015c730609..3be2406c6e 100644
--- ./stdio-common/tst-printf-format-s.sh
+++ ./stdio-common/tst-printf-format-s.sh
@@ -23,12 +23,10 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
echo Verifying s
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-s s |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion s output error, first line:\n/") 2>&1 ||
exit 1
diff --git ./stdio-common/tst-printf-format-short.sh ./stdio-common/tst-printf-format-short.sh
index a7df8b8e6d..b474f65b96 100644
--- ./stdio-common/tst-printf-format-short.sh
+++ ./stdio-common/tst-printf-format-short.sh
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
status=0
for f in d i; do
@@ -32,7 +30,7 @@ for f in d i; do
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-short $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-uchar.sh ./stdio-common/tst-printf-format-uchar.sh
index 356de4217d..d0d3627e8d 100644
--- ./stdio-common/tst-printf-format-uchar.sh
+++ ./stdio-common/tst-printf-format-uchar.sh
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
status=0
for f in o u x X; do
@@ -32,7 +30,7 @@ for f in o u x X; do
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-uchar $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-uint.sh ./stdio-common/tst-printf-format-uint.sh
index b496047a49..2c32133212 100644
--- ./stdio-common/tst-printf-format-uint.sh
+++ ./stdio-common/tst-printf-format-uint.sh
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="4294967295"
-for AWK in "$AWK -M" "$AWK"; do
- val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue
- test "$val" = "$ref" && status=0 && break
-done
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
+status=0
for f in o u x X; do
echo Verifying $f
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-uint $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-ullong.sh ./stdio-common/tst-printf-format-ullong.sh
index f030f66a24..1136f61bf9 100644
--- ./stdio-common/tst-printf-format-ullong.sh
+++ ./stdio-common/tst-printf-format-ullong.sh
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="18446744073709551615"
-for AWK in "$AWK -M" "$AWK"; do
- val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue
- test "$val" = "$ref" && status=0 && break
-done
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
+status=0
for f in o u x X; do
echo Verifying $f
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ullong $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-ulong.sh ./stdio-common/tst-printf-format-ulong.sh
index 7102575ed2..15802cec63 100644
--- ./stdio-common/tst-printf-format-ulong.sh
+++ ./stdio-common/tst-printf-format-ulong.sh
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
-status=77
-
-# Verify that AWK can handle the range required. It also catches:
-# "gawk: warning: -M ignored: MPFR/GMP support not compiled in"
-# message produced where bignum support is not there, which is the
-# only indication as the use of '-M' does not affect the exit status
-# in this case.
-ref="18446744073709551615"
-for AWK in "$AWK -M" "$AWK"; do
- val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue
- test "$val" = "$ref" && status=0 && break
-done
-
-test $status -eq 0 || { echo "No working AWK found" && exit $status; }
+status=0
for f in o u x X; do
echo Verifying $f
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ulong $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format-ushort.sh ./stdio-common/tst-printf-format-ushort.sh
index 5f612b5398..b08ae2906c 100644
--- ./stdio-common/tst-printf-format-ushort.sh
+++ ./stdio-common/tst-printf-format-ushort.sh
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift
test_program_prefix=$1; shift
-AWK="env LC_ALL=C ${AWK:-awk}"
-
status=0
for f in o u x X; do
@@ -32,7 +30,7 @@ for f in o u x X; do
(set -o pipefail
${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ushort $f |
- $AWK -f tst-printf-format.awk 2>&1 |
+ ${PYTHON:-python3} tst-printf-format.py 2>&1 |
head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1
done
diff --git ./stdio-common/tst-printf-format.awk ./stdio-common/tst-printf-format.awk
deleted file mode 100644
index 57bea12621..0000000000
--- ./stdio-common/tst-printf-format.awk
+++ /dev/null
@@ -1,130 +0,0 @@
-# Testing of printf conversions.
-# Copyright (C) 2024-2026 Free Software Foundation, Inc.
-# This file is part of the GNU C Library.
-
-# The GNU C Library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-
-# The GNU C Library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with the GNU C Library; if not, see
-# <https://www.gnu.org/licenses/>.
-
-BEGIN {
- FS = ":"
-}
-
-/^prec:/ {
- PREC = $2
- next
-}
-
-/^val:/ {
- val = $2
- # Prepend "+" for +Inf or +NaN value lacking a sign, because gawk
- # interpretes them as strings rather than numeric values in the
- # non-bignum mode unless a sign has been explicitly given. Keep
- # original 'val' for reporting.
- value = gensub(/^(INF|NAN|inf|nan)/, "+\\1", 1, val)
- # Neither changes between the conversions applied to this value.
- value_infnan = value ~ /(INF|NAN|inf|nan)/
- value_zero = value == 0
- next
-}
-
-/^%/ {
- # Discard the trailing empty field, used to improve legibility of data.
- input = $--NF
- format = $1
- width = $2
- precision = "." $(NF - 1)
- # Discard any negative precision, which is to be taken as if omitted.
- sub(/\.-.*/, "", precision)
- # Simplify handling and paste the precision and width specified as
- # arguments to '*' directly into the format.
- sub(/\.\*/, precision, format)
- sub(/\*/, width, format)
- # Discard length modifiers. They are only relevant to C data types.
- sub(/([DHLjhltz]|wf?[1-9][0-9]*)/, "", format)
- # Discard the '#' flag with the octal conversion if output starts with
- # 0 in the absence of this flag. In that case no extra 0 is supposed
- # to be produced, but gawk prepends it anyway.
- if (index(format, "#") && format ~ /#.*o/)
- {
- tmpfmt = gensub(/#/, "", "g", format)
- tmpout = sprintf(tmpfmt, value)
- if (tmpout ~ /^ *0/)
- format = tmpfmt
- }
- # Likewise with the hexadecimal conversion where zero value with the
- # precision of zero is supposed to produce no characters, but gawk
- # outputs 0 instead.
- else if (index(format, "#") && format ~ /#.*[Xx]/)
- {
- tmpfmt = gensub(/#/, "", "g", format)
- tmpout = sprintf(tmpfmt, value)
- if (tmpout ~ /^ *$/)
- format = tmpfmt
- }
- # AWK interpretes input opportunistically as a number, which interferes
- # with how the 'c' conversion works: "a" input will result in "a" output
- # however "0" input will result in "^@" output rather than "0". Force
- # the value to be interpreted as a string then, by appending "".
- output = sprintf(format, value "")
- # Make up for various anomalies with the handling of +/-Inf and +/-NaN
- # values and reprint the output produced using the string conversion,
- # with the field width carried over and the relevant flags handled by
- # hand.
- if (value_infnan && format ~ /[EFGefg]/)
- {
- minus = format ~ /-/ ? "-" : ""
- sign = value ~ /-/ ? "-" : format ~ /\+/ ? "+" : format ~ / / ? " " : ""
- if (format ~ /^%[^\.1-9]*[1-9][0-9]*/)
- width = gensub(/^%[^\.1-9]*([1-9][0-9]*).*$/, "\\1", 1, format)
- else
- width = ""
- output = gensub(/[-+ ]/, "", "g", output)
- output = sprintf("%" minus width "s", sign output)
- }
- # Produce "+" where the '+' flag has been used with a signed integer
- # conversion for zero value, observing any field width in effect.
- # In that case "+" is always supposed to be produced, but with the
- # precision of zero gawk in the non-bignum mode produces any padding
- # requested only.
- else if (value_zero && format ~ /\+.*[di]/)
- {
- output = gensub(/^( *) $/, format ~ /-/ ? "+\\1" : "\\1+", 1, output)
- output = gensub(/^$/, "+", 1, output)
- }
- # Produce " " where the space flag has been used with a signed integer
- # conversion for zero value. In that case at least one " " is
- # supposed to be produced, but with the precision of zero gawk in the
- # non-bignum mode produces nothing.
- else if (value_zero && format ~ / .*[di]/)
- {
- output = gensub(/^$/, " ", 1, output)
- }
- if (output != input)
- {
- printf "(\"%s\"%s%s, %s) => \"%s\", expected \"%s\"\n", \
- $1, (NF > 2 ? ", " $2 : ""), (NF > 3 ? ", " $3 : ""), val, \
- input, output > "/dev/stderr"
- status = 1
- }
- next
-}
-
-{
- printf "unrecognized input: \"%s\"\n", $0 > "/dev/stderr"
- status = 1
-}
-
-END {
- exit status
-}
diff --git ./stdio-common/tst-printf-format.py ./stdio-common/tst-printf-format.py
new file mode 100644
index 0000000000..d10580ae8a
--- /dev/null
+++ ./stdio-common/tst-printf-format.py
@@ -0,0 +1,440 @@
+#!/usr/bin/python3
+# Verification of formatted printf output.
+# Copyright (C) 2024-2026 Free Software Foundation, Inc.
+# This file is part of the GNU C Library.
+#
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+"""Verify records of formatted printf output.
+
+The record stream produced by tst-printf-format-skeleton.c is read from
+standard input and each record's output is checked against a reference
+value computed here. Diagnostics for records that do not match are written
+to standard error and a nonzero exit status is returned.
+
+This is deliberately an independent implementation of the ISO C format
+processing rules: every conversion is computed with exact integer and
+rational arithmetic and nothing defers to the C library, so a bug in printf
+cannot verify itself. Working exactly also means no dependence on the
+range or precision of any host floating-point type, so the wider types are
+handled without arbitrary-precision support having to be built into the
+interpreter.
+"""
+
+import io
+import os
+import sys
+from fractions import Fraction
+
+# Conversions grouped by the C type of the corresponding argument.
+FLOAT_CONVS = frozenset("eEfFgG")
+INT_CONVS = frozenset("diouxX")
+
+# The conversion specifier selects the base an integer is written in.
+INT_FORMATS = {"o": "%o", "u": "%d", "x": "%x", "X": "%X"}
+
+# Flags and length modifiers accepted in a conversion specification.
+FLAG_CHARS = "-+ #0"
+LENGTH_CHARS = "hlLqjzt"
+
+# log10(2), used only to seed the decimal exponent search.
+LOG10_2 = 0.3010299956639812
+
+
+def diagnose(text):
+ """Report TEXT on standard error.
+
+ Records hold arbitrary bytes, taken as Latin-1 on the way in, so put
+ them back the same way rather than through whatever encoding the
+ locale would otherwise select."""
+ sys.stderr.buffer.write(text.encode("latin-1") + b"\n")
+ sys.stderr.buffer.flush()
+
+
+def round_ratio(num, den):
+ """Round the non-negative ratio NUM/DEN to the nearest integer, with
+ ties going to even, matching the default rounding mode."""
+ quot, rem = divmod(num, den)
+ rem *= 2
+ if rem > den or (rem == den and quot & 1):
+ quot += 1
+ return quot
+
+
+def scale_ratio(num, den, power):
+ """Return NUM/DEN multiplied by 10**POWER, as a pair of integers."""
+ if power >= 0:
+ return num * 10 ** power, den
+ return num, den * 10 ** -power
+
+
+def decimal_to_binary(text, mant_bits):
+ """Convert the decimal string TEXT to the nearest value having MANT_BITS
+ of significand, returned as an exact Fraction.
+
+ The generator prints reference values with enough digits to reproduce
+ the original, so this recovers the exact value converted."""
+ fr = Fraction(text)
+ if fr == 0:
+ return fr
+ neg = fr < 0
+ num, den = (-fr.numerator if neg else fr.numerator), fr.denominator
+ # Scale so that 2**(mant_bits-1) <= num/den < 2**mant_bits.
+ exp = num.bit_length() - den.bit_length() - mant_bits
+ if exp >= 0:
+ den <<= exp
+ else:
+ num <<= -exp
+ while num >= den << mant_bits:
+ den <<= 1
+ exp += 1
+ while num < den << (mant_bits - 1):
+ num <<= 1
+ exp -= 1
+ mant = round_ratio(num, den)
+ if mant >> mant_bits:
+ mant >>= 1
+ exp += 1
+ fr = Fraction(mant << exp) if exp >= 0 else Fraction(mant, 1 << -exp)
+ return -fr if neg else fr
+
+
+def decimal_digits(fr, ndigits):
+ """Round the positive Fraction FR to NDIGITS significant decimal digits.
+
+ Return the digit string and the decimal exponent X such that the value
+ is D[0].D[1:] * 10**X, i.e. X is the exponent an 'e' conversion prints."""
+ num, den = fr.numerator, fr.denominator
+ # Seed from the binary magnitude; the estimate is within one decade.
+ x = int((num.bit_length() - den.bit_length()) * LOG10_2)
+ while True:
+ hi_num, hi_den = scale_ratio(num, den, -(x + 1))
+ if hi_num < hi_den:
+ break
+ x += 1
+ while True:
+ lo_num, lo_den = scale_ratio(num, den, -x)
+ if lo_num >= lo_den:
+ break
+ x -= 1
+ digits = round_ratio(*scale_ratio(num, den, ndigits - 1 - x))
+ if digits >= 10 ** ndigits:
+ # Rounding carried into the next decade, e.g. 9.99 -> 10.0.
+ digits //= 10
+ x += 1
+ return str(digits).zfill(ndigits), x
+
+
+class Spec:
+ """A parsed conversion specification."""
+
+ __slots__ = ("minus", "plus", "space", "alt", "zero", "width", "prec",
+ "conv", "neg_zero")
+
+ def __init__(self, fmt, args):
+ """Parse FMT, taking values for any '*' from ARGS in order."""
+ self.minus = self.plus = self.space = self.alt = self.zero = False
+ self.neg_zero = False
+ i = 1 # skip '%'
+ while i < len(fmt) and fmt[i] in FLAG_CHARS:
+ char = fmt[i]
+ self.minus |= char == "-"
+ self.plus |= char == "+"
+ self.space |= char == " "
+ self.alt |= char == "#"
+ self.zero |= char == "0"
+ i += 1
+
+ self.width = 0
+ if i < len(fmt) and fmt[i] == "*":
+ self.width = int(args.pop(0))
+ i += 1
+ # A negative field width is a '-' flag with a positive width.
+ if self.width < 0:
+ self.minus = True
+ self.width = -self.width
+ else:
+ start = i
+ while i < len(fmt) and fmt[i].isdigit():
+ i += 1
+ if i > start:
+ self.width = int(fmt[start:i])
+
+ self.prec = None
+ if i < len(fmt) and fmt[i] == ".":
+ i += 1
+ if i < len(fmt) and fmt[i] == "*":
+ self.prec = int(args.pop(0))
+ i += 1
+ # A negative precision is taken as if it were omitted.
+ if self.prec < 0:
+ self.prec = None
+ else:
+ start = i
+ while i < len(fmt) and fmt[i].isdigit():
+ i += 1
+ self.prec = int(fmt[start:i]) if i > start else 0
+
+ # Length modifiers only select the C type of the argument, which the
+ # generator has already applied; they do not affect the output.
+ while i < len(fmt) and fmt[i] in LENGTH_CHARS:
+ i += 1
+ self.conv = fmt[i]
+
+ def pad(self, body, zero_ok=True, sign=""):
+ """Apply the field width to SIGN followed by BODY."""
+ count = self.width - len(sign) - len(body)
+ if count <= 0:
+ return sign + body
+ if self.minus:
+ return sign + body + " " * count
+ if self.zero and zero_ok:
+ return sign + "0" * count + body
+ return " " * count + sign + body
+
+ def sign_of(self, neg):
+ """The character introducing a signed conversion of a value whose
+ sign is NEG, honoring the '+' and space flags."""
+ if neg:
+ return "-"
+ if self.plus:
+ return "+"
+ if self.space:
+ return " "
+ return ""
+
+
+def convert_int(value, spec):
+ conv = spec.conv
+ if conv in "di":
+ neg = value < 0
+ digits = str(-value if neg else value)
+ sign = spec.sign_of(neg)
+ else:
+ digits = INT_FORMATS[conv] % value
+ sign = ""
+ if spec.prec is not None:
+ # A zero value converted with a precision of zero produces no
+ # characters at all.
+ if value == 0 and spec.prec == 0:
+ digits = ""
+ digits = digits.rjust(spec.prec, "0")
+ if spec.alt:
+ if conv == "o" and not digits.startswith("0"):
+ digits = "0" + digits
+ elif conv in "xX" and value != 0:
+ # The base prefix precedes any '0' flag padding, so it pads
+ # along with the sign rather than with the digits.
+ sign = "0x" if conv == "x" else "0X"
+ # An explicit precision defeats the '0' flag.
+ return spec.pad(digits, zero_ok=spec.prec is None, sign=sign)
+
+
+def convert_string(value, spec):
+ if spec.prec is not None:
+ value = value[:spec.prec]
+ return spec.pad(value, zero_ok=False)
+
+
+def convert_char(value, spec):
+ return spec.pad(value[:1], zero_ok=False)
+
+
+def convert_special(kind, neg, spec):
+ """Convert an infinity or a NaN. The field is padded with spaces
+ whether or not the '0' flag was given, and the alternative form has no
+ effect."""
+ body = kind.upper() if spec.conv in "EFG" else kind
+ return spec.pad(body, zero_ok=False, sign=spec.sign_of(neg))
+
+
+def render_f(fr, prec, alt, strip):
+ """Render the non-negative Fraction FR in the style of 'f'."""
+ scaled = round_ratio(*scale_ratio(fr.numerator, fr.denominator, prec))
+ text = str(scaled).zfill(prec + 1)
+ if prec:
+ whole, frac = text[:-prec], text[-prec:]
+ else:
+ whole, frac = text, ""
+ if strip:
+ frac = frac.rstrip("0")
+ if frac or alt:
+ return whole + "." + frac
+ return whole
+
+
+def render_e(fr, prec, alt, upper, strip):
+ """Render the non-negative Fraction FR in the style of 'e'."""
+ if fr == 0:
+ digits, exp = "0" * (prec + 1), 0
+ else:
+ digits, exp = decimal_digits(fr, prec + 1)
+ whole, frac = digits[0], digits[1:]
+ if strip:
+ frac = frac.rstrip("0")
+ mant = whole + "." + frac if frac or alt else whole
+ return "%s%s%s%02d" % (mant, "E" if upper else "e",
+ "-" if exp < 0 else "+", abs(exp))
+
+
+def convert_float(value, spec, cache):
+ """Convert VALUE, an exact Fraction. CACHE memoizes rendered digits for
+ the value currently being converted."""
+ conv = spec.conv
+ neg = value < 0 or (value == 0 and spec.neg_zero)
+ fr = -value if value < 0 else value
+ prec = 6 if spec.prec is None else spec.prec
+ upper = conv in "EFG"
+
+ # The generator iterates a large number of flag and field width
+ # combinations over each value, so the same digits are called for over
+ # and over. They depend only on the magnitude, the precision and the
+ # alternative form, which is what keeps the wider types inexpensive
+ # despite the arithmetic being exact.
+ key = (conv, prec, spec.alt)
+ body = cache.get(key)
+ if body is None:
+ if conv in "gG":
+ sig = 1 if prec == 0 else prec
+ _, exp = decimal_digits(fr, sig) if fr != 0 else ("", 0)
+ if -4 <= exp < sig:
+ body = render_f(fr, sig - 1 - exp, spec.alt,
+ strip=not spec.alt)
+ else:
+ body = render_e(fr, sig - 1, spec.alt, upper,
+ strip=not spec.alt)
+ elif conv in "eE":
+ body = render_e(fr, prec, spec.alt, upper, strip=False)
+ else:
+ body = render_f(fr, prec, spec.alt, strip=False)
+ cache[key] = body
+
+ return spec.pad(body, zero_ok=True, sign=spec.sign_of(neg))
+
+
+class Block:
+ """The state a run of records shares: the C type they exercise and the
+ single value they all convert."""
+
+ __slots__ = ("mant_bits", "val_text", "value", "special",
+ "neg_zero", "cache")
+
+ def __init__(self):
+ self.mant_bits = 0
+ self.set_value("")
+
+ def set_value(self, text):
+ """Begin a run of records converting the value written as TEXT."""
+ self.val_text = text
+ self.value = None
+ self.special = None
+ self.neg_zero = False
+ self.cache = {} # digits memoized by convert_float
+
+ def interpret(self, conv):
+ """Interpret the value text as the C type conversion CONV takes."""
+ if conv in FLOAT_CONVS:
+ lowered = self.val_text.lower()
+ if "inf" in lowered or "nan" in lowered:
+ self.special = "nan" if "nan" in lowered else "inf"
+ else:
+ self.value = decimal_to_binary(self.val_text, self.mant_bits)
+ self.neg_zero = self.val_text.lstrip().startswith("-")
+ elif conv in INT_CONVS:
+ self.value = int(self.val_text)
+ else:
+ self.value = self.val_text
+
+ def expect(self, spec):
+ """The output SPEC is required to produce for this block's value."""
+ conv = spec.conv
+ # Which C type the value has follows from the conversion specifier,
+ # which is not known until the first record of the block, so the
+ # text is interpreted here rather than where it was read.
+ if self.value is None and self.special is None:
+ self.interpret(conv)
+ spec.neg_zero = self.neg_zero
+ if conv in FLOAT_CONVS:
+ if self.special is not None:
+ return convert_special(self.special, self.neg_zero, spec)
+ return convert_float(self.value, spec, self.cache)
+ if conv in INT_CONVS:
+ return convert_int(self.value, spec)
+ if conv == "c":
+ return convert_char(self.value, spec)
+ return convert_string(self.value, spec)
+
+
+def check(raw, block):
+ """Check one record against the value BLOCK holds, reporting a mismatch
+ on standard error. Return whether the record was in order."""
+ # Records are colon separated and end with an empty field, included to
+ # make the data easier to read. What precedes the output are the format
+ # and then any width and precision supplied as arguments.
+ fields = raw.split(":")
+ fields.pop()
+ fmt, actual, args = fields[0], fields[-1], fields[1:-1]
+
+ expect = block.expect(Spec(fmt, list(args)))
+ if expect == actual:
+ return True
+ diagnose('("%s"%s, %s) => "%s", expected "%s"'
+ % (fmt, "".join(", " + arg for arg in args), block.val_text,
+ actual, expect))
+ return False
+
+
+def main():
+ # The wider types at the precisions exercised produce integers well past
+ # the default limit on conversion to a string. Versions that predate
+ # the limit have no such call and need no lifting.
+ if hasattr(sys, "set_int_max_str_digits"):
+ sys.set_int_max_str_digits(0)
+
+ # Records hold arbitrary bytes, so read them as Latin-1 rather than
+ # through whatever encoding the locale would otherwise select.
+ stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="latin-1",
+ newline="\n")
+
+ block = Block()
+ status = 0
+
+ for raw in stdin:
+ raw = raw.rstrip("\n")
+
+ if raw.startswith("prec:"):
+ block.mant_bits = int(raw[5:])
+ elif raw.startswith("val:"):
+ block.set_value(raw[4:])
+ elif raw.startswith("%"):
+ if not check(raw, block):
+ status = 1
+ else:
+ diagnose('unrecognized input: "%s"' % raw)
+ status = 1
+
+ return status
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except BrokenPipeError:
+ # The wrappers pipe into 'head', which stops reading once it has
+ # the first diagnostic. Point what is left of standard error at
+ # the null device, so that flushing at exit does not run into the
+ # same failure and report it a second time.
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stderr.fileno())
+ sys.exit(1)
--
2.54.0
More information about the Libc-alpha
mailing list