#!/usr/bin/perl
# Basic tests for "numfmt".

# Copyright (C) 2012-2026 Free Software Foundation, Inc.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program 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 General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

use strict;

(my $program_name = $0) =~ s|.*/||;
my $prog = 'numfmt';

my $limits = getlimits ();

# TODO: add localization tests with "grouping"
# Turn off localization of executable's output.
@ENV{qw(LANGUAGE LANG LC_ALL)} = ('C') x 3;

my $locale = $ENV{LOCALE_FR_UTF8};
! defined $locale || $locale eq 'none'
  and $locale = 'C';

my $try = "Try '$prog --help' for more information.\n";

my @Tests =
    (
     ['1', '1234',             {OUT => "1234"}],
     ['2', '--from=si 1K',     {OUT => "1000"}],
     ['3', '--from=iec 1K',    {OUT => "1024"}],
     ['4', '--from=auto 1K',   {OUT => "1000"}],
     ['5', '--from=auto 1Ki',  {OUT => "1024"}],
     ['5.1', '--from=iec-i 1Ki',  {OUT => "1024"}],
     ['5.2', '--from=iec-i 1',  {OUT => "1"}],

     ['6', {IN_PIPE => "1234\n"},              {OUT => "1234"}],
     ['6nul', {IN_PIPE => "1234\000foo bar\n"},{OUT => "1234"}],
     ['7', '--from=si', {IN_PIPE => "2K\n"}, {OUT => "2000"}],
     ['7a', '--invalid=fail', {IN_PIPE => "no_NL"}, {OUT => "no_NL"},
              {ERR => "$prog: invalid number: 'no_NL'\n"},
              {EXIT => '2'}],

     ['8',  '--to=si 2000',                   {OUT => "2.0k"}],
     ['9',  '--to=si 2001',                   {OUT => "2.1k"}],
     ['10', '--to=si 1999',                   {OUT => "2.0k"}],
     ['11', '--to=si --round=down   2001',   {OUT => "2.0k"}],
     ['12', '--to=si --round=down   1999',   {OUT => "1.9k"}],
     ['13', '--to=si --round=up 1901',   {OUT => "2.0k"}],
     ['14', '--to=si --round=down   1901',   {OUT => "1.9k"}],
     ['15', '--to=si --round=nearest 1901',   {OUT => "1.9k"}],
     ['16', '--to=si --round=nearest 1945',   {OUT => "1.9k"}],
     ['17', '--to=si --round=nearest 1955',   {OUT => "2.0k"}],

     ['18',  '--to=iec 2048',                  {OUT => "2.0K"}],
     ['19',  '--to=iec 2049',                  {OUT => "2.1K"}],
     ['20', '--to=iec 2047',                   {OUT => "2.0K"}],
     ['21', '--to=iec --round=down   2049',   {OUT => "2.0K"}],
     ['22', '--to=iec --round=down   2047',   {OUT => "1.9K"}],
     ['23', '--to=iec --round=up 2040',   {OUT => "2.0K"}],
     ['24', '--to=iec --round=down   2040',   {OUT => "1.9K"}],
     ['25', '--to=iec --round=nearest 1996',   {OUT => "1.9K"}],
     ['26', '--to=iec --round=nearest 1997',   {OUT => "2.0K"}],
     ['27', '--to=iec-i 2048',                  {OUT => "2.0Ki"}],

     ['neg-1', '-- -1234',                     {OUT => "-1234"}],
     ['neg-2', '--padding=5 -- -1234',         {OUT => "-1234"}],
     ['neg-3', '--padding=6 -- -1234',         {OUT => " -1234"}],
     ['neg-4', '--to=iec -- 9100 -9100',       {OUT => "8.9K\n-8.9K"}],
     ['neg-5', '-- -0.1',                      {OUT => "-0.1"}],
     ['neg-6', '-- -0',                        {OUT => "0"}],
     ['neg-7', '-- -0.-1',
              {ERR => "$prog: invalid number: '-0.-1'\n"},
              {EXIT => '2'}],

     ['float-1', '1.1',                        {OUT => "1.1"}],
     ['float-2', '1.22',                       {OUT => "1.22"}],
     ['float-3', '1.22.',
             {ERR => "$prog: invalid suffix in input: '1.22.'\n"},
             {EXIT => '2'}],

     ['unit-1', '--from-unit=512 4',   {OUT => "2048"}],
     ['unit-2', '--to-unit=512 2048',   {OUT => "4"}],
     ['unit-3', '--from-unit=512 --from=si 4M',   {OUT => "2048000000"}],
     ['unit-4', '--from-unit=512 --from=iec --to=iec 4M',   {OUT => "2.0G"}],
     ['unit-5', '--from-unit=AA --from=iec --to=iec 4M',
             {ERR => "$prog: invalid unit size: 'AA'\n"},
             {EXIT => '1'}],
     ['unit-6', '--from-unit=54W --from=iec --to=iec 4M',
             {ERR => "$prog: invalid unit size: '54W'\n"},
             {EXIT => '1'}],
     ['unit-7', '--from-unit=K 30', {OUT=>"30000"}],
     ['unit-7.1', '--from-unit=Ki 30', {OUT=>"30720"}],
     ['unit-7.2', '--from-unit=i 0',
             {ERR => "$prog: invalid unit size: 'i'\n"},
             {EXIT => '1'}],
     ['unit-7.3', '--from-unit=1i 0',
             {ERR => "$prog: invalid unit size: '1i'\n"},
             {EXIT => '1'}],
     ['unit-8', '--from-unit='.$limits->{UINTMAX_OFLOW}.' --to=iec 30',
             {ERR => "$prog: invalid unit size: '$limits->{UINTMAX_OFLOW}'\n"},
             {EXIT => '1'}],
     ['unit-9', '--from-unit=0 1',
             {ERR => "$prog: invalid unit size: '0'\n"},
             {EXIT => '1'}],
     ['unit-10', '--to-unit=0 1',
             {ERR => "$prog: invalid unit size: '0'\n"},
             {EXIT => '1'}],

     # Test Suffix logic
     ['suf-1', '4000',    {OUT=>'4000'}],
     ['suf-2', '4J',
             {ERR => "$prog: invalid suffix in input: '4J'\n"},
             {EXIT => '2'}],
     ['suf-2.1', '4M',
             {ERR => "$prog: rejecting suffix " .
             "in input: '4M' (consider using --from)\n"},
             {EXIT => '2'}],
     ['suf-3', '--from=si 4M',  {OUT=>'4000000'}],
     ['suf-4', '--from=si 4J',
             {ERR => "$prog: invalid suffix in input: '4J'\n"},
             {EXIT => '2'}],
     ['suf-5', '--from=si 4MJ',
             {ERR => "$prog: invalid suffix in input '4MJ': 'J'\n"},
             {EXIT => '2'}],

     ['suf-6', '--from=iec 4M',  {OUT=>'4194304'}],
     ['suf-7', '--from=auto 4M',  {OUT=>'4000000'}],
     ['suf-8', '--from=auto 4Mi',  {OUT=>'4194304'}],
     ['suf-9', '--from=auto 4MiJ',
             {ERR => "$prog: invalid suffix in input '4MiJ': 'J'\n"},
             {EXIT => '2'}],
     ['suf-10', '--from=auto 4JiJ',
             {ERR => "$prog: invalid suffix in input: '4JiJ'\n"},
             {EXIT => '2'}],

     # characters after a white space are OK - printed as-is
     ['suf-11', '"4 M"',     {OUT=>'4 M'}],

     # Custom suffix
     ['suf-12', '--suffix=Foo 70Foo',               {OUT=>'70Foo'}],
     ['suf-13', '--suffix=Foo 70',                  {OUT=>'70Foo'}],
     ['suf-14', '--suffix=Foo --from=si 70K',       {OUT=>'70000Foo'}],
     ['suf-15', '--suffix=Foo --from=si 70KFoo',    {OUT=>'70000Foo'}],
     ['suf-16', '--suffix=Foo --to=si   7000Foo',    {OUT=>'7.0kFoo'}],
     ['suf-17', '--suffix=Foo --to=si   7000Bar',
              {ERR => "$prog: invalid suffix in input: '7000Bar'\n"},
              {EXIT => '2'}],
     ['suf-18', '--suffix=Foo --to=si   7000FooF',
              {ERR => "$prog: invalid suffix in input: '7000FooF'\n"},
              {EXIT => '2'}],
     ['suf-18.1', '--invalid=ignore --suffix=QWE 12q3QWE', {OUT=>"12q3QWE"}],
     ['suf-18.2', '--invalid=abort --suffix=QWE 12q3QWE',
              {ERR => "$prog: invalid suffix in input: '12q3'\n"},
              {EXIT => '2'}],
     # space(s) between number and suffix.  Note only field 1 is used
     # by default so specify the NUL delimiter to consider the whole "line".
     ['suf-19', "-d '' --from=si '4.0 K'",         {OUT => "4000"}],
     ['suf-20',
      '--suffix=Foo' . 'x' x 122 . 'y 0',
      {OUT => '0Foo' . 'x' x 122 . 'y'}],
     ['suf-21', "-d '' --from=si '4  '",         {OUT => "4"}],
     # Multiple spaces between number and suffix should be rejected
     ['suf-22', "-d '' --from=auto '2  K'",
             {ERR => "$prog: invalid suffix in input: '2  K'\n"},
             {EXIT => 2}],
     # Trailing spaces should be accepted
     ['suf-23', "-d '' --from=auto '2 '",  {OUT=>'2'}],
     ['suf-24', "-d '' --from=auto '2  '", {OUT=>'2'}],
     ['suf-25', "-d '' --from=auto '2K '", {OUT=>'2000'}],

     ## Unit Separator
     # Output with space separator
     ['unit-sep-1', '--to=si --unit-separator=" " 1000',  {OUT=>"1.0 k"}],
     ['unit-sep-2', '--to=iec --unit-separator=" " 1024', {OUT=>"1.0 K"}],
     ['unit-sep-3', '--to=iec-i --unit-separator=" " 2048', {OUT=>"2.0 Ki"}],

     # Output with multi-character separator
     ['unit-sep-4', '--to=si --unit-separator="__" 1000', {OUT=>"1.0__k"}],
     ['unit-sep-5', '--to=iec --unit-separator="::" 2048', {OUT=>"2.0::K"}],

     # Input with space separator
     ['unit-sep-6', '-d "" --from=si --unit-sep=" " "1 K"', {OUT=>"1000"}],
     ['unit-sep-7', '-d "" --from=iec --unit-sep=" " "2 M"', {OUT=>"2097152"}],

     # Input with multi-character separator
     ['unit-sep-8', '-d "" --from=si --unit-separator="  "',
      {IN_PIPE=>"1  K\n2  M\n3  G\n"},
      {OUT=>"1000\n2000000\n3000000000"}],
     ['unit-sep-9', '--from=iec --unit-separator="'."\xC2\xA0".'"',
      {IN_PIPE=>"4\xC2\xA0K\n"}, {OUT=>"4096"}],
     ['unit-sep-10', '--from=iec --unit-separator="::"',
      {IN_PIPE=>"4::K\n"}, {OUT=>"4096"}],

     # input with empty separator
     ['unit-sep-11', '-d "" --from=si --unit-separator=""',
      {IN_PIPE=>"1K\n2M\n3G\n"},
      {OUT=>"1000\n2000000\n3000000000"}],
     ['unit-sep-12', '-d "" --from=si --unit-separator="" "1 K"',
      {ERR=>"$prog: invalid suffix in input: '1 K'\n"},
      {EXIT=>2}],

     # Combined with suffix
     ['unit-sep-13', '--to=si --unit-separator=" " --suffix=B 1000',
      {OUT=>"1.0 kB"}],
     ['unit-sep-14', '--to=si --unit-separator=" " --suffix=" B" 1000',
      {OUT=>"1.0 k B"}],
     ['unit-sep-15', '-d "" --from=si --unit-separator=" " --suffix=B',
      {IN_PIPE=>"5 KB\n"}, {OUT=>"5000B"}],

     # No separator when there's no unit (power=0)
     ['unit-sep-16', '--to=si --unit-separator=" " 500', {OUT=>"500"}],

     # Round-trip test
     ['unit-sep-17', '--from=iec --to=iec --unit-separator="_"',
      {IN_PIPE=>"1_K\n"}, {OUT=>"1.0_K"}],

     # Currently field delimiters have higher precedence than unit separators.
     # Even if this is changed in future, the following should hold.

     # The space should act as a field delimiter here
     ['unit-sep-18', '--from=si --unit-separator=" " "1 K_Field2"',
      {OUT=>"1 K_Field2"}],
     # Same as above but with 'i' suffix - should split at space with --from=si
     ['unit-sep-19', '--from=si --unit-separator=" " "5 Ki_Field2"',
      {OUT=>"5 Ki_Field2"}],
     # With --from=auto, Ki followed by invalid char should also split
     ['unit-sep-20', '--from=auto --unit-separator=" " "5 Ki_Field2"',
      {OUT=>"5 Ki_Field2"}],
     # With custom delimiter, space after K should not be treated as delimiter
     ['unit-sep-21', '-d: --from=si --unit-separator=" " "5 K:Field2"',
      {OUT=>"5000:Field2"}],
     # Fail case: space after K with custom delimiter should error
     ['unit-sep-22-fail', '-d: --from=si --unit-separator=" " "5 K Field2"',
      {ERR=>"$prog: invalid suffix in input '5 K Field2': 'Field2'\n"},
      {EXIT=>2}],

     # If Unit separator consumed before delimiter char,
     # this would change to outputting "5000 2"
     ['unit-sep-23', '--from=si --field=1 --unit-separator=" " -d " " "5 K 2"',
      {OUT=>"5 K 2"}],

     ## GROUPING

     # "C" locale - no grouping (locale-specific tests, below)
     ['grp-1', '--from=si --grouping 7M',   {OUT=>'7000000'}],
     ['grp-2', '--from=si --to=si --grouping 7M',
              {ERR => "$prog: grouping cannot be combined with --to\n"},
              {EXIT => '1'}],


     ## Padding
     ['pad-1', '--padding=10 5',             {OUT=>'         5'}],
     ['pad-2', '--padding=-10 5',            {OUT=>'5         '}],
     ['pad-3', '--padding=A 5',
             {ERR => "$prog: invalid padding value 'A'\n"},
             {EXIT => '1'}],
     ['pad-3.1', '--padding=0 5',
             {ERR => "$prog: invalid padding value '0'\n"},
             {EXIT => '1'}],
     ['pad-4', '--padding=10 --to=si 50000',             {OUT=>'       50k'}],
     ['pad-5', '--padding=-10 --to=si 50000',            {OUT=>'50k       '}],

     # padding too narrow
     ['pad-6', '--padding=2 --to=si 1000', {OUT=>'1.0k'}],


     # Padding + suffix
     ['pad-7', '--padding=10 --suffix=foo --to=si 50000',
             {OUT=>'    50kfoo'}],
     ['pad-8', '--padding=-10 --suffix=foo --to=si 50000',
             {OUT=>'50kfoo    '}],


     # Delimiters
     ['delim-1', '--delimiter=: --from=auto 40M:',   {OUT=>'40000000:'}],
     ['delim-2', '--delimiter="" --from=auto "40 M"',{OUT=>'40000000'}],
     ['delim-3', '--delimiter=" " --from=auto "40M Foo"',{OUT=>'40000000 Foo'}],
     ['delim-4', '--delimiter=: --from=auto 40M:60M',  {OUT=>'40000000:60M'}],
     ['delim-5', '-d: --field=2 --from=auto :40M:60M',  {OUT=>':40000000:60M'}],
     ['delim-6', '-d: --field 3 --from=auto 40M:60M', {OUT=>"40M:60M"}],
     # Ensure we don't hit https://sourceware.org/PR29511
     ['delim-7', "-d '\xc2' --field=2 --invalid=ignore '1\xc2\xb72K'",
             {OUT => "1\xc2\xb72K"}],
     ['delim-err-1', '-d,, --to=si 1', {EXIT=>1},
             {ERR => "$prog: the delimiter must be a single character\n"}],

     #Fields
     ['field-1', '--field A',
             {ERR => "$prog: invalid field value 'A'\n$try"},
             {EXIT => '1'}],
     ['field-2', '--field 2 --from=auto "Hello 40M World 90G"',
             {OUT=>'Hello 40000000 World 90G'}],
     ['field-3', '--field 3 --from=auto "Hello 40M World 90G"',
             {OUT=>"Hello 40M "},
             {ERR=>"$prog: invalid number: 'World'\n"},
             {EXIT => 2},],
     # Last field - no text after number
     ['field-4', '--field 4 --from=auto "Hello 40M World 90G"',
             {OUT=>"Hello 40M World 90000000000"}],
     # Last field - a delimiter after the number
     ['field-5', '--field 4 --from=auto "Hello 40M World 90G "',
             {OUT=>"Hello 40M World 90000000000 "}],

     # Mix Fields + Delimiters
     ['field-6', '--delimiter=: --field 2 --from=auto "Hello:40M:World:90G"',
             {OUT=>"Hello:40000000:World:90G"}],

     # not enough fields
     ['field-8', '--field 3 --to=si "Hello World"', {OUT=>"Hello World"}],

     # Multiple fields
     ['field-range-1', '--field 2,4 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1000 2.0k 3000 4.0k 5000"}],

     ['field-range-2', '--field 2-4 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1000 2.0k 3.0k 4.0k 5000"}],

     ['field-range-3', '--field 1,2,3-5 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5.0k"}],

     ['field-range-4', '--field 1-5 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5.0k"}],

     ['field-range-5', '--field 1-3,5 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4000 5.0k"}],

     ['field-range-6', '--field 3- --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1000 2000 3.0k 4.0k 5.0k"}],

     ['field-range-7', '--field -3 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4000 5000"}],

     ['field-range-8', '--field 1-2,4-5 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3000 4.0k 5.0k"}],
     ['field-range-9', '--field 4-5,1-2 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3000 4.0k 5.0k"}],

     ['field-range-10','--field 1-3,2-4 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5000"}],
     ['field-range-11','--field 2-4,1-3 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5000"}],

     ['field-range-12','--field 1-1,3-3 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2000 3.0k 4000 5000"}],

     ['field-range-13', '--field 1,-2 --to=si "1000 2000 3000"',
             {OUT=>"1.0k 2.0k 3000"}],

     ['field-range-14', '--field -2,4- --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3000 4.0k 5.0k"}],
     ['field-range-15', '--field -2,-4 --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5000"}],
     ['field-range-16', '--field 2-,4- --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1000 2.0k 3.0k 4.0k 5.0k"}],
     ['field-range-17', '--field 4-,2- --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1000 2.0k 3.0k 4.0k 5.0k"}],

     # white space are valid field separators
     # (undocumented? but works in cut as well).
     ['field-range-18', '--field "1,2 4" --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3000 4.0k 5000"}],

     # Unlike 'cut', a lone '-' means 'all fields', even as part of a list
     # of fields.
     ['field-range-19','--field 3,- --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5.0k"}],

     ['all-fields-1', '--field=- --to=si "1000 2000 3000 4000 5000"',
             {OUT=>"1.0k 2.0k 3.0k 4.0k 5.0k"}],

     ['field-range-err-1', '--field -foo --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: invalid field value 'foo'\n$try"}],
     ['field-range-err-2', '--field --3 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}],
     ['field-range-err-3', '--field 0 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: fields are numbered from 1\n$try"}],
     ['field-range-err-4', '--field 3-2 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: invalid decreasing range\n$try"}],
     ['field-range-err-6', '--field - --field 1- --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
     ['field-range-err-7', '--field -1 --field 1- --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
     ['field-range-err-8', '--field -1 --field 1,2,3 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
     ['field-range-err-9', '--field 1- --field 1,2,3 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
     ['field-range-err-10','--field 1,2,3 --field 1- --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
     ['field-range-err-11','--field 1-2-3 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}],
     ['field-range-err-12','--field 0-1 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: fields are numbered from 1\n$try"}],
     ['field-range-err-13','--field '.$limits->{UINTMAX_MAX}.',22 --to=si 10',
             {EXIT=>1}, {ERR=>"$prog: field number " .
                            "'".$limits->{UINTMAX_MAX}."' is too large\n$try"}],

     # Auto-consume white-space, setup auto-padding
     ['whitespace-1', '--to=si --field 2 "A    500 B"', {OUT=>"A    500 B"}],
     ['whitespace-2', '--to=si --field 2 "A   5000 B"', {OUT=>"A   5.0k B"}],
     ['whitespace-3', '--to=si "  500"', {OUT=>"  500"}],
     ['whitespace-4', '--to=si " 6500"', {OUT=>" 6.5k"}],
     # NOTE: auto-padding is not enabled if the value is on the first
     #       field and there's no white-space before it.
     ['whitespace-5', '--to=si "6000000"', {OUT=>"6.0M"}],
     # but if there is whitespace, assume auto-padding is desired.
     ['whitespace-6', '--to=si " 6000000"', {OUT=>"    6.0M"}],

     # auto-padding - lines have same padding-width
     #  (padding_buffer will be alloc'd just once)
     ['whitespace-7', '--to=si --field 2',
             {IN_PIPE=>"rootfs    100000\n" .
                       "udevxx   2000000\n"},
             {OUT    =>"rootfs      100k\n" .
                       "udevxx      2.0M"}],
     # auto-padding - second line requires a
     # larger padding (padding-buffer needs to be realloc'd)
     ['whitespace-8', '--to=si --field 2',
             {IN_PIPE=>"rootfs    100000\n" .
                       "udev         20000000\n"},
             {OUT    =>"rootfs      100k\n" .
                       "udev              20M"}],


     # Corner-cases:
     # weird mix of identical suffix,delimiters
     # The priority is:
     #   1. delimiters (and fields) are parsed (in process_line()
     #   2. optional custom suffix is removed (in process_suffixed_number())
     #   3. Remaining suffixes must be valid SI/IEC (in human_xstrtol())

     # custom suffix comes BEFORE SI/IEC suffix,
     #   so these are 40 of "M", not 40,000,000.
     ['mix-1', '--suffix=M --from=si 40M',     {OUT=>"40M"}],

     # These are forty-million Ms .
     ['mix-2', '--suffix=M --from=si 40MM',     {OUT=>"40000000M"}],

     ['mix-3', '--suffix=M --from=auto 40MM',     {OUT=>"40000000M"}],
     ['mix-4', '--suffix=M --from=auto 40MiM',     {OUT=>"41943040M"}],
     ['mix-5', '--suffix=M --to=si --from=si 4MM',     {OUT=>"4.0MM"}],

     # This might be confusing to the user, but it's legit:
     # The M in the output is the custom suffix, not Mega.
     ['mix-6', '--suffix=M 40',     {OUT=>"40M"}],
     ['mix-7', '--suffix=M 4000000',     {OUT=>"4000000M"}],
     ['mix-8', '--suffix=M --to=si 4000000',     {OUT=>"4.0MM"}],

     # The output 'M' is the custom suffix.
     ['mix-10', '--delimiter=M --suffix=M 40',     {OUT=>"40M"}],

     # The INPUT 'M' is a delimiter (delimiters are top priority)
     # The output contains one M for custom suffix, and one 'M' delimiter.
     ['mix-11', '--delimiter=M --suffix=M 40M',     {OUT=>"40MM"}],

     # Same as above, the "M" is NOT treated as a mega SI prefix,
     ['mix-12', '--delimiter=M --from=si --suffix=M 40M',     {OUT=>"40MM"}],

     # The 'M' is treated as a delimiter, and so the input value is '4000'
     ['mix-13', '--delimiter=M --to=si --from=auto 4000M5000M9000',
             {OUT=>"4.0kM5000M9000"}],
     # 'M' is the delimiter, so the second input field is '5000'
     ['mix-14', '--delimiter=M --field 2 --from=auto --to=si 4000M5000M9000',
             {OUT=>"4000M5.0kM9000"}],



     ## Header testing

     # header - silently ignored with command line parameters
     ['header-1', '--header --to=iec 4096', {OUT=>"4.0K"}],

     # header warning with --debug
     ['header-2', '--debug --header --to=iec 4096', {OUT=>"4.0K"},
             {ERR=>"$prog: --header ignored with command-line input\n"}],

     ['header-3', '--header=A',
             {ERR=>"$prog: invalid header value 'A'\n"},
             {EXIT => 1},],
     ['header-4', '--header=0',
             {ERR=>"$prog: invalid header value '0'\n"},
             {EXIT => 1},],
     ['header-5', '--header=-6',
             {ERR=>"$prog: invalid header value '-6'\n"},
             {EXIT => 1},],
     ['header-6', '--debug --header --to=iec',
             {IN_PIPE=>"size\n5000\n90000\n"},
             {OUT=>"size\n4.9K\n88K"}],
     ['header-7', '--debug --header=3 --to=iec',
             {IN_PIPE=>"hello\nworld\nsize\n5000\n90000\n"},
             {OUT=>"hello\nworld\nsize\n4.9K\n88K"}],
     # header, but no actual content
     ['header-8', '--header=2 --to=iec',
             {IN_PIPE=>"hello\nworld\n"},
             {OUT=>"hello\nworld"}],
     # not enough header lines
     ['header-9', '--header=3 --to=iec',
             {IN_PIPE=>"hello\nworld\n"},
             {OUT=>"hello\nworld"}],


     ## human_strtod testing

     # NO_DIGITS_FOUND
     ['strtod-1', '--from=si "foo"',
             {ERR=>"$prog: invalid number: 'foo'\n"},
             {EXIT=> 2}],
     ['strtod-2', '--from=si ""',
             {ERR=>"$prog: invalid number: ''\n"},
             {EXIT=> 2}],

     # FRACTION_NO_DIGITS_FOUND
     ['strtod-5', '--from=si 12.',
             {ERR=>"$prog: invalid number: '12.'\n"},
             {EXIT=>2}],
     ['strtod-6', '--from=si 12.K',
             {ERR=>"$prog: invalid number: '12.K'\n"},
             {EXIT=>2}],

     # whitespace is not allowed after decimal-point
     ['strtod-6.1', '--from=si --delimiter=, "12.  2"',
             {ERR=>"$prog: invalid number: '12.  2'\n"},
             {EXIT=>2}],

     # INVALID_SUFFIX
     ['strtod-9', '--from=si 12.2J',
             {ERR=>"$prog: invalid suffix in input: '12.2J'\n"},
             {EXIT=>2}],

     # VALID_BUT_FORBIDDEN_SUFFIX
     ['strtod-10', '12M',
             {ERR => "$prog: rejecting suffix " .
                     "in input: '12M' (consider using --from)\n"},
             {EXIT=>2}],

     # MISSING_I_SUFFIX
     ['strtod-11', '--from=iec-i 12M',
             {ERR => "$prog: missing 'i' suffix in input: " .
                     "'12M' (e.g Ki/Mi/Gi)\n"},
             {EXIT=>2}],

     #
     # Test double_to_human()
     #

     # 1K and smaller
     ['dbl-to-human-1','--to=si 800',  {OUT=>"800"}],
     ['dbl-to-human-2','--to=si 0',  {OUT=>"0"}],
     ['dbl-to-human-2.1','--to=si 999',  {OUT=>"999"}],
     ['dbl-to-human-2.2','--to=si 1000',  {OUT=>"1.0k"}],
     #NOTE: the following are consistent with "ls -lh" output
     ['dbl-to-human-2.3','--to=iec 999',  {OUT=>"999"}],
     ['dbl-to-human-2.4','--to=iec 1023',  {OUT=>"1023"}],
     ['dbl-to-human-2.5','--to=iec 1024',  {OUT=>"1.0K"}],
     ['dbl-to-human-2.6','--to=iec 1025',  {OUT=>"1.1K"}],
     ['dbl-to-human-2.7','--to=iec 0',  {OUT=>"0"}],
     # no "i" suffix if output has no suffix
     ['dbl-to-human-2.8','--to=iec-i 0',  {OUT=>"0"}],

     # values resulting in "N.Nx" output
     ['dbl-to-human-3','--to=si 8000', {OUT=>"8.0k"}],
     ['dbl-to-human-3.1','--to=si 8001', {OUT=>"8.1k"}],
     ['dbl-to-human-4','--to=si --round=down 8001', {OUT=>"8.0k"}],

     ['dbl-to-human-5','--to=si --round=down 3500', {OUT=>"3.5k"}],
     ['dbl-to-human-6','--to=si --round=nearest 3500', {OUT=>"3.5k"}],
     ['dbl-to-human-7','--to=si --round=up 3500', {OUT=>"3.5k"}],

     ['dbl-to-human-8','--to=si --round=down    3501', {OUT=>"3.5k"}],
     ['dbl-to-human-9','--to=si --round=nearest  3501', {OUT=>"3.5k"}],
     ['dbl-to-human-10','--to=si --round=up 3501', {OUT=>"3.6k"}],

     ['dbl-to-human-11','--to=si --round=nearest  3550', {OUT=>"3.6k"}],
     ['dbl-to-human-12','--to=si --from=si 999.89K', {OUT=>"1.0M"}],
     ['dbl-to-human-13','--to=si --from=si 9.9K', {OUT=>"9.9k"}],
     ['dbl-to-human-14','--to=si 9900', {OUT=>"9.9k"}],
     ['dbl-to-human-15','--to=iec --from=si 3.3K', {OUT=>"3.3K"}],
     ['dbl-to-human-16','--to=iec --round=down --from=si 3.3K', {OUT=>"3.2K"}],

     # values resulting in 'NNx' output
     ['dbl-to-human-17','--to=si 9999', {OUT=>"10k"}],
     ['dbl-to-human-18','--to=si --round=down 35000', {OUT=>"35k"}],
     ['dbl-to-human-19','--to=iec 35000', {OUT=>"35K"}],
     ['dbl-to-human-20','--to=iec --round=down 35000', {OUT=>"34K"}],
     ['dbl-to-human-21','--to=iec 35000000', {OUT=>"34M"}],
     ['dbl-to-human-22','--to=iec --round=down 35000000', {OUT=>"33M"}],
     ['dbl-to-human-23','--to=si  35000001', {OUT=>"36M"}],
     ['dbl-to-human-24','--to=si --from=si  9.99M', {OUT=>"10M"}],
     ['dbl-to-human-25','--to=si --from=iec 9.99M', {OUT=>"11M"}],
     ['dbl-to-human-25.1','--to=iec 99999', {OUT=>"98K"}],

     # values resulting in 'NNNx' output
     ['dbl-to-human-26','--to=si 999000000000', {OUT=>"999G"}],
     ['dbl-to-human-27','--to=iec 999000000000', {OUT=>"931G"}],
     ['dbl-to-human-28','--to=si 123600000000000', {OUT=>"124T"}],
     ['dbl-to-human-29','--to=si 998123', {OUT=>"999k"}],
     ['dbl-to-human-30','--to=si --round=nearest 998123', {OUT=>"998k"}],
     ['dbl-to-human-31','--to=si 99999', {OUT=>"100k"}],
     ['dbl-to-human-32','--to=iec 102399', {OUT=>"100K"}],
     ['dbl-to-human-33','--to=iec-i 102399', {OUT=>"100Ki"}],


     # Default --round=from-zero
     ['round-1','--to-unit=1024 -- 6000 -6000',
             {OUT=>"6\n-6"}],
     ['round-2','--to-unit=1024 -- 6000.0 -6000.0',
             {OUT=>"5.9\n-5.9"}],
     ['round-3','--to-unit=1024 -- 6000.00 -6000.00',
             {OUT=>"5.86\n-5.86"}],
     ['round-4','--to-unit=1024 -- 6000.000 -6000.000',
             {OUT=>"5.860\n-5.860"}],
     ['round-5','--to-unit=1024 -- 6000.0000 -6000.0000',
             {OUT=>"5.8594\n-5.8594"}],
     # --round=up
     ['round-1-up','--round=up --to-unit=1024 -- 6000 -6000',
             {OUT=>"6\n-5"}],
     ['round-2-up','--round=up --to-unit=1024 -- 6000.0 -6000.0',
             {OUT=>"5.9\n-5.8"}],
     ['round-3-up','--round=up --to-unit=1024 -- 6000.00 -6000.00',
             {OUT=>"5.86\n-5.85"}],
     ['round-4-up','--round=up --to-unit=1024 -- 6000.000 -6000.000',
             {OUT=>"5.860\n-5.859"}],
     ['round-5-up','--round=up --to-unit=1024 -- 6000.0000 -6000.0000',
             {OUT=>"5.8594\n-5.8593"}],
     # --round=down
     ['round-1-down','--round=down --to-unit=1024 -- 6000 -6000',
             {OUT=>"5\n-6"}],
     ['round-2-down','--round=down --to-unit=1024 -- 6000.0 -6000.0',
             {OUT=>"5.8\n-5.9"}],
     ['round-3-down','--round=down --to-unit=1024 -- 6000.00 -6000.00',
             {OUT=>"5.85\n-5.86"}],
     ['round-4-down','--round=down --to-unit=1024 -- 6000.000 -6000.000',
             {OUT=>"5.859\n-5.860"}],
     ['round-5-down','--round=down --to-unit=1024 -- 6000.0000 -6000.0000',
             {OUT=>"5.8593\n-5.8594"}],
     # --round=towards-zero
     ['round-1-to-zero','--ro=towards-zero --to-u=1024 -- 6000 -6000',
             {OUT=>"5\n-5"}],
     ['round-2-to-zero','--ro=towards-zero --to-u=1024 -- 6000.0 -6000.0',
             {OUT=>"5.8\n-5.8"}],
     ['round-3-to-zero','--ro=towards-zero --to-u=1024 -- 6000.00 -6000.00',
             {OUT=>"5.85\n-5.85"}],
     ['round-4-to-zero','--ro=towards-zero --to-u=1024 -- 6000.000 -6000.000',
             {OUT=>"5.859\n-5.859"}],
     ['round-5-to-zero','--ro=towards-zero --to-u=1024 -- 6000.0000 -6000.0000',
             {OUT=>"5.8593\n-5.8593"}],
     # --round=nearest
     ['round-1-near','--ro=nearest --to-u=1024 -- 6000 -6000',
             {OUT=>"6\n-6"}],
     ['round-2-near','--ro=nearest --to-u=1024 -- 6000.0 -6000.0',
             {OUT=>"5.9\n-5.9"}],
     ['round-3-near','--ro=nearest --to-u=1024 -- 6000.00 -6000.00',
             {OUT=>"5.86\n-5.86"}],
     ['round-4-near','--ro=nearest --to-u=1024 -- 6000.000 -6000.000',
             {OUT=>"5.859\n-5.859"}],
     ['round-5-near','--ro=nearest --to-u=1024 -- 6000.0000 -6000.0000',
             {OUT=>"5.8594\n-5.8594"}],


     # Leading zeros weren't handled appropriately before 8.24
     ['leading-1','0000000000000000000000000001', {OUT=>"1"}],
     ['leading-2','.1', {OUT=>"0.1"}],
     ['leading-3','bad.1',
             {ERR => "$prog: invalid number: 'bad.1'\n"},
             {EXIT => 2}],
     ['leading-4','..1',
             {ERR => "$prog: invalid suffix in input: '..1'\n"},
             {EXIT => 2}],
     ['leading-5','1.',
             {ERR => "$prog: invalid number: '1.'\n"},
             {EXIT => 2}],

     # precision override
     ['precision-1','--format=%.4f 9991239123 --to=si', {OUT=>"9.9913G"}],
     ['precision-2','--format=%.1f 9991239123 --to=si', {OUT=>"10.0G"}],
     ['precision-3','--format=%.1f 1', {OUT=>"1.0"}],
     ['precision-4','--format=%.1f 1.12', {OUT=>"1.2"}],
     ['precision-5','--format=%.1f 9991239123 --to-unit=G', {OUT=>"10.0"}],
     ['precision-6','--format="% .1f" 9991239123 --to-unit=G', {OUT=>"10.0"}],
     ['precision-7','--format=%.-1f 1.1',
             {ERR => "$prog: invalid precision in format '%.-1f'\n"},
             {EXIT => 1}],
     ['precision-8','--format=%.+1f 1.1',
             {ERR => "$prog: invalid precision in format '%.+1f'\n"},
             {EXIT => 1}],
     ['precision-9','--format="%. 1f" 1.1',
             {ERR => "$prog: invalid precision in format '%. 1f'\n"},
             {EXIT => 1}],

     # debug warnings
     ['debug-1', '--debug 4096', {OUT=>"4096"},
             {ERR=>"$prog: no conversion option specified\n"}],
     # '--padding' is a valid conversion option - no warning should be printed
     ['debug-1.1', '--debug --padding 10 4096', {OUT=>"      4096"}],
     ['debug-2', '--debug --grouping --from=si 4.0K', {OUT=>"4000"},
             {ERR=>"$prog: grouping has no effect in this locale\n"}],

     # dev-debug messages - the actual messages don't matter
     # just ensure the program works, and for code coverage testing.
     ['devdebug-1', '---debug --from=si 4.9K', {OUT=>"4900"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-2', '---debug 4900', {OUT=>"4900"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-3', '---debug --from=auto 4Mi', {OUT=>"4194304"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-4', '---debug --to=si 4000000', {OUT=>"4.0M"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-5', '---debug --to=si --padding=5 4000000', {OUT=>" 4.0M"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-6', '---debug --suffix=Foo 1234Foo', {OUT=>"1234Foo"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-7', '---debug --suffix=Foo 1234', {OUT=>"1234Foo"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-9', '---debug --grouping 10000', {OUT=>"10000"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-10', '---debug --format %f 10000', {OUT=>"10000"},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],
     ['devdebug-11', '---debug --format "%\'-10f" 10000',{OUT=>"10000     "},
             {ERR=>""},
             {ERR_SUBST=>"s/.*//msg"}],

     # Invalid parameters
     ['help-1', '--foobar',
             {ERR=>"$prog: unrecognized option\n$try"},
             {ERR_SUBST=>"s/option.*/option/; s/unknown/unrecognized/"},
             {EXIT=>1}],

     ## Format string - check error detection
     ['fmt-err-1', '--format ""',
             {ERR=>"$prog: format '' has no % directive\n"},
             {EXIT=>1}],
     ['fmt-err-2', '--format "hello"',
             {ERR=>"$prog: format 'hello' has no % directive\n"},
             {EXIT=>1}],
     ['fmt-err-3', '--format "hello%"',
             {ERR=>"$prog: format 'hello%' ends in %\n"},
             {EXIT=>1}],
     ['fmt-err-4', '--format "%d"',
             {ERR=>"$prog: invalid format '%d', " .
                   "directive must be %[0]['][-][N][.][N]f\n"},
             {EXIT=>1}],
     ['fmt-err-5', '--format "% -43 f"',
             {ERR=>"$prog: invalid format '% -43 f', " .
                   "directive must be %[0]['][-][N][.][N]f\n"},
             {EXIT=>1}],
     ['fmt-err-6', '--format "%f %f"',
             {ERR=>"$prog: format '%f %f' has too many % directives\n"},
             {EXIT=>1}],
     ['fmt-err-9', '--format "%f" --grouping',
             {ERR=>"$prog: --grouping cannot be combined with --format\n"},
             {EXIT=>1}],
     ['fmt-err-10', '--format "%\'f" --to=si',
             {ERR=>"$prog: grouping cannot be combined with --to\n"},
             {EXIT=>1}],
     ['fmt-err-11', '--debug --format "%\'f" 5000', {OUT=>"5000"},
             {ERR=>"$prog: grouping has no effect in this locale\n"}],

     ## Format string - check some corner cases
     ['fmt-1', '--format "%% %f" 5000', {OUT=>"%%5000"}],
     ['fmt-2', '--format "%f %%" 5000', {OUT=>"5000 %%"}],

     ['fmt-3', '--format "--%f--" 5000000', {OUT=>"--5000000--"}],
     ['fmt-4', '--format "--%f--" --to=si 5000000', {OUT=>"--5.0M--"}],

     ['fmt-5', '--format "--%10f--" --to=si 5000000',{OUT=>"--      5.0M--"}],
     ['fmt-6', '--format "--%-10f--" --to=si 5000000',{OUT=>"--5.0M      --"}],
     ['fmt-7', '--format "--%10f--" 5000000',{OUT=>"--   5000000--"}],
     ['fmt-8', '--format "--%-10f--" 5000000',{OUT=>"--5000000   --"}],

     # too-short width
     ['fmt-9', '--format "--%5f--" 5000000',{OUT=>"--5000000--"}],

     # Format + Suffix
     ['fmt-10', '--format "--%10f--" --suffix Foo 50', {OUT=>"--     50Foo--"}],
     ['fmt-11', '--format "--%-10f--" --suffix Foo 50',{OUT=>"--50Foo     --"}],

     # Grouping in C locale - no grouping effect
     ['fmt-12', '--format "%\'f" 50000',{OUT=>"50000"}],
     ['fmt-13', '--format "%\'10f" 50000', {OUT=>"     50000"}],
     ['fmt-14', '--format "%\'-10f" 50000',{OUT=>"50000     "}],

     # Very large format strings
     ['fmt-15', '--format "--%100000f--" --to=si 4200',
                  {OUT=>"--" . " " x 99996 . "4.2k--" }],

     # --format padding overrides --padding
     ['fmt-16', '--format="%6f" --padding=66 1234',{OUT=>"  1234"}],

     # zero padding
     ['fmt-17', '--format="%06f" 1234',{OUT=>"001234"}],
     # also support spaces (which are ignored as spacing is handled separately)
     ['fmt-18', '--format="%0 6f" 1234',{OUT=>"001234"}],
     # handle generic padding in combination
     ['fmt-22', '--format="%06f" --padding=7 1234',{OUT=>" 001234"}],
     ['fmt-23', '--format="%06f" --padding=-7 1234',{OUT=>"001234 "}],


     ## Check all errors again, this time with --invalid=fail
     ##  Input will be printed without conversion,
     ##  and exit code will be 2
     ['ign-err-1', '--invalid=fail 4J',
             {ERR => "$prog: invalid suffix in input: '4J'\n"},
             {OUT => "4J\n"},
             {EXIT => 2}],
     ['ign-err-2', '--invalid=fail 4M',
             {ERR => "$prog: rejecting suffix " .
             "in input: '4M' (consider using --from)\n"},
             {OUT => "4M\n"},
             {EXIT => 2}],
     ['ign-err-3', '--invalid=fail --from=si 4MJ',
             {ERR => "$prog: invalid suffix in input '4MJ': 'J'\n"},
             {OUT => "4MJ\n"},
             {EXIT => 2}],
     ['ign-err-4', '--invalid=fail --suffix=Foo --to=si   7000FooF',
              {ERR => "$prog: invalid suffix in input: '7000FooF'\n"},
              {OUT => "7000FooF\n"},
              {EXIT => 2}],
     ['ign-err-5','--invalid=fail --field 3 --from=auto "Hello 40M World 90G"',
             {ERR => "$prog: invalid number: 'World'\n"},
             {OUT => "Hello 40M World 90G\n"},
             {EXIT => 2}],
     ['ign-err-7', '--invalid=fail --from=si "foo"',
             {ERR => "$prog: invalid number: 'foo'\n"},
             {OUT => "foo\n"},
             {EXIT=> 2}],
     ['ign-err-8', '--invalid=fail 12M',
             {ERR => "$prog: rejecting suffix " .
                     "in input: '12M' (consider using --from)\n"},
             {OUT => "12M\n"},
             {EXIT => 2}],
     ['ign-err-9', '--invalid=fail --from=iec-i 12M',
             {ERR => "$prog: missing 'i' suffix in input: " .
                     "'12M' (e.g Ki/Mi/Gi)\n"},
             {OUT => "12M\n"},
             {EXIT=>2}],

     ## Ignore Errors with multiple conversions
     ['ign-err-m1', '--invalid=ignore --to=si 1000 2000 bad 3000',
             {OUT => "1.0k\n2.0k\nbad\n3.0k"},
             {EXIT => 0}],
     ['ign-err-m1.1', '--invalid=ignore --to=si',
             {IN_PIPE => "1000\n2000\nbad\n3000\n"},
             {OUT => "1.0k\n2.0k\nbad\n3.0k"},
             {EXIT => 0}],
     ['ign-err-m1.3', '--invalid=fail --debug --to=si 1000 2000 3000',
             {OUT => "1.0k\n2.0k\n3.0k"},
             {EXIT => 0}],
     ['ign-err-m2', '--invalid=fail --to=si 1000 Foo 3000',
             {OUT => "1.0k\nFoo\n3.0k\n"},
             {ERR => "$prog: invalid number: 'Foo'\n"},
             {EXIT => 2}],
     ['ign-err-m2.1', '--invalid=warn --to=si',
             {IN_PIPE => "1000\nFoo\n3000\n"},
             {OUT => "1.0k\nFoo\n3.0k"},
             {ERR => "$prog: invalid number: 'Foo'\n"},
             {EXIT => 0}],

     # --debug will trigger a final warning at EOF
     ['ign-err-m2.2', '--invalid=fail --debug --to=si 1000 Foo 3000',
             {OUT => "1.0k\nFoo\n3.0k\n"},
             {ERR => "$prog: invalid number: 'Foo'\n" .
                     "$prog: failed to convert some of the input numbers\n"},
             {EXIT => 2}],

     ['ign-err-m3', '--invalid=fail --field 2 --from=si --to=iec',
             {IN_PIPE => "A 1K x\nB 2M y\nC 3G z\n"},
             {OUT => "A 1000 x\nB 2.0M y\nC 2.8G z"},
             {EXIT => 0}],
     # invalid input on one of the fields
     ['ign-err-m3.1', '--invalid=fail --field 2 --from=si --to=iec',
             {IN_PIPE => "A 1K x\nB Foo y\nC 3G z\n"},
             {OUT => "A 1000 x\nB Foo y\nC 2.8G z\n"},
             {ERR => "$prog: invalid number: 'Foo'\n"},
             {EXIT => 2}],

     # A leading '-9...' must be treated as an invalid short option, not as
     # a negative positional argument.
     ['neg-arg-not-option', '--to=iec -9923868',
             {ERR_SUBST => q!s/'9'/9/;s/unknown/invalid/!},
             {ERR => "$prog: invalid option -- 9\n" .
                     "Try '$prog --help' for more information.\n"},
             {EXIT => 1}],

     # Scientific notation ('1e9') is not accepted as numeric input;
     # it is reported as an invalid suffix.
     ['reject-sci-notation', '1e9',
             {ERR => "$prog: invalid suffix in input: '1e9'\n"},
             {EXIT => 2}],

     # '--from-unit' scaling must preserve fractional precision of the input
     # rather than rounding to an integer.
     ['from-unit-fraction', '--from=iec --from-unit=959 -- -615484.454',
             {OUT => "-590249591.386"}],

     # Zero-padded '--format' must place the sign before the padding zeros
     # for negative numbers (matches C printf).
     ['zero-pad-neg-sign', '--from=none --format=%018.2f -- -9869647',
             {OUT => "-00000009869647.00"}],

     # '--to-unit=N' must select the output prefix based on the scaled value
     # (value / N), not the unscaled input.
     ['to-unit-prefix', '--to=iec-i --to-unit=885 100000',
             {OUT => "113"}],

     # '--format=%.0f' combined with '--to=<scale>' must honor the '.0'
     # precision specifier (no fractional digit).
     ['fmt-zero-prec-scale', '--to=iec --format=%.0f 5183776',
             {OUT => "5M"}],
    );

# test null-terminated lines
my @NullDelim_Tests =
  (
     # Input from STDIN
     ['z1', '-z --to=iec',
             {IN_PIPE => "1025\x002048\x00"}, {OUT=>"1.1K\x002.0K\x00"}],

     # Input from the commandline - terminated by NULL vs NL
     ['z3', '   --to=iec 1024',  {OUT=>"1.0K\n"}],
     ['z2', '-z --to=iec 1024',  {OUT=>"1.0K\x00"}],

     # Input from STDIN, with fields
     ['z4', '-z --field=3 --to=si',
             {IN_PIPE => "A B 1001 C\x00" .
                         "D E 2002 F\x00"},
             {OUT => "A B 1.1k C\x00" .
                     "D E 2.1k F\x00"}],

     # Input from STDIN, with fields and embedded NL
     ['z5', '-z --field=3 --to=si',
             {IN_PIPE => "A\nB 1001 C\x00" .
                         "D E\n2002 F\x00"},
             {OUT => "A B 1.1k C\x00" .
                     "D E 2.1k F\x00"}],
  );

my @Limit_Tests =
  (
     # Large Values
     ['large-1','1000000000000000', {OUT=>"1000000000000000"}],
     # 18 digits is OK
     ['large-2','1000000000000000000', {OUT=>"1000000000000000000"}],

     # Large integers beyond 2^53 must be preserved exactly
     # (no double round-trip).
     ['large-2a', '915339622755539213', {OUT => "915339622755539213"}],

     # 19 digits is too much (without output scaling)
     ['large-3','10000000000000000000',
             {ERR => "$prog: value too large to be printed: '1e+19' " .
                     "(consider using --to)\n"},
             {EXIT=>2}],
     ['large-4','1000000000000000000.0',
             {ERR => "$prog: value/precision too large to be printed: " .
                     "'1e+18/1' (consider using --to)\n"},
             {EXIT=>2}],


     # Test input:
     # Up to 33 digits is OK.
     ['large-3.1', '--to=si                           1', {OUT=>   "1"}],
     ['large-3.2', '--to=si                          10', {OUT=>  "10"}],
     ['large-3.3', '--to=si                         100', {OUT=> "100"}],
     ['large-3.4', '--to=si                        1000', {OUT=>"1.0k"}],
     ['large-3.5', '--to=si                       10000', {OUT=> "10k"}],
     ['large-3.6', '--to=si                      100000', {OUT=>"100k"}],
     ['large-3.7', '--to=si                     1000000', {OUT=>"1.0M"}],
     ['large-3.8', '--to=si                    10000000', {OUT=> "10M"}],
     ['large-3.9', '--to=si                   100000000', {OUT=>"100M"}],
     ['large-3.10','--to=si                  1000000000', {OUT=>"1.0G"}],
     ['large-3.11','--to=si                 10000000000', {OUT=> "10G"}],
     ['large-3.12','--to=si                100000000000', {OUT=>"100G"}],
     ['large-3.13','--to=si               1000000000000', {OUT=>"1.0T"}],
     ['large-3.14','--to=si              10000000000000', {OUT=> "10T"}],
     ['large-3.15','--to=si             100000000000000', {OUT=>"100T"}],
     ['large-3.16','--to=si            1000000000000000', {OUT=>"1.0P"}],
     ['large-3.17','--to=si           10000000000000000', {OUT=> "10P"}],
     ['large-3.18','--to=si          100000000000000000', {OUT=>"100P"}],
     ['large-3.19','--to=si         1000000000000000000', {OUT=>"1.0E"}],
     ['large-3.20','--to=si        10000000000000000000', {OUT=> "10E"}],
     ['large-3.21','--to=si       210000000000000000000', {OUT=>"210E"}],
     ['large-3.22','--to=si      3210000000000000000000', {OUT=>"3.3Z"}],
     ['large-3.23','--to=si     43210000000000000000000', {OUT=> "44Z"}],
     ['large-3.24','--to=si    543210000000000000000000', {OUT=>"544Z"}],
     ['large-3.25','--to=si   6543210000000000000000000', {OUT=>"6.6Y"}],
     ['large-3.26','--to=si  76543210000000000000000000', {OUT=> "77Y"}],
     ['large-3.27','--to=si 876543210000000000000000000', {OUT=>"877Y"}],
     ['large-3.28','--to=si      9876543210000000000000000000', {OUT=>"9.9R"}],
     ['large-3.29','--to=si     19876543210000000000000000000', {OUT=> "20R"}],
     ['large-3.30','--to=si    219876543210000000000000000000', {OUT=>"220R"}],
     ['large-3.31','--to=si   3219876543210000000000000000000', {OUT=>"3.3Q"}],
     ['large-3.32','--to=si  43219876543210000000000000000000', {OUT=> "44Q"}],
     ['large-3.33','--to=si 543219876543210000000000000000000', {OUT=>"544Q"}],

     # More than 33 digits is not OK
     ['large-3.34','--to=si 6543219876543210000000000000000000',
             {ERR => "$prog: value too large to be converted: " .
                     "'6543219876543210000000000000000000'\n"},
             {EXIT => 2}],

     # Test Output
     ['large-4.1', '--from=si  9.7M',               {OUT=>"9700000"}],
     ['large-4.2', '--from=si  10M',              {OUT =>"10000000"}],
     ['large-4.3', '--from=si  200M',            {OUT =>"200000000"}],
     ['large-4.4', '--from=si  3G',             {OUT =>"3000000000"}],
     ['large-4.5', '--from=si  40G',           {OUT =>"40000000000"}],
     ['large-4.6', '--from=si  500G',         {OUT =>"500000000000"}],
     ['large-4.7', '--from=si  6T',          {OUT =>"6000000000000"}],
     ['large-4.8', '--from=si  70T',        {OUT =>"70000000000000"}],
     ['large-4.9', '--from=si  800T',      {OUT =>"800000000000000"}],
     ['large-4.10','--from=si  9P',       {OUT =>"9000000000000000"}],
     ['large-4.11','--from=si  10P',     {OUT =>"10000000000000000"}],
     ['large-4.12','--from=si  200P',   {OUT =>"200000000000000000"}],
     ['large-4.13','--from=si  3E',    {OUT =>"3000000000000000000"}],

     # More than 18 digits of output without scaling - no good.
     ['large-4.14','--from=si  40E',
             {ERR => "$prog: value too large to be printed: '4e+19' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.15','--from=si  500E',
             {ERR => "$prog: value too large to be printed: '5e+20' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.16','--from=si  6Z',
             {ERR => "$prog: value too large to be printed: '6e+21' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.17','--from=si  70Z',
             {ERR => "$prog: value too large to be printed: '7e+22' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.18','--from=si  800Z',
             {ERR => "$prog: value too large to be printed: '8e+23' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.19','--from=si  9Y',
             {ERR => "$prog: value too large to be printed: '9e+24' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.20','--from=si  10Y',
             {ERR => "$prog: value too large to be printed: '1e+25' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-4.21','--from=si  200Y',
             {ERR => "$prog: value too large to be printed: '2e+26' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],

     ['large-5.1','--to=si 1000000000000000000', {OUT=>"1.0E"}],
     ['large-5','--from=si --to=si 2E', {OUT=>"2.0E"}],
     ['large-6','--from=si --to=si 3.4Z', {OUT=>"3.4Z"}],
     ['large-7','--from=si --to=si 80Y', {OUT=>"80Y"}],
     ['large-8','--from=si --to=si 9000Z', {OUT=>"9.0Y"}],

     ['large-10','--from=si --to=si 999Q', {OUT=>"999Q"}],
     ['large-11','--from=si --to=iec 999Q', {OUT=>"789Q"}],
     ['large-12','--from=si --round=down --to=iec 999Q', {OUT=>"788Q"}],

     # units can also affect the output
     ['large-13','--from=si --from-unit=1000000 9P',
             {ERR => "$prog: value too large to be printed: '9e+21' " .
                     "(consider using --to)\n"},
             {EXIT => 2}],
     ['large-13.1','--from=si --from-unit=1000000 --to=si 9P', {OUT=>"9.0Z"}],

     # Numbers>999Q are never acceptable, regardless of scaling
     ['large-14','--from=si --to=si 999Q', {OUT=>"999Q"}],
     ['large-14.1','--from=si --to=si 1000Q',
             {ERR => "$prog: value too large to be printed: '1e+33' " .
                     "(cannot handle values > 999Q)\n"},
             {EXIT => 2}],
     ['large-14.2','--from=si --to=si --from-unit=10000 1Q',
             {ERR => "$prog: value too large to be printed: '1e+34' " .
                     "(cannot handle values > 999Q)\n"},
             {EXIT => 2}],

     # intmax_t overflow when rounding caused this to fail before 8.24
     ['large-15',$limits->{INTMAX_OFLOW}, {OUT=>$limits->{INTMAX_OFLOW}}],
     ['large-16','9.300000000000000000', {OUT=>'9.300000000000000000'}],

     # INTEGRAL_OVERFLOW
     ['strtod-3', '--from=si "1234567890123456789012345678901234567890'.
                  '1234567890123456789012345678901234567890"',
             {ERR=>"$prog: value too large to be converted: '" .
                     "1234567890123456789012345678901234567890" .
                     "1234567890123456789012345678901234567890'\n",
                     },
             {EXIT=> 2}],

     # FRACTION_OVERFLOW
     ['strtod-7', '--from=si "12.1234567890123456789012345678901234567890'.
                  '1234567890123456789012345678901234567890"',
             {ERR=>"$prog: value too large to be converted: '" .
                     "12.1234567890123456789012345678901234567890" .
                     "1234567890123456789012345678901234567890'\n",
                     },
             {EXIT=> 2}],

     ['debug-4', '--to=si --debug 12345678901234567890',
             {OUT=>"13E"},
             {ERR=>"$prog: large input value '12345678901234567890':" .
                   " possible precision loss\n"}],
     ['debug-5', '--to=si --from=si --debug 1.12345678901234567890Y',
             {OUT=>"1.2Y"},
             {ERR=>"$prog: large input value '1.12345678901234567890Y':" .
                   " possible precision loss\n"}],

     ['ign-err-10','--invalid=fail 10000000000000000000',
             {ERR => "$prog: value too large to be printed: '1e+19' " .
                     "(consider using --to)\n"},
             {OUT => "10000000000000000000\n"},
             {EXIT=>2}],
     ['ign-err-11','--invalid=fail --to=si 6543219876543210000000000000000000',
             {ERR => "$prog: value too large to be converted: " .
                     "'6543219876543210000000000000000000'\n"},
             {OUT => "6543219876543210000000000000000000\n"},
             {EXIT => 2}],
  );
# Restrict these tests to systems with LDBL_DIG == 18
(system "$prog ---debug 1 2>&1|grep 'MAX_UNSCALED_DIGITS: 18' > /dev/null") == 0
  and push @Tests, @Limit_Tests;

my $lg = ' ';
if ($locale ne 'C')
  {
    open(LOC_GRP, "env LC_ALL=$locale printf \"%'d\" 1111|tr -d 1|")
      or die "Can't fork command: $!";
    $lg = <LOC_GRP>;
    close(LOC_GRP) || die "Failed to read locale grouping from printf";
    if (! defined $lg || $lg eq '')
      {
        warn "skipping locale grouping tests as no grouping character\n";
        $locale = 'C';
      }
  }

my @Locale_Tests =
  (
     # Locale that supports grouping, but without '--grouping' parameter
     ['lcl-grp-1', '--from=si 7M',   {OUT=>"7000000"},
             {ENV=>"LC_ALL=$locale"}],

     # Locale with grouping
     ['lcl-grp-2', '--from=si --grouping 7M',   {OUT=>"7${lg}000${lg}000"},
             {ENV=>"LC_ALL=$locale"}],

     # Locale with grouping and debug - no debug warning message
     ['lcl-grp-3', '--from=si --debug --grouping 7M',
             {OUT=>"7${lg}000${lg}000"},
             {ENV=>"LC_ALL=$locale"}],

     # Input with locale's grouping
     ['lcl-strtod-1', '--from=si 1${lg}234K', {OUT=>"1234000"},
             {ENV=>"LC_ALL=$locale"}],

     # Input with locale's grouping.  Note position not validated.
     ['lcl-strtod-2', '--from=si 12${lg}34K', {OUT=>"1234000"},
             {ENV=>"LC_ALL=$locale"}],

     # Input with locale's decimal-point
     ['lcl-strtod-3', '--from=si 12,2K', {OUT=>"12200"},
             {ENV=>"LC_ALL=$locale"}],

     # Input with locale's grouping and decimal-point
     ['lcl-strtod-4', '--from=si 1${lg}23,4K', {OUT=>"123400"},
             {ENV=>"LC_ALL=$locale"}],

     ['lcl-dbl-to-human-1', '--to=si 1100', {OUT=>"1,1k"},
             {ENV=>"LC_ALL=$locale"}],

     # Format + Grouping
     ['lcl-fmt-1', '--format "%\'f" 50000',{OUT=>"50${lg}000"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-fmt-2', '--format "--%\'10f--" 50000', {OUT=>"--    50${lg}000--"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-fmt-3', '--format "--%\'-10f--" 50000',{OUT=>"--50${lg}000    --"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-fmt-4', '--format "--%-10f--" --to=si 5000000',
             {OUT=>"--5,0M      --"},
             {ENV=>"LC_ALL=$locale"}],
     # handle zero/grouping in combination
     ['lcl-fmt-5', '--format="%\'06f" 1234',{OUT=>"01${lg}234"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-fmt-6', '--format="%0\'6f" 1234',{OUT=>"01${lg}234"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-fmt-7', '--format="%0\'\'6f" 1234',{OUT=>"01${lg}234"},
             {ENV=>"LC_ALL=$locale"}],

     # Single blank/NBSP acceptance between number and suffix
     ['lcl-suf-1', "-d '' --from=auto '2 K'",      {OUT => "2000"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-suf-2', "-d '' --from=auto '2\tK'",      {OUT => "2000"},
             {ENV=>"LC_ALL=$locale"}],
     # NBSP characters: U+00A0, U+2007, U+202F, U+2060
     ['lcl-suf-3', "--from=auto '2\xc2\xa0K'", {OUT => "2000"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-suf-4', "--from=auto '2\xe2\x80\x87Ki'", {OUT => "2048"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-suf-5', "--from=auto '2\xe2\x80\xafK'", {OUT => "2000"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-suf-6', "--from=auto '2\xe2\x81\xa0Ki'", {OUT => "2048"},
             {ENV=>"LC_ALL=$locale"}],
     # multi-byte blank char (em space, \u2003)
     #   Ensure trailing multi-byte blanks skipped
     ['lcl-suf-7', "'2\xe2\x80\x83 '", {OUT => "2  "},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-suf-8', "-d '' --from=auto '2Ki\xe2\x80\x83 '", {OUT => "2048"},
             {ENV=>"LC_ALL=$locale"}],
     #   Ensure multi-byte blank field separators not corrupted
     ['lcl-suf-9',  "--field=1 '1\xe2\x80\x832'", {OUT => "1 2"},
             {ENV=>"LC_ALL=$locale"}],
     ['lcl-suf-10', "--field=2 '1\xe2\x80\x832'", {OUT => "1 2"},
             {ENV=>"LC_ALL=$locale"}],
     #   Ensure multi-byte blank field separators width determined correctly
     ['lcl-suf-11', "--field=2 '1 \xe2\x80\x832'",
             {OUT => "1  2"}, {ENV=>"LC_ALL=$locale"}],

     # Support multi-byte delimiter
     ['lcl-delim-1', "-d '\xc2\xb7' --field=2 --from=auto '1\xc2\xb72K'",
             {OUT => "1\xc2\xb72000"}, {ENV=>"LC_ALL=$locale"}],

  );
if ($locale ne 'C')
  {
    # Reset locale to 'C' if LOCALE_FR_UTF8 doesn't output as expected
    # as determined by the separate printf program.
    my $printf_cmd = "env LC_ALL=$locale printf \"%'06.f\" 1234;" .
                     "env LC_ALL=$locale printf \"%'6.f\"  1234;";
    open(LOC_NUM, "$printf_cmd|") or die "Can't fork command: $!";
    my $loc_num = <LOC_NUM>;
    close(LOC_NUM) || die "Failed to read grouped number from printf";
    my $loc_match = "01${lg}234 1${lg}234";
    if ($loc_num ne $loc_match)
      {
        warn "skipping locale grouping tests as $loc_num != $loc_match\n";
        $locale = 'C';
      }
  }
push @Tests, @Locale_Tests if $locale ne 'C';

## Check all valid/invalid suffixes
foreach my $suf ( 'A' .. 'Z', 'a' .. 'z' ) {
  if ( $suf =~ /^[KkMGTPEZYRQ]$/ )
    {
      my $si_suf = $suf;
      my $iec_suf = $suf;
      if ( $suf eq "k" )
        {
          $iec_suf = "K";
        }
      if ( $suf eq "K" )
        {
          $si_suf = "k";
        }
      push @Tests, ["auto-suf-si-$suf","--from=si --to=si 1$suf",
              {OUT=>"1.0$si_suf"}];
      push @Tests, ["auto-suf-iec-$suf","--from=iec --to=iec 1$suf",
              {OUT=>"1.0$iec_suf"}];
      push @Tests, ["auto-suf-auto-$suf","--from=auto --to=iec 1${suf}i",
              {OUT=>"1.0$iec_suf"}];
      push @Tests, ["auto-suf-iec-to-ieci-$suf","--from=iec --to=iec-i 1${suf}",
              {OUT=>"1.0${iec_suf}i"}];
      push @Tests, ["auto-suf-ieci-to-iec-$suf",
              "--from=iec-i --to=iec 1${suf}i",{OUT=>"1.0${iec_suf}"}];
    }
  else
    {
      push @Tests, ["auto-suf-si-$suf","--from=si --to=si 1$suf",
              {ERR=>"$prog: invalid suffix in input: '1${suf}'\n"},
              {EXIT=>2}];
    }
}

# Prepend the command line argument and append a newline to end
# of each expected 'OUT' string.
my $t;

Test:
foreach $t (@Tests)
  {
    # Don't fiddle with expected OUT string if there's a nonzero exit status.
    foreach my $e (@$t)
      {
        ref $e eq 'HASH' && exists $e->{EXIT} && $e->{EXIT}
          and next Test;
      }

    foreach my $e (@$t)
      {
        ref $e eq 'HASH' && exists $e->{OUT}
          and $e->{OUT} .= "\n"
      }
  }

# Add test for null-terminated lines (after adjusting the OUT string, above).
push @Tests, @NullDelim_Tests;

my $save_temps = $ENV{SAVE_TEMPS};
my $verbose = $ENV{VERBOSE};

my $fail = run_tests ($program_name, $prog, \@Tests, $save_temps, $verbose);
exit $fail;
