diff --git a/tests/cli.rs b/tests/cli.rs index 1f40cb6..11d4157 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -76,6 +76,131 @@ fn unknown_option_exits_one() { let (code, _out, err) = run_cli(&["src", "text", "codesize", "--not-a-real-flag"]); assert_eq!(code, EXIT_ERROR); assert!(err.contains("error:"), "stderr={err:?}"); + assert!( + err.contains("unknown option: --not-a-real-flag"), + "stderr={err:?}" + ); +} + +#[test] +fn bare_help_word_prints_usage_and_exits_zero() { + let (code, out, err) = run_cli(&["help"]); + assert_eq!(code, EXIT_SUCCESS); + assert!(out.contains("Usage:"), "stdout={out:?}"); + assert!(err.is_empty(), "stderr={err:?}"); +} + +#[test] +fn unknown_single_dash_option_names_it_and_exits_one() { + let (code, _out, err) = run_cli(&["src", "text", "codesize", "-x"]); + assert_eq!(code, EXIT_ERROR); + assert!(err.contains("unknown option: -x"), "stderr={err:?}"); +} + +#[test] +fn lone_dash_is_treated_as_a_positional_not_an_option() { + // A single "-" does not start the unknown-option branch; it becomes a + // fourth positional, so the ruleset parses fine and "-" is an unmatched + // path that yields a discovery error, not an "unknown option" error. + let (code, _out, err) = run_cli(&["-", "text", "codesize"]); + assert_eq!(code, EXIT_ERROR); + assert!(!err.contains("unknown option"), "stderr={err:?}"); +} + +#[test] +fn missing_value_for_option_names_the_flag_and_exits_one() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "text", "codesize", "--reportfile"]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("missing value for --reportfile"), + "stderr={err:?}" + ); +} + +#[test] +fn minimumpriority_rejects_non_integer_value() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--minimumpriority", + "nope", + ]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("--minimumpriority requires an integer"), + "stderr={err:?}" + ); +} + +#[test] +fn minimumpriority_rejects_out_of_range_value() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + for value in ["0", "6"] { + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--minimumpriority", + value, + ]); + assert_eq!(code, EXIT_ERROR, "value={value}"); + assert!( + err.contains("--minimumpriority must be between 1 and 5"), + "value={value} stderr={err:?}" + ); + } +} + +#[test] +fn maximumpriority_rejects_non_integer_and_out_of_range_value() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--maximumpriority", + "bogus", + ]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("--maximumpriority requires an integer"), + "stderr={err:?}" + ); + + let (code, _out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--maximumpriority", + "9", + ]); + assert_eq!(code, EXIT_ERROR); + assert!( + err.contains("--maximumpriority must be between 1 and 5"), + "stderr={err:?}" + ); +} + +#[test] +fn suffixes_without_leading_dot_are_normalized() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "b.txt", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + dir.path().to_str().unwrap(), + "text", + "codesize", + "--suffixes", + "txt", + ]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("b.txt"), "stdout={out:?}"); } #[test] @@ -607,6 +732,22 @@ fn minimumpriority_drops_lower_priority_rules() { assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); } +#[test] +fn minimumpriority_one_is_accepted() { + // Kills parse_priority range mutant `1..=5` → `2..=5`. + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--minimumpriority", + "1", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + #[test] fn maximumpriority_drops_higher_priority_rules() { // maximumpriority 4 keeps priority >= 4; ExcessiveParameterList (3) drops. @@ -623,6 +764,65 @@ fn maximumpriority_drops_higher_priority_rules() { assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); } +#[test] +fn maximumpriority_five_is_accepted() { + // Kills parse_priority range mutant that rejects 5; filters EPL (priority 3). + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--maximumpriority", + "5", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + +#[test] +fn value_option_before_positionals_consumes_exactly_one_argument() { + // Kills parse_args `i += 1` body/±1 mutants on the value skip. + // Correct: suffixes=[.txt] so hit.txt is analyzed and miss.rs is not. + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "hit.txt", &fixture_with_params(11)); + write_file(dir.path(), "miss.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + "--suffixes", + "txt", + dir.path().to_str().unwrap(), + "text", + "codesize", + ]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("hit.txt"), "stdout={out:?}"); + assert!(!out.contains("miss.rs"), "stdout={out:?}"); +} + +#[test] +fn value_option_does_not_skip_the_following_flag() { + // Kills parse_args `i += 2` mutant: must still honour ignore-violations. + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let report = dir.path().join("out.txt"); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--reportfile", + report.to_str().unwrap(), + "--ignore-violations-on-exit", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(out.is_empty(), "stdout={out:?}"); + assert!(report.is_file(), "report missing"); + let body = fs::read_to_string(&report).unwrap(); + assert!( + body.contains("ExcessiveParameterList"), + "report={body:?}" + ); +} + #[test] fn xml_ruleset_path_loads_refs_excludes_and_overrides() { let dir = TempDir::new().unwrap(); @@ -727,3 +927,225 @@ fn verbose_prints_ruleset_load_diagnostics() { ); } } + +#[test] +fn two_positionals_print_usage_and_exits_one() { + let (code, out, err) = run_cli(&["src", "text"]); + assert_eq!(code, EXIT_ERROR); + assert!(out.is_empty(), "stdout={out:?}"); + assert!(err.contains("Usage:"), "stderr={err:?}"); +} + +#[test] +fn usage_lists_every_documented_option_name() { + let (code, out, err) = run_cli(&["--help"]); + assert_eq!(code, EXIT_SUCCESS); + assert!(err.is_empty(), "stderr={err:?}"); + for needle in [ + "Usage: messrust ", + "--minimumpriority", + "--maximumpriority", + "--reportfile", + "--suffixes", + "--exclude", + "--only, --enable", + "--disable", + "--ignore-tests", + "--strict", + "--color", + "--verbose, -v", + "--ignore-errors-on-exit", + "--ignore-violations-on-exit", + "--version", + "--help, -h", + "text", + "json", + "sarif", + ] { + assert!(out.contains(needle), "missing {needle:?} in {out:?}"); + } +} + +#[test] +fn missing_value_for_each_value_option_names_the_flag() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + for flag in [ + "--suffixes", + "--exclude", + "--only", + "--enable", + "--disable", + "--minimumpriority", + "--maximumpriority", + ] { + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "text", "codesize", flag]); + assert_eq!(code, EXIT_ERROR, "flag={flag}"); + let expected = format!("missing value for {flag}"); + assert!(err.contains(&expected), "flag={flag} stderr={err:?}"); + } +} + +#[test] +fn comma_separated_paths_are_each_analyzed() { + let dir = TempDir::new().unwrap(); + let a = write_file(dir.path(), "a.rs", &fixture_with_params(11)); + let b = write_file(dir.path(), "b.rs", &fixture_with_params(11)); + let paths = format!("{},{}", a.display(), b.display()); + let (code, out, err) = run_cli(&[&paths, "text", "codesize"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("a.rs"), "stdout={out:?}"); + assert!(out.contains("b.rs"), "stdout={out:?}"); +} + +#[test] +fn empty_path_segments_in_comma_list_are_dropped() { + let dir = TempDir::new().unwrap(); + let a = write_file(dir.path(), "only.rs", &fixture_with_params(11)); + let paths = format!(",{},", a.display()); + let (code, out, err) = run_cli(&[&paths, "text", "codesize"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("only.rs"), "stdout={out:?}"); +} + +#[test] +fn only_wins_when_both_only_and_enable_are_set() { + let dir = TempDir::new().unwrap(); + // ExcessiveMethodLength needs a long body; ExcessiveParameterList is the + // easy codesize finding. Keep only ExcessiveMethodLength via --only while + // --enable names the parameter-list rule — --only must win, so no finding. + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--only", + "ExcessiveMethodLength", + "--enable", + "ExcessiveParameterList", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?} stdout={out:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); +} + +#[test] +fn ignore_errors_alone_with_only_processing_errors_exits_zero() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "bad.rs", "fn broken( {\n"); + let (code, out, err) = run_cli(&[ + dir.path().to_str().unwrap(), + "text", + "codesize", + "--ignore-errors-on-exit", + ]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?} stdout={out:?}"); + assert!(out.contains("bad.rs"), "stdout={out:?}"); +} + +#[test] +fn without_verbose_unimplemented_rule_is_silent() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let stub = dir.path().join("stub.xml"); + fs::write( + &stub, + r#" + + + +"#, + ) + .unwrap(); + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "text", stub.to_str().unwrap()]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(!err.contains("warning:"), "stderr={err:?}"); +} + +#[test] +fn unknown_format_lists_available_formats() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "clean.rs", &fixture_with_params(0)); + let (code, _out, err) = run_cli(&[path.to_str().unwrap(), "not-a-format", "codesize"]); + assert_eq!(code, EXIT_ERROR); + assert!(err.contains("unknown report format"), "stderr={err:?}"); + assert!(err.contains("Available:"), "stderr={err:?}"); + assert!(err.contains("text"), "stderr={err:?}"); + assert!(err.contains("json"), "stderr={err:?}"); +} + +#[test] +fn comma_separated_rulesets_all_load() { + let dir = TempDir::new().unwrap(); + // codesize finds ExcessiveParameterList; naming (opinionated) finds ShortVariable. + let hot = write_file( + dir.path(), + "hot.rs", + "fn entry_point(param_0: i32, param_1: i32, param_2: i32, param_3: i32, param_4: i32, param_5: i32, param_6: i32, param_7: i32, param_8: i32, param_9: i32, param_10: i32) { let x = 1; let _ = x; }\n", + ); + let (code, out, err) = run_cli(&[hot.to_str().unwrap(), "text", "codesize,naming"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("ExcessiveParameterList"), "stdout={out:?}"); + assert!(out.contains("ShortVariable"), "naming ruleset must load: stdout={out:?}"); +} + +#[test] +fn default_suffixes_analyze_rs_only() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "hot.rs", &fixture_with_params(11)); + write_file(dir.path(), "hot.txt", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[dir.path().to_str().unwrap(), "text", "codesize"]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("hot.rs"), "stdout={out:?}"); + assert!(!out.contains("hot.txt"), "stdout={out:?}"); +} + +#[test] +fn exclude_accepts_comma_separated_substrings() { + let dir = TempDir::new().unwrap(); + write_file(dir.path(), "keep/a.rs", &fixture_with_params(11)); + write_file(dir.path(), "drop_one/b.rs", &fixture_with_params(11)); + write_file(dir.path(), "drop_two/c.rs", &fixture_with_params(11)); + let (code, out, err) = run_cli(&[ + dir.path().to_str().unwrap(), + "text", + "codesize", + "--exclude", + "drop_one,drop_two", + ]); + assert_eq!(code, EXIT_VIOLATION, "stderr={err:?}"); + assert!(out.contains("a.rs"), "stdout={out:?}"); + assert!(!out.contains("b.rs"), "stdout={out:?}"); + assert!(!out.contains("c.rs"), "stdout={out:?}"); +} + +#[test] +fn reportfile_parent_missing_is_an_error() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + let missing = dir.path().join("nope").join("report.txt"); + let (code, out, err) = run_cli(&[ + path.to_str().unwrap(), + "text", + "codesize", + "--reportfile", + missing.to_str().unwrap(), + ]); + assert_eq!(code, EXIT_ERROR, "stderr={err:?}"); + assert!(out.is_empty(), "stdout={out:?}"); + assert!(err.contains("error:"), "stderr={err:?}"); +} + +#[test] +fn version_does_not_run_analysis() { + let dir = TempDir::new().unwrap(); + let path = write_file(dir.path(), "fixture.rs", &fixture_with_params(11)); + // --version is only recognized as argv[0]; later args are ignored because + // handle_info_flags returns before parse/analysis. + let (code, out, err) = run_cli(&["--version", path.to_str().unwrap()]); + assert_eq!(code, EXIT_SUCCESS, "stderr={err:?}"); + assert!(out.starts_with("messrust "), "stdout={out:?}"); + assert!(!out.contains("ExcessiveParameterList"), "stdout={out:?}"); + assert!(err.is_empty(), "stderr={err:?}"); +}