Skip to content

Commit 02a17c1

Browse files
committed
numfmt: add unit tests for detailed_error_message, parse_number_part, and apply_grouping
1 parent e746df6 commit 02a17c1

File tree

3 files changed

+159
-17
lines changed

3 files changed

+159
-17
lines changed

src/uu/numfmt/src/format.rs

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

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

src/uu/numfmt/src/numfmt.rs

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

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

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

8993
if buffer_output {
90-
writer.write_all(&buf)?;
94+
writer.write_all(fmt_buf)?;
9195
}
9296
Ok(false)
9397
}

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)