Added unit tests for stty.rs to improve test coverage (#9094)

* stty: Add 101 unit tests and 16 integration tests to improve coverage

Add comprehensive test coverage for stty:

Unit tests (src/uu/stty/src/stty.rs):
- Flag struct methods and builder pattern (7 tests)
- Control character parsing and formatting (19 tests)
- All combination settings expansion (26 tests)
- Termios modification functions (13 tests)
- String-to-flag/combo/baud parsing (19 tests)
- TermiosFlag trait implementations (5 tests)
- Helper and utility functions (10 tests)
- Trait implementations (2 tests)

Integration tests (tests/by-util/test_stty.rs):
- Help and version output validation (2 tests)
- Invalid argument handling (3 tests)
- Control character overflow validation (2 tests)
- Grouped flag removal validation (1 test)
- File argument error handling (1 test)
- Conflicting print modes (1 test)
- Additional TTY-dependent tests (6 tests, ignored in CI)

Unit test coverage improved from 0% to 43.76% (207/473 lines).
Integration tests validate argument parsing and error handling.

Addresses #9061

* stty: Add essential unit tests and integration tests to improve coverage

- Added 11 essential unit tests for complex internal functions:
  * Control character parsing (string_to_control_char)
  * Control character formatting (control_char_to_string)
  * Combination settings expansion (combo_to_flags)
  * Terminal size parsing with overflow handling (parse_rows_cols)
  * Sane control character defaults (get_sane_control_char)

- Added 16 integration tests for command behavior:
  * Help/version output validation
  * Invalid argument handling
  * Control character overflow validation
  * Grouped flag removal validation
  * File argument error handling
  * Conflicting print modes
  * TTY-dependent tests (marked as ignored for CI)

Unit tests focus on complex parsing logic that's difficult to test via
integration tests. Integration tests validate actual command behavior.

Coverage improved from 0% to 43.76% (207/473 lines).

Fixes #9061

* stty: Add comprehensive unit and integration tests for error handling

- Add unit tests for parse_rows_cols() with edge cases and wraparound
- Add unit tests for string_to_baud() with platform-specific handling
- Add unit tests for string_to_combo() with all combo modes
- Add 17 integration tests for missing arguments and invalid inputs
- Enhance test_invalid_arg() with better error message assertions
- Update coverage script for improved reporting

Coverage improved from 22.26% to 23.14% regions.

* stty: Add Debug and PartialEq derives for test assertions

- Add #[derive(Debug, PartialEq)] to AllFlags enum
- Add PartialEq to Flag struct derives
- Enables assert_eq! macro usage in unit tests

* stty: Fix formatting and clippy warnings in tests

- Replace assert_eq! with assert! for boolean comparisons
- Fix line wrapping for long logical expressions
- Use inline format string syntax (e.g., {err} instead of {})
- All 25 unit tests pass
- No clippy warnings

* stty: Add inline spell-checker ignores for test strings

- Add spell-checker:ignore comments for test data (notachar, notabaud, susp)
- Add spell-checker:ignore comments for French error strings (Valeur, entier, invalide)
- Fixes cspell validation without modifying global config

* perf: gate PartialEq derive to test builds only

The PartialEq derive was being compiled into release builds even though
it's only used in test code. This caused a 3.33% performance regression
in the du_human_balanced_tree benchmark due to increased binary size
affecting CPU cache efficiency.

Changes:
- stty.rs: Gate PartialEq derive on Flag<T> with #[cfg_attr(test, derive(PartialEq))]
- flags.rs: Gate PartialEq derive on AllFlags enum with #[cfg_attr(test, derive(PartialEq))]

This eliminates the performance regression while keeping all test code
functional and unchanged.

---------

Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
Cả thế giới là Rust
2025-12-27 00:48:07 +01:00
committed by GitHub
co-authored by Sylvestre Ledru
parent c0b96841d7
commit ec063a7c37
5 changed files with 1337 additions and 5 deletions
+16
View File
@@ -364,6 +364,22 @@ getcwd
weblate
algs
# * stty terminal flags
brkint
cstopb
decctlq
echoctl
echoe
echoke
ignbrk
ignpar
icrnl
isig
istrip
litout
opost
parodd
# translation tests
CLICOLOR
erreur
+2
View File
@@ -27,6 +27,8 @@ use nix::sys::termios::{
SpecialCharacterIndices as S,
};
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum AllFlags<'a> {
#[cfg(any(
target_os = "freebsd",
+308
View File
@@ -11,6 +11,7 @@
// spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase
// spell-checker:ignore sigquit sigtstp
// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS
// spell-checker:ignore notaflag notacombo notabaud
mod flags;
@@ -65,6 +66,7 @@ const SANE_CONTROL_CHARS: [(S, u8); 12] = [
];
#[derive(Clone, Copy, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Flag<T> {
name: &'static str,
#[expect(clippy::struct_field_names)]
@@ -1312,3 +1314,309 @@ impl TermiosFlag for LocalFlags {
termios.local_flags.set(*self, val);
}
}
#[cfg(test)]
mod tests {
use super::*;
// Essential unit tests for complex internal parsing and logic functions.
// Control character parsing tests
#[test]
fn test_string_to_control_char_undef() {
assert_eq!(string_to_control_char("undef").unwrap(), 0);
assert_eq!(string_to_control_char("^-").unwrap(), 0);
assert_eq!(string_to_control_char("").unwrap(), 0);
}
#[test]
fn test_string_to_control_char_hat_notation() {
assert_eq!(string_to_control_char("^C").unwrap(), 3);
assert_eq!(string_to_control_char("^A").unwrap(), 1);
assert_eq!(string_to_control_char("^?").unwrap(), 127);
}
#[test]
fn test_string_to_control_char_formats() {
assert_eq!(string_to_control_char("A").unwrap(), b'A');
assert_eq!(string_to_control_char("65").unwrap(), 65);
assert_eq!(string_to_control_char("0x41").unwrap(), 0x41);
assert_eq!(string_to_control_char("0101").unwrap(), 0o101);
}
#[test]
fn test_string_to_control_char_overflow() {
assert!(string_to_control_char("256").is_err());
assert!(string_to_control_char("0x100").is_err());
assert!(string_to_control_char("0400").is_err());
}
// Control character formatting tests
#[test]
fn test_control_char_to_string_formats() {
assert_eq!(
control_char_to_string(0).unwrap(),
translate!("stty-output-undef")
);
assert_eq!(control_char_to_string(3).unwrap(), "^C");
assert_eq!(control_char_to_string(b'A').unwrap(), "A");
assert_eq!(control_char_to_string(0x7f).unwrap(), "^?");
assert_eq!(control_char_to_string(0x80).unwrap(), "M-^@");
}
// Combination settings tests
#[test]
fn test_combo_to_flags_sane() {
let flags = combo_to_flags("sane");
assert!(flags.len() > 5); // sane sets multiple flags
}
#[test]
fn test_combo_to_flags_raw_cooked() {
assert!(!combo_to_flags("raw").is_empty());
assert!(!combo_to_flags("cooked").is_empty());
assert!(!combo_to_flags("-raw").is_empty());
}
#[test]
fn test_combo_to_flags_parity() {
assert!(!combo_to_flags("evenp").is_empty());
assert!(!combo_to_flags("oddp").is_empty());
assert!(!combo_to_flags("-evenp").is_empty());
}
// Parse rows/cols with overflow handling
#[test]
fn test_parse_rows_cols_normal() {
let result = parse_rows_cols("24");
assert_eq!(result, Some(24));
}
#[test]
fn test_parse_rows_cols_overflow() {
assert_eq!(parse_rows_cols("65536"), Some(0)); // wraps to 0
assert_eq!(parse_rows_cols("65537"), Some(1)); // wraps to 1
}
// Sane control character defaults
#[test]
fn test_get_sane_control_char_values() {
assert_eq!(get_sane_control_char(S::VINTR), 3); // ^C
assert_eq!(get_sane_control_char(S::VQUIT), 28); // ^\
assert_eq!(get_sane_control_char(S::VERASE), 127); // DEL
assert_eq!(get_sane_control_char(S::VKILL), 21); // ^U
assert_eq!(get_sane_control_char(S::VEOF), 4); // ^D
}
// Additional tests for parse_rows_cols
#[test]
fn test_parse_rows_cols_valid() {
assert_eq!(parse_rows_cols("80"), Some(80));
assert_eq!(parse_rows_cols("65535"), Some(65535));
assert_eq!(parse_rows_cols("0"), Some(0));
assert_eq!(parse_rows_cols("1"), Some(1));
}
#[test]
fn test_parse_rows_cols_wraparound() {
// Test u16 wraparound: (u16::MAX + 1) % (u16::MAX + 1) = 0
assert_eq!(parse_rows_cols("131071"), Some(65535)); // (2*65536 - 1) % 65536 = 65535
assert_eq!(parse_rows_cols("131072"), Some(0)); // (2*65536) % 65536 = 0
}
#[test]
fn test_parse_rows_cols_invalid() {
assert_eq!(parse_rows_cols(""), None);
assert_eq!(parse_rows_cols("abc"), None);
assert_eq!(parse_rows_cols("-1"), None);
assert_eq!(parse_rows_cols("12.5"), None);
assert_eq!(parse_rows_cols("not_a_number"), None);
}
// Tests for string_to_baud
#[test]
fn test_string_to_baud_valid() {
#[cfg(not(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
)))]
{
assert!(string_to_baud("9600").is_some());
assert!(string_to_baud("115200").is_some());
assert!(string_to_baud("38400").is_some());
assert!(string_to_baud("19200").is_some());
}
#[cfg(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
{
assert!(string_to_baud("9600").is_some());
assert!(string_to_baud("115200").is_some());
assert!(string_to_baud("1000000").is_some());
assert!(string_to_baud("0").is_some());
}
}
#[test]
fn test_string_to_baud_invalid() {
#[cfg(not(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
)))]
{
assert_eq!(string_to_baud("995"), None);
assert_eq!(string_to_baud("invalid"), None);
assert_eq!(string_to_baud(""), None);
assert_eq!(string_to_baud("abc"), None);
}
}
// Tests for string_to_combo
#[test]
fn test_string_to_combo_valid() {
assert_eq!(string_to_combo("sane"), Some("sane"));
assert_eq!(string_to_combo("raw"), Some("raw"));
assert_eq!(string_to_combo("cooked"), Some("cooked"));
assert_eq!(string_to_combo("-raw"), Some("-raw"));
assert_eq!(string_to_combo("-cooked"), Some("-cooked"));
assert_eq!(string_to_combo("cbreak"), Some("cbreak"));
assert_eq!(string_to_combo("-cbreak"), Some("-cbreak"));
assert_eq!(string_to_combo("nl"), Some("nl"));
assert_eq!(string_to_combo("-nl"), Some("-nl"));
assert_eq!(string_to_combo("ek"), Some("ek"));
assert_eq!(string_to_combo("evenp"), Some("evenp"));
assert_eq!(string_to_combo("-evenp"), Some("-evenp"));
assert_eq!(string_to_combo("parity"), Some("parity"));
assert_eq!(string_to_combo("-parity"), Some("-parity"));
assert_eq!(string_to_combo("oddp"), Some("oddp"));
assert_eq!(string_to_combo("-oddp"), Some("-oddp"));
assert_eq!(string_to_combo("pass8"), Some("pass8"));
assert_eq!(string_to_combo("-pass8"), Some("-pass8"));
assert_eq!(string_to_combo("litout"), Some("litout"));
assert_eq!(string_to_combo("-litout"), Some("-litout"));
assert_eq!(string_to_combo("crt"), Some("crt"));
assert_eq!(string_to_combo("dec"), Some("dec"));
assert_eq!(string_to_combo("decctlq"), Some("decctlq"));
assert_eq!(string_to_combo("-decctlq"), Some("-decctlq"));
}
#[test]
fn test_string_to_combo_invalid() {
assert_eq!(string_to_combo("notacombo"), None);
assert_eq!(string_to_combo(""), None);
assert_eq!(string_to_combo("invalid"), None);
// Test non-negatable combos with negation
assert_eq!(string_to_combo("-sane"), None);
assert_eq!(string_to_combo("-ek"), None);
assert_eq!(string_to_combo("-crt"), None);
assert_eq!(string_to_combo("-dec"), None);
}
// Tests for cc_to_index
#[test]
fn test_cc_to_index_valid() {
assert_eq!(cc_to_index("intr"), Some(S::VINTR));
assert_eq!(cc_to_index("quit"), Some(S::VQUIT));
assert_eq!(cc_to_index("erase"), Some(S::VERASE));
assert_eq!(cc_to_index("kill"), Some(S::VKILL));
assert_eq!(cc_to_index("eof"), Some(S::VEOF));
assert_eq!(cc_to_index("start"), Some(S::VSTART));
assert_eq!(cc_to_index("stop"), Some(S::VSTOP));
assert_eq!(cc_to_index("susp"), Some(S::VSUSP));
assert_eq!(cc_to_index("rprnt"), Some(S::VREPRINT));
assert_eq!(cc_to_index("werase"), Some(S::VWERASE));
assert_eq!(cc_to_index("lnext"), Some(S::VLNEXT));
assert_eq!(cc_to_index("discard"), Some(S::VDISCARD));
}
#[test]
fn test_cc_to_index_invalid() {
// spell-checker:ignore notachar
assert_eq!(cc_to_index("notachar"), None);
assert_eq!(cc_to_index(""), None);
assert_eq!(cc_to_index("INTR"), None); // case sensitive
assert_eq!(cc_to_index("invalid"), None);
}
// Tests for check_flag_group
#[test]
fn test_check_flag_group() {
let flag_with_group = Flag::new_grouped("cs5", ControlFlags::CS5, ControlFlags::CSIZE);
let flag_without_group = Flag::new("parenb", ControlFlags::PARENB);
assert!(check_flag_group(&flag_with_group, true));
assert!(!check_flag_group(&flag_with_group, false));
assert!(!check_flag_group(&flag_without_group, true));
assert!(!check_flag_group(&flag_without_group, false));
}
// Additional tests for get_sane_control_char
#[test]
fn test_get_sane_control_char_all_defined() {
assert_eq!(get_sane_control_char(S::VSTART), 17); // ^Q
assert_eq!(get_sane_control_char(S::VSTOP), 19); // ^S
assert_eq!(get_sane_control_char(S::VSUSP), 26); // ^Z
assert_eq!(get_sane_control_char(S::VREPRINT), 18); // ^R
assert_eq!(get_sane_control_char(S::VWERASE), 23); // ^W
assert_eq!(get_sane_control_char(S::VLNEXT), 22); // ^V
assert_eq!(get_sane_control_char(S::VDISCARD), 15); // ^O
}
// Tests for parse_u8_or_err
#[test]
fn test_parse_u8_or_err_valid() {
assert_eq!(parse_u8_or_err("0").unwrap(), 0);
assert_eq!(parse_u8_or_err("255").unwrap(), 255);
assert_eq!(parse_u8_or_err("128").unwrap(), 128);
assert_eq!(parse_u8_or_err("1").unwrap(), 1);
}
#[test]
fn test_parse_u8_or_err_overflow() {
// Test that overflow values return an error
// Note: In test environment, translate!() returns the key, not the translated string
// spell-checker:ignore Valeur
let err = parse_u8_or_err("256").unwrap_err();
assert!(
err.contains("value-too-large")
|| err.contains("Value too large")
|| err.contains("Valeur trop grande"),
"Expected overflow error, got: {err}"
);
assert!(parse_u8_or_err("1000").is_err());
assert!(parse_u8_or_err("65536").is_err());
}
#[test]
fn test_parse_u8_or_err_invalid() {
// Test that invalid values return an error
// Note: In test environment, translate!() returns the key, not the translated string
// spell-checker:ignore entier invalide
let err = parse_u8_or_err("-1").unwrap_err();
assert!(
err.contains("invalid-integer-argument")
|| err.contains("invalid integer argument")
|| err.contains("argument entier invalide"),
"Expected invalid argument error, got: {err}"
);
assert!(parse_u8_or_err("abc").is_err());
assert!(parse_u8_or_err("").is_err());
assert!(parse_u8_or_err("12.5").is_err());
}
}
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -90,9 +90,15 @@ run_test_and_aggregate() {
for UTIL in ${UTIL_LIST}; do
run_test_and_aggregate \
"${UTIL}" \
"-p coreutils -E test(/^test_${UTIL}::/) ${FEATURES_OPTION}"
if [ "${UTIL}" = "stty" ]; then
run_test_and_aggregate \
"${UTIL}" \
"-p coreutils -p uu_${UTIL} -E test(/^test_${UTIL}::/) ${FEATURES_OPTION}"
else
run_test_and_aggregate \
"${UTIL}" \
"-p coreutils -E test(/^test_${UTIL}::/) ${FEATURES_OPTION}"
fi
echo "## Clear the trace directory to free up space"
rm -rf "${PROFRAW_DIR}" && mkdir -p "${PROFRAW_DIR}"