diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c26966d..70d40fe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,12 +14,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- rust: [stable, beta]
steps:
- uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@master
+ - uses: dtolnay/rust-toolchain@stable
with:
- toolchain: ${{ matrix.rust }}
components: clippy, rustfmt
- name: Run tests
run: cargo test --verbose
@@ -28,12 +26,12 @@ jobs:
- name: Check formatting
run: cargo fmt -- --check
- name: Code coverage
- if: matrix.os == 'ubuntu-latest' && matrix.rust == 'stable'
+ if: matrix.os == 'ubuntu-latest'
run: |
cargo install cargo-tarpaulin
- cargo tarpaulin --out Xml --output-dir coverage
+ cargo tarpaulin --tests --out Xml --output-dir coverage
- name: Upload to codecov
- if: matrix.os == 'ubuntu-latest' && matrix.rust == 'stable'
+ if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v4
with:
files: ./coverage/cobertura.xml
diff --git a/coverage-test/cobertura.xml b/coverage-test/cobertura.xml
new file mode 100644
index 0000000..3ab9380
--- /dev/null
+++ b/coverage-test/cobertura.xml
@@ -0,0 +1 @@
+/Users/jmachen/code/rpg
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index 15bf6f4..a738655 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1218,4 +1218,265 @@ mod tests {
print_columns(passwords.clone(), 1, false);
print_columns(passwords, 3, false);
}
+
+ #[test]
+ fn test_parse_exclude_chars_non_printable_start() {
+ // Test range with start character < 32 (non-printable)
+ // This should skip the range logic and treat as individual chars
+ let result = parse_exclude_chars(vec!["\x1f-9".to_string()]);
+ // Should succeed but treat as individual characters, not a range
+ assert!(result.is_ok());
+ let chars = result.unwrap();
+ // Should contain the characters from the string, not a range expansion
+ assert!(chars.len() >= 2); // At least \x1f, -, and 9
+ }
+
+ #[test]
+ fn test_parse_exclude_chars_non_printable_end() {
+ // Test range with end character >= 127 (non-printable)
+ // This should skip the range logic
+ let result = parse_exclude_chars(vec!["a-\x7f".to_string()]);
+ // Should succeed but treat as individual characters
+ assert!(result.is_ok());
+ // Should not expand as a range
+ let chars = result.unwrap();
+ // The range logic should be skipped due to end >= 127
+ // So it should treat as individual characters
+ assert!(chars.contains(&'a'));
+ }
+
+ #[test]
+ fn test_parse_exclude_chars_duplicate_handling() {
+ // Test that duplicates are properly handled
+ let result = parse_exclude_chars(vec!["a".to_string(), "a".to_string(), "b".to_string()]);
+ assert!(result.is_ok());
+ let chars = result.unwrap();
+ // Should only contain one 'a' and one 'b'
+ assert_eq!(chars.iter().filter(|&&c| c == 'a').count(), 1);
+ assert_eq!(chars.iter().filter(|&&c| c == 'b').count(), 1);
+ }
+
+ #[test]
+ fn test_parse_exclude_chars_duplicate_in_range_and_individual() {
+ // Test that characters in ranges are not duplicated when also specified individually
+ let result = parse_exclude_chars(vec!["a-c".to_string(), "b".to_string()]);
+ assert!(result.is_ok());
+ let chars = result.unwrap();
+ // Should contain a, b, c each once
+ assert_eq!(chars.iter().filter(|&&c| c == 'a').count(), 1);
+ assert_eq!(chars.iter().filter(|&&c| c == 'b').count(), 1);
+ assert_eq!(chars.iter().filter(|&&c| c == 'c').count(), 1);
+ }
+
+ #[test]
+ fn test_parse_exclude_chars_boundary_conditions() {
+ // Test range exactly at printable ASCII boundaries
+ // Space (32) to ~ (126) should work
+ let result = parse_exclude_chars(vec![" -~".to_string()]);
+ assert!(result.is_ok());
+ let chars = result.unwrap();
+ // Should expand to all printable ASCII
+ assert!(chars.contains(&' '));
+ assert!(chars.contains(&'~'));
+ }
+
+ #[test]
+ fn test_build_char_set_all_types_disabled_lowercase_available() {
+ // Test with all character types disabled, but lowercase still available
+ let args = create_test_args(true, true, true, vec![]);
+ let char_set = build_char_set(&args).unwrap();
+ // Should only contain lowercase letters
+ assert!(!char_set.is_empty());
+ assert!(char_set.contains(&b'a'));
+ assert!(char_set.contains(&b'z'));
+ assert!(!char_set.contains(&b'A'));
+ assert!(!char_set.contains(&b'0'));
+ assert!(!char_set.contains(&b'!'));
+ }
+
+ #[test]
+ fn test_build_char_set_include_chars_empty_after_exclude() {
+ // Test include_chars where exclude_chars removes all characters
+ let mut args = create_test_args(false, false, false, vec!['a', 'b', 'c']);
+ args.include_chars = Some(vec!['a', 'b', 'c']);
+ let result = build_char_set(&args);
+ assert!(result.is_err());
+ assert!(matches!(
+ result.unwrap_err(),
+ PasswordError::EmptyCharacterSet
+ ));
+ }
+
+ #[test]
+ fn test_build_char_set_include_chars_with_exclusions_partial() {
+ // Test include_chars with exclude_chars that removes some but not all
+ let mut args = create_test_args(false, false, false, vec!['a']);
+ args.include_chars = Some(vec!['a', 'b', 'c', 'd', 'e']);
+ let char_set = build_char_set(&args).unwrap();
+ assert_eq!(char_set.len(), 4); // b, c, d, e
+ assert!(!char_set.contains(&b'a'));
+ assert!(char_set.contains(&b'b'));
+ assert!(char_set.contains(&b'c'));
+ assert!(char_set.contains(&b'd'));
+ assert!(char_set.contains(&b'e'));
+ }
+
+ #[test]
+ fn test_password_error_display_all_variants() {
+ // Ensure all error variants are tested for Display implementation
+ let err = PasswordError::InvalidLength;
+ let msg = err.to_string();
+ assert!(msg.contains("Password length must be greater than 0"));
+
+ let err = PasswordError::InvalidLengthTooLong;
+ let msg = err.to_string();
+ assert!(msg.contains("exceeds maximum of 10,000"));
+
+ let err = PasswordError::InvalidCount;
+ let msg = err.to_string();
+ assert!(msg.contains("Password count must be greater than 0"));
+
+ let err = PasswordError::EmptyCharacterSet;
+ let msg = err.to_string();
+ assert!(msg.contains("All characters have been excluded"));
+ assert!(msg.contains("Hint"));
+
+ let err = PasswordError::AllTypesDisabled;
+ let msg = err.to_string();
+ assert!(msg.contains("All character types are disabled"));
+ assert!(msg.contains("Hint"));
+ }
+
+ #[test]
+ fn test_password_error_source() {
+ // Test that PasswordError implements std::error::Error
+ let err = PasswordError::InvalidLength;
+ // Should be able to use as Error trait object
+ let _err_ref: &dyn std::error::Error = &err;
+ }
+
+ #[test]
+ fn test_generate_password_with_minimums_exceeding_length() {
+ use rand::{SeedableRng, rngs::StdRng};
+
+ let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
+ // Request 5 minimums but length is only 4
+ // Minimums take precedence, so password will be length 5
+ let mut rng = StdRng::seed_from_u64(1001);
+ let password = generate_password_with_minimums(&char_set, 4, Some(5), None, None, &mut rng);
+
+ // Should generate a password with at least 5 capitals (minimum takes precedence)
+ assert!(password.len() >= 5);
+ let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
+ assert!(capitals >= 5); // Minimum requirement is met
+ }
+
+ #[test]
+ fn test_generate_password_with_minimums_sum_exceeds_length() {
+ use rand::{SeedableRng, rngs::StdRng};
+
+ let char_set = vec![
+ b'a', b'b', b'c', b'A', b'B', b'C', b'0', b'1', b'2', b'!', b'@', b'#',
+ ];
+ // Request min_capitals=3, min_numerals=3, min_symbols=3, but length=6
+ // Minimums take precedence, so password will be at least length 9
+ let mut rng = StdRng::seed_from_u64(1002);
+ let password =
+ generate_password_with_minimums(&char_set, 6, Some(3), Some(3), Some(3), &mut rng);
+
+ // Password length should be at least 9 (sum of minimums)
+ // May be more if minimums are applied then filled up to length
+ assert!(password.len() >= 9);
+ let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
+ let numerals = password.chars().filter(|c| c.is_ascii_digit()).count();
+ let symbols = password.chars().filter(|c| !c.is_alphanumeric()).count();
+
+ // Should meet all minimum requirements
+ assert!(capitals >= 3);
+ assert!(numerals >= 3);
+ assert!(symbols >= 3);
+ }
+
+ #[test]
+ fn test_generate_password_with_minimums_exact_length() {
+ use rand::{SeedableRng, rngs::StdRng};
+
+ let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
+ // Request min_capitals=2, min_numerals=2, length=4
+ let mut rng = StdRng::seed_from_u64(1003);
+ let password =
+ generate_password_with_minimums(&char_set, 4, Some(2), Some(2), None, &mut rng);
+
+ assert_eq!(password.len(), 4);
+ let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
+ let numerals = password.chars().filter(|c| c.is_ascii_digit()).count();
+
+ assert!(capitals >= 2);
+ assert!(numerals >= 2);
+ }
+
+ #[test]
+ fn test_generate_password_from_pattern_all_same_type() {
+ use rand::{SeedableRng, rngs::StdRng};
+
+ let char_set = vec![b'a', b'b', b'c', b'A', b'B', b'C', b'0', b'1', b'2'];
+ let pattern = vec![
+ PatternChar::Lowercase,
+ PatternChar::Lowercase,
+ PatternChar::Lowercase,
+ ];
+
+ let mut rng = StdRng::seed_from_u64(2001);
+ let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
+
+ assert_eq!(password.len(), 3);
+ for c in password.chars() {
+ assert!(c.is_ascii_lowercase());
+ }
+ }
+
+ #[test]
+ fn test_generate_password_from_pattern_very_long() {
+ use rand::{SeedableRng, rngs::StdRng};
+
+ let char_set = vec![b'a', b'b', b'A', b'B', b'0', b'1', b'!', b'@'];
+ // Create a pattern of length 100
+ let pattern: Vec = (0..100)
+ .map(|i| match i % 4 {
+ 0 => PatternChar::Lowercase,
+ 1 => PatternChar::Uppercase,
+ 2 => PatternChar::Numeric,
+ _ => PatternChar::Symbol,
+ })
+ .collect();
+
+ let mut rng = StdRng::seed_from_u64(2002);
+ let password = generate_password_from_pattern(&char_set, &pattern, &mut rng);
+
+ assert_eq!(password.len(), 100);
+ }
+
+ #[test]
+ fn test_print_columns_very_long_passwords() {
+ let passwords = vec!["a".repeat(100), "b".repeat(50), "c".repeat(150)];
+ // Test width calculation with very long passwords
+ print_columns(passwords.clone(), 1, false);
+ print_columns(passwords.clone(), 2, false);
+ print_columns(passwords, 3, true);
+ }
+
+ #[test]
+ fn test_print_columns_single_password_multi_column() {
+ // Test single password with multiple columns (should still print it)
+ let passwords = vec!["single".to_string()];
+ print_columns(passwords, 5, false);
+ }
+
+ #[test]
+ fn test_validate_args_all_types_disabled_lowercase_available() {
+ // Test validate_args when all types are disabled but lowercase available
+ let args = create_test_args(true, true, true, vec![]);
+ let result = validate_args(&args);
+ assert!(result.is_ok()); // Should be valid since lowercase is still available
+ }
}
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 08f2c0e..511ab8c 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -92,3 +92,427 @@ fn test_seed_reproducibility() {
assert_eq!(pass1, pass2);
}
+
+#[test]
+fn test_cli_invalid_exclude_chars() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--exclude-chars", "z-a", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success(), "Should fail with invalid range");
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(
+ stderr.contains("Invalid range") || stderr.contains("Error parsing exclude characters")
+ );
+}
+
+#[test]
+fn test_cli_invalid_include_chars() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--include-chars", "z-a", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success(), "Should fail with invalid range");
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(
+ stderr.contains("Invalid range") || stderr.contains("Error parsing include characters")
+ );
+}
+
+#[test]
+fn test_cli_invalid_pattern() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--pattern", "LLX", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success(), "Should fail with invalid pattern");
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(
+ stderr.contains("Invalid pattern character") || stderr.contains("Error parsing pattern")
+ );
+}
+
+#[test]
+fn test_cli_invalid_length_zero() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--length", "0", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success(), "Should fail with length 0");
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(stderr.contains("Password length must be greater than 0"));
+}
+
+#[test]
+fn test_cli_invalid_length_too_long() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--length", "10001", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(!output.status.success(), "Should fail with length > 10000");
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ assert!(stderr.contains("exceeds maximum of 10,000"));
+}
+
+#[test]
+fn test_cli_json_output() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["2", "--length", "10", "--format", "json", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+
+ // Should be valid JSON
+ let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");
+ assert!(json.get("passwords").is_some());
+ assert!(json.get("count").is_some());
+ assert!(json.get("length").is_some());
+ assert!(json.get("entropy_bits").is_some());
+
+ let passwords = json.get("passwords").unwrap().as_array().unwrap();
+ assert_eq!(passwords.len(), 2);
+ assert_eq!(passwords[0].as_str().unwrap().len(), 10);
+}
+
+#[test]
+fn test_cli_table_output() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["6", "--table", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
+
+ // Should have passwords in table format (may be on multiple lines in columns)
+ // With 6 passwords and 2 columns, we'd expect at least 3 lines
+ assert!(
+ lines.len() >= 3,
+ "Expected at least 3 lines in table format, got {}",
+ lines.len()
+ );
+ // Verify passwords are present by checking non-empty content
+ assert!(!stdout.trim().is_empty(), "Output should contain passwords");
+}
+
+#[test]
+fn test_cli_table_with_header() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["6", "--table"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+
+ // Should contain header when not in quiet mode
+ assert!(stdout.contains("Printing"));
+}
+
+#[test]
+fn test_cli_quiet_mode() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["3", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+
+ // Should not contain banner or header in quiet mode
+ assert!(!stdout.contains("RPG v"));
+ assert!(!stdout.contains("Printing"));
+
+ // Should only have passwords
+ let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
+ assert_eq!(lines.len(), 3);
+}
+
+#[test]
+fn test_cli_pattern_generation() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--pattern", "LLLNNNSSS", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ assert_eq!(password.len(), 9);
+ // Verify pattern was followed (can't predict exact chars but can verify types)
+ let chars: Vec = password.chars().collect();
+ assert!(chars[0].is_ascii_lowercase());
+ assert!(chars[1].is_ascii_lowercase());
+ assert!(chars[2].is_ascii_lowercase());
+ assert!(chars[3].is_ascii_digit());
+ assert!(chars[4].is_ascii_digit());
+ assert!(chars[5].is_ascii_digit());
+ assert!(!chars[6].is_alphanumeric());
+ assert!(!chars[7].is_alphanumeric());
+ assert!(!chars[8].is_alphanumeric());
+}
+
+#[test]
+fn test_cli_pattern_case_insensitive() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--pattern", "lllununss", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ assert_eq!(password.len(), 9);
+}
+
+#[test]
+fn test_cli_minimum_requirements() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&[
+ "1",
+ "--length",
+ "10",
+ "--min-capitals",
+ "2",
+ "--min-numerals",
+ "2",
+ "--min-symbols",
+ "2",
+ "--quiet",
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ assert_eq!(password.len(), 10);
+ let capitals = password.chars().filter(|c| c.is_ascii_uppercase()).count();
+ let numerals = password.chars().filter(|c| c.is_ascii_digit()).count();
+ let symbols = password.chars().filter(|c| !c.is_alphanumeric()).count();
+
+ assert!(capitals >= 2);
+ assert!(numerals >= 2);
+ assert!(symbols >= 2);
+}
+
+#[test]
+fn test_cli_seed_reproducibility_with_options() {
+ let output1 = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&[
+ "1",
+ "--seed",
+ "999",
+ "--length",
+ "20",
+ "--min-capitals",
+ "3",
+ "--quiet",
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ let output2 = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&[
+ "1",
+ "--seed",
+ "999",
+ "--length",
+ "20",
+ "--min-capitals",
+ "3",
+ "--quiet",
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output1.status.success());
+ assert!(output2.status.success());
+
+ let stdout1 = String::from_utf8(output1.stdout).unwrap();
+ let stdout2 = String::from_utf8(output2.stdout).unwrap();
+ let pass1: Vec<&str> = stdout1.lines().filter(|l| !l.is_empty()).collect();
+ let pass2: Vec<&str> = stdout2.lines().filter(|l| !l.is_empty()).collect();
+
+ assert_eq!(pass1, pass2);
+}
+
+#[test]
+fn test_cli_include_chars() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&[
+ "1",
+ "--include-chars",
+ "a,b,c,1,2,3",
+ "--length",
+ "10",
+ "--quiet",
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ assert_eq!(password.len(), 10);
+ // All characters should be from the include set
+ for c in password.chars() {
+ assert!(matches!(c, 'a' | 'b' | 'c' | '1' | '2' | '3'));
+ }
+}
+
+#[test]
+fn test_cli_include_chars_with_exclude() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&[
+ "1",
+ "--include-chars",
+ "a,b,c",
+ "--exclude-chars",
+ "a",
+ "--length",
+ "10",
+ "--quiet",
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ assert_eq!(password.len(), 10);
+ // Should only contain 'b' or 'c'
+ for c in password.chars() {
+ assert!(matches!(c, 'b' | 'c'));
+ }
+}
+
+#[test]
+fn test_cli_include_chars_with_range() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&[
+ "1",
+ "--include-chars",
+ "a-z,0-9",
+ "--length",
+ "10",
+ "--quiet",
+ ])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ assert_eq!(password.len(), 10);
+ // All characters should be lowercase or digit
+ for c in password.chars() {
+ assert!(c.is_ascii_lowercase() || c.is_ascii_digit());
+ }
+}
+
+#[test]
+fn test_cli_character_type_combinations() {
+ // Test with capitals off
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--capitals-off", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ // Should not contain uppercase letters
+ assert!(!password.chars().any(|c| c.is_ascii_uppercase()));
+}
+
+#[test]
+fn test_cli_pattern_overrides_length() {
+ // Pattern length should override --length option
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--pattern", "LLL", "--length", "20", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ // Should be length 3 (pattern length), not 20
+ assert_eq!(password.len(), 3);
+}
+
+#[test]
+fn test_cli_exclude_chars_with_range() {
+ let output = Command::new(env!("CARGO_BIN_EXE_rpg"))
+ .args(&["1", "--exclude-chars", "a-z", "--quiet"])
+ .output()
+ .expect("Failed to execute command");
+
+ assert!(output.status.success(), "Command failed: {:?}", output);
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let password = stdout
+ .lines()
+ .filter(|l| !l.is_empty())
+ .next()
+ .unwrap()
+ .trim();
+
+ // Should not contain lowercase letters
+ assert!(!password.chars().any(|c| c.is_ascii_lowercase()));
+}