Skip to content

Commit 0c5be9f

Browse files
committed
numfmt: add unit tests for detailed_error_message, parse_number_part, and apply_grouping
1 parent 22e31ed commit 0c5be9f

3 files changed

Lines changed: 159 additions & 17 deletions

File tree

src/uu/numfmt/src/format.rs

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,10 @@ fn apply_grouping(s: &str) -> String {
262262
grouped.push_str(&integer[..first_group]);
263263
for chunk in integer.as_bytes()[first_group..].chunks(3) {
264264
grouped.push_str(grouping_separator);
265-
// SAFETY: integer is known to be valid UTF-8 ASCII digits
266-
grouped.push_str(std::str::from_utf8(chunk).unwrap());
265+
// integer is valid UTF-8 ASCII digits, so this cannot fail
266+
if let Ok(s) = std::str::from_utf8(chunk) {
267+
grouped.push_str(s);
268+
}
267269
}
268270

269271
if !fraction.is_empty() {
@@ -995,4 +997,140 @@ mod tests {
995997
assert_eq!(raw_suffix as i32, RawSuffix::Q as i32);
996998
assert_eq!(value, 5.0);
997999
}
1000+
1001+
#[test]
1002+
fn test_detailed_error_message_empty() {
1003+
let result = detailed_error_message("", Unit::Auto, "");
1004+
assert!(result.is_some());
1005+
}
1006+
1007+
#[test]
1008+
fn test_detailed_error_message_valid_number() {
1009+
// A plain valid number should return None (no error)
1010+
assert!(detailed_error_message("123", Unit::Auto, "").is_none());
1011+
assert!(detailed_error_message("5K", Unit::Auto, "").is_none());
1012+
assert!(detailed_error_message("-3.5M", Unit::Auto, "").is_none());
1013+
}
1014+
1015+
#[test]
1016+
fn test_detailed_error_message_trailing_garbage() {
1017+
// Number with suffix followed by extra chars
1018+
let result = detailed_error_message("5Kx", Unit::Auto, "").unwrap();
1019+
assert!(
1020+
result.contains("numfmt-error-invalid-specific-suffix")
1021+
|| result.contains("invalid suffix")
1022+
);
1023+
}
1024+
1025+
#[test]
1026+
fn test_detailed_error_message_dot_only() {
1027+
let result = detailed_error_message(".", Unit::Auto, "").unwrap();
1028+
assert!(
1029+
result.contains("numfmt-error-invalid-suffix") || result.contains("invalid suffix")
1030+
);
1031+
}
1032+
1033+
#[test]
1034+
fn test_detailed_error_message_trailing_dot() {
1035+
let result = detailed_error_message("5.", Unit::Auto, "").unwrap();
1036+
assert!(
1037+
result.contains("numfmt-error-invalid-number") || result.contains("invalid number")
1038+
);
1039+
}
1040+
1041+
#[test]
1042+
fn test_detailed_error_message_unit_separator() {
1043+
// With unit separator, "5 K" is valid
1044+
assert!(detailed_error_message("5 K", Unit::Auto, " ").is_none());
1045+
1046+
// "5 Kx" should report trailing garbage after the suffix
1047+
let result = detailed_error_message("5 Kx", Unit::Auto, " ");
1048+
assert!(result.is_some());
1049+
}
1050+
1051+
#[test]
1052+
fn test_parse_number_part_valid() {
1053+
assert_eq!(parse_number_part("42", "42").unwrap(), 42.0);
1054+
assert_eq!(parse_number_part("-3.5", "-3.5").unwrap(), -3.5);
1055+
assert_eq!(parse_number_part("0", "0").unwrap(), 0.0);
1056+
}
1057+
1058+
#[test]
1059+
fn test_parse_number_part_trailing_dot() {
1060+
assert!(parse_number_part("5.", "5.").is_err());
1061+
}
1062+
1063+
#[test]
1064+
fn test_parse_number_part_non_numeric() {
1065+
assert!(parse_number_part("abc", "abc").is_err());
1066+
assert!(parse_number_part("", "").is_err());
1067+
}
1068+
1069+
#[test]
1070+
fn test_apply_grouping_short_numbers() {
1071+
// Numbers with fewer than 4 digits should be unchanged
1072+
assert_eq!(apply_grouping("0"), "0");
1073+
assert_eq!(apply_grouping("999"), "999");
1074+
assert_eq!(apply_grouping("-99"), "-99");
1075+
}
1076+
1077+
#[test]
1078+
fn test_apply_grouping_with_fraction() {
1079+
// Fraction part should not be grouped
1080+
let result = apply_grouping("1234.567");
1081+
// Depending on locale, separator may or may not be present
1082+
assert!(result.contains("567"));
1083+
assert!(result.contains('.'));
1084+
}
1085+
1086+
#[test]
1087+
fn test_apply_grouping_negative() {
1088+
let result = apply_grouping("-1234");
1089+
assert!(result.starts_with('-'));
1090+
}
1091+
1092+
#[test]
1093+
fn test_apply_grouping_large_numbers() {
1094+
// These tests verify grouping structure; actual separator depends on locale
1095+
let result = apply_grouping("1000000");
1096+
// Should have separators inserted (length grows if separator is non-empty)
1097+
assert!(result.len() >= 7);
1098+
1099+
let result = apply_grouping("1234567890");
1100+
assert!(result.len() >= 10);
1101+
1102+
let result = apply_grouping("-9999999999999");
1103+
assert!(result.starts_with('-'));
1104+
assert!(result.len() >= 13);
1105+
}
1106+
1107+
#[test]
1108+
fn test_apply_grouping_tiny_fraction() {
1109+
// Small decimal: integer part < 4 digits, so no grouping
1110+
assert_eq!(apply_grouping("0.000001"), "0.000001");
1111+
assert_eq!(apply_grouping("1.23456789"), "1.23456789");
1112+
}
1113+
1114+
#[test]
1115+
fn test_apply_grouping_exactly_four_digits() {
1116+
let result = apply_grouping("1000");
1117+
// Should be grouped (4 digits)
1118+
assert!(result.len() >= 4);
1119+
}
1120+
1121+
#[test]
1122+
fn test_parse_number_part_large_and_tiny() {
1123+
assert_eq!(
1124+
parse_number_part("999999999999", "999999999999").unwrap(),
1125+
999_999_999_999.0
1126+
);
1127+
assert_eq!(
1128+
parse_number_part("0.000000001", "0.000000001").unwrap(),
1129+
0.000_000_001
1130+
);
1131+
assert_eq!(
1132+
parse_number_part("-99999999", "-99999999").unwrap(),
1133+
-99_999_999.0
1134+
);
1135+
}
9981136
}

src/uu/numfmt/src/numfmt.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ mod units;
3232

3333
/// Format a single line and write it, handling `--invalid` error modes.
3434
///
35+
/// `fmt_buf` is a caller-owned scratch buffer reused across calls to avoid
36+
/// allocating a new `Vec` on every line in non-abort invalid modes.
37+
///
3538
/// Returns `true` if the line contained invalid input (only possible in
3639
/// non-abort modes).
3740
fn format_and_write<W: std::io::Write>(
3841
writer: &mut W,
3942
input_line: &[u8],
4043
options: &NumfmtOptions,
4144
eol: Option<u8>,
45+
fmt_buf: &mut Vec<u8>,
4246
) -> UResult<bool> {
4347
// GNU truncates at the first embedded null byte.
4448
let line = match memchr::memchr(b'\0', input_line) {
@@ -49,8 +53,8 @@ fn format_and_write<W: std::io::Write>(
4953
// In non-abort modes we buffer the formatted output so that on error we
5054
// can emit the original line instead.
5155
let buffer_output = !matches!(options.invalid, InvalidModes::Abort);
52-
let mut buf = Vec::new();
53-
let dest: &mut dyn std::io::Write = if buffer_output { &mut buf } else { writer };
56+
fmt_buf.clear();
57+
let dest: &mut dyn std::io::Write = if buffer_output { fmt_buf } else { writer };
5458

5559
let result = if options.delimiter.is_some() {
5660
write_formatted_with_delimiter(dest, line, options, eol)
@@ -86,7 +90,7 @@ fn format_and_write<W: std::io::Write>(
8690
}
8791

8892
if buffer_output {
89-
writer.write_all(&buf)?;
93+
writer.write_all(fmt_buf)?;
9094
}
9195
Ok(false)
9296
}

util/gnu-patches/tests_numfmt.patch

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,28 @@ Index: gnu/tests/numfmt/numfmt.pl
2929
['field-range-err-6', '--field - --field 1- --to=si 10',
3030
- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
3131
+ {EXIT=>1},
32-
+ {ERR_SUBST=>"s/.*//msg"},
33-
+ {ERR=>""}],
32+
+ {ERR_SUBST=>"s/\n.*//s"},
33+
+ {ERR=>"error: the argument '--field <FIELDS>' cannot be used multiple times"}],
3434
['field-range-err-7', '--field -1 --field 1- --to=si 10',
3535
- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
3636
+ {EXIT=>1},
37-
+ {ERR_SUBST=>"s/.*//msg"},
38-
+ {ERR=>""}],
37+
+ {ERR_SUBST=>"s/\n.*//s"},
38+
+ {ERR=>"error: the argument '--field <FIELDS>' cannot be used multiple times"}],
3939
['field-range-err-8', '--field -1 --field 1,2,3 --to=si 10',
4040
- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
4141
+ {EXIT=>1},
42-
+ {ERR_SUBST=>"s/.*//msg"},
43-
+ {ERR=>""}],
42+
+ {ERR_SUBST=>"s/\n.*//s"},
43+
+ {ERR=>"error: the argument '--field <FIELDS>' cannot be used multiple times"}],
4444
['field-range-err-9', '--field 1- --field 1,2,3 --to=si 10',
4545
- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
4646
+ {EXIT=>1},
47-
+ {ERR_SUBST=>"s/.*//msg"},
48-
+ {ERR=>""}],
47+
+ {ERR_SUBST=>"s/\n.*//s"},
48+
+ {ERR=>"error: the argument '--field <FIELDS>' cannot be used multiple times"}],
4949
['field-range-err-10','--field 1,2,3 --field 1- --to=si 10',
5050
- {EXIT=>1}, {ERR=>"$prog: multiple field specifications\n"}],
5151
+ {EXIT=>1},
52-
+ {ERR_SUBST=>"s/.*//msg"},
53-
+ {ERR=>""}],
52+
+ {ERR_SUBST=>"s/\n.*//s"},
53+
+ {ERR=>"error: the argument '--field <FIELDS>' cannot be used multiple times"}],
5454
['field-range-err-11','--field 1-2-3 --to=si 10',
5555
- {EXIT=>1}, {ERR=>"$prog: invalid field range\n$try"}],
5656
+ {EXIT=>1}, {ERR=>"$prog: range '1-2-3' was invalid: failed to parse range\n"}],
@@ -113,8 +113,8 @@ Index: gnu/tests/numfmt/numfmt.pl
113113
['help-1', '--foobar',
114114
- {ERR=>"$prog: unrecognized option\n$try"},
115115
- {ERR_SUBST=>"s/option.*/option/; s/unknown/unrecognized/"},
116-
+ {ERR=>""},
117-
+ {ERR_SUBST=>"s/.*//msg"},
116+
+ {ERR=>"error: unexpected argument '--foobar' found"},
117+
+ {ERR_SUBST=>"s/\n.*//s"},
118118
{EXIT=>1}],
119119

120120
## Format string - check error detection

0 commit comments

Comments
 (0)