stdio-common: Verify printf format tests with Python rather than AWK

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.

Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>
This commit is contained in:
Matt Turner
2026-09-02 12:37:19 -05:00
committed by Adhemerval Zanella
parent e1643c8df3
commit dc6e310e79
19 changed files with 478 additions and 350 deletions
-4
View File
@@ -522,10 +522,6 @@ build the GNU C Library:
version 5.4.1 is the newest verified to work to build the GNU C version 5.4.1 is the newest verified to work to build the GNU C
Library. 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 * GNU 'bison' 2.7 or later
'bison' is used to generate the 'yacc' parser code in the 'intl' 'bison' is used to generate the 'yacc' parser code in the 'intl'
-4
View File
@@ -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 As of release time, @code{gawk} version 5.4.1 is the newest verified
to work to build @theglibc{}. 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 @item
GNU @code{bison} 2.7 or later GNU @code{bison} 2.7 or later
+2 -2
View File
@@ -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. # We can't split a quoted recipe line, so do it via an auxiliary variable.
make-tst-printf-format-out = \ make-tst-printf-format-out = \
AWK='$(AWK)' BASH='$(BASH)' \ PYTHON='$(PYTHON)' BASH='$(BASH)' \
$(BASH) $< $@ $(common-objpfx) \ $(BASH) $< $@ $(common-objpfx) \
'$(run-program-prefix-before-env) \ '$(run-program-prefix-before-env) \
$(run-program-env) \ $(run-program-env) \
@@ -642,7 +642,7 @@ make-tst-printf-format-out = \
$(run-program-prefix-after-env)' $(run-program-prefix-after-env)'
$(objpfx)tst-printf-format-%.out: \ $(objpfx)tst-printf-format-%.out: \
tst-printf-format.sh $(foreach c,$(convs),tst-printf-format-$(c).sh) \ 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) > $@; \ $(make-tst-printf-format-out) > $@; \
$(evaluate-test) $(evaluate-test)
+1 -3
View File
@@ -23,12 +23,10 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}"
echo Verifying c echo Verifying c
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-c c | ${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 || head -n 1 | sed "s/^/Conversion c output error, first line:\n/") 2>&1 ||
exit 1 exit 1
+1 -3
View File
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}"
status=0 status=0
for f in d i; do for f in d i; do
@@ -32,7 +30,7 @@ for f in d i; do
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-char $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+9 -45
View File
@@ -24,51 +24,15 @@ format=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
# For floating-point formats we need to use the bignum mode even if the status=0
# 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=77 echo Verifying $format
(set -o pipefail
# Verify that AWK can handle the range required. It also catches: ${test_program_prefix} \
# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-double $format |
# message produced where bignum support is not there, which is the ${PYTHON:-python3} tst-printf-format.py 2>&1 |
# only indication as the use of '-M' does not affect the exit status head -n 1 |
# in this case. sed "s/^/Conversion $format output error, first line:\n/") 2>&1 ||
ref="-1.79769313486231570814527423731704357e+308" status=1
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
exit $status exit $status
+2 -17
View File
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}" 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="-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; }
for f in d i; do for f in d i; do
echo Verifying $f echo Verifying $f
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-int $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+9 -45
View File
@@ -24,51 +24,15 @@ format=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
# For floating-point formats we need to use the bignum mode even if the status=0
# 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=77 echo Verifying $format
(set -o pipefail
# Verify that AWK can handle the range required. It also catches: ${test_program_prefix} \
# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ldouble $format |
# message produced where bignum support is not there, which is the ${PYTHON:-python3} tst-printf-format.py 2>&1 |
# only indication as the use of '-M' does not affect the exit status head -n 1 |
# in this case. sed "s/^/Conversion $format output error, first line:\n/") 2>&1 ||
ref="-1.18973149535723176508575932662800702e+4932" status=1
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
exit $status exit $status
+2 -17
View File
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}" 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="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; }
for f in d i; do for f in d i; do
echo Verifying $f echo Verifying $f
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-llong $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+2 -17
View File
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}" 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="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; }
for f in d i; do for f in d i; do
echo Verifying $f echo Verifying $f
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-long $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+1 -3
View File
@@ -23,12 +23,10 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}"
echo Verifying s echo Verifying s
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-s s | ${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 || head -n 1 | sed "s/^/Conversion s output error, first line:\n/") 2>&1 ||
exit 1 exit 1
+1 -3
View File
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}"
status=0 status=0
for f in d i; do for f in d i; do
@@ -32,7 +30,7 @@ for f in d i; do
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-short $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+1 -3
View File
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}"
status=0 status=0
for f in o u x X; do for f in o u x X; do
@@ -32,7 +30,7 @@ for f in o u x X; do
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-uchar $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+2 -17
View File
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}" 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="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; }
for f in o u x X; do for f in o u x X; do
echo Verifying $f echo Verifying $f
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-uint $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+2 -17
View File
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}" 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="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; }
for f in o u x X; do for f in o u x X; do
echo Verifying $f echo Verifying $f
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ullong $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+2 -17
View File
@@ -23,29 +23,14 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}" 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="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; }
for f in o u x X; do for f in o u x X; do
echo Verifying $f echo Verifying $f
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ulong $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
+1 -3
View File
@@ -23,8 +23,6 @@ xprintf=$1; shift
common_objpfx=$1; shift common_objpfx=$1; shift
test_program_prefix=$1; shift test_program_prefix=$1; shift
AWK="env LC_ALL=C ${AWK:-awk}"
status=0 status=0
for f in o u x X; do for f in o u x X; do
@@ -32,7 +30,7 @@ for f in o u x X; do
(set -o pipefail (set -o pipefail
${test_program_prefix} \ ${test_program_prefix} \
${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ushort $f | ${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 || head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 ||
status=1 status=1
done done
-130
View File
@@ -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
}
+440
View File
@@ -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)