diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 531b66b..afe8214 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] env: CARGO_TERM_COLOR: always @@ -20,42 +20,34 @@ jobs: rust: [stable] steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ matrix.rust }} - components: clippy, rustfmt - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v4 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - name: Run tests - run: cargo test --verbose - - - name: Run clippy - run: cargo clippy --all-targets -- -D warnings - - - name: Check formatting - run: cargo fmt -- --check - - - name: Test sample-cli example - run: | - cd examples/sample-cli - cargo test --verbose \ No newline at end of file + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + components: clippy, rustfmt + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - uses: pre-commit/action@v3.0.1 + - run: cargo test --verbose + - run: | + cd examples/sample-cli + cargo test --verbose diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d568e32..ed66cac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: name: Rust formatter - id: clippy name: Rust linter - args: ['--all-targets', '--', '-D', 'warnings'] + args: + ["--fix", "--allow-dirty", "--all-targets", "--", "-D", "warnings"] - id: cargo-check - name: Rust compiler check \ No newline at end of file + name: Rust compiler check diff --git a/fuzz/fuzz_targets/env_substitution.rs b/fuzz/fuzz_targets/env_substitution.rs index 05939f1..026f553 100644 --- a/fuzz/fuzz_targets/env_substitution.rs +++ b/fuzz/fuzz_targets/env_substitution.rs @@ -6,7 +6,7 @@ use testscript_rs::TestEnvironment; fuzz_target!(|data: &[u8]| { // Convert bytes to string, handling invalid UTF-8 gracefully let input = String::from_utf8_lossy(data); - + // Create a test environment to test environment variable substitution if let Ok(mut env) = TestEnvironment::new() { // Add some test environment variables with potentially problematic values @@ -20,40 +20,43 @@ fuzz_target!(|data: &[u8]| { ("NESTED", "${OTHER}"), ("OTHER", "value"), ]; - + for (key, value) in test_vars { env.env_vars.insert(key.to_string(), value.to_string()); } - + // Test environment variable substitution // This should never panic, regardless of input let result = env.substitute_env_vars(&input); - + // Basic sanity check - result should be a valid string let _len = result.len(); - + // Test with some edge cases let edge_cases = vec![ - format!("${{{}}}", input), // ${input} - format!("${}", input), // $input + format!("${{{}}}", input), // ${input} + format!("${}", input), // $input format!("{}${{WORK}}", input), // input${WORK} format!("${{WORK}}{}", input), // ${WORK}input - format!("$${}$$", input), // $$input$$ + format!("$${}$$", input), // $$input$$ ]; - + for edge_case in edge_cases { let _result = env.substitute_env_vars(&edge_case); // Should not panic } - + // Test that substitution is idempotent for non-recursive cases let once = env.substitute_env_vars(&input); let twice = env.substitute_env_vars(&once); - + // For most inputs, applying substitution twice should yield the same result // unless the first substitution introduced new variables to substitute if !once.contains('$') { - assert_eq!(once, twice, "Substitution should be idempotent when no $ remains"); + assert_eq!( + once, twice, + "Substitution should be idempotent when no $ remains" + ); } } -}); \ No newline at end of file +}); diff --git a/fuzz/fuzz_targets/parser.rs b/fuzz/fuzz_targets/parser.rs index 03f20f3..ceecb60 100644 --- a/fuzz/fuzz_targets/parser.rs +++ b/fuzz/fuzz_targets/parser.rs @@ -6,11 +6,11 @@ use testscript_rs::parser; fuzz_target!(|data: &[u8]| { // Convert bytes to string, handling invalid UTF-8 gracefully let input = String::from_utf8_lossy(data); - + // Fuzz the main parser function // The parser should never panic and always return a proper Result let result = parser::parse(&input); - + // We don't care if parsing succeeds or fails, just that it doesn't panic // If it succeeds, verify the result is well-formed if let Ok(ref script) = result { @@ -22,13 +22,13 @@ fuzz_target!(|data: &[u8]| { // Line number should be positive assert!(command.line_num > 0, "Invalid line number"); } - + for file in &script.files { // File name should not be empty if file exists assert!(!file.name.is_empty(), "Empty file name"); } } - + // Test that the parser is deterministic - same input should produce same result let result2 = parser::parse(&input); match (result.is_ok(), result2.is_ok()) { diff --git a/fuzz/fuzz_targets/structured.rs b/fuzz/fuzz_targets/structured.rs index 20c38be..f34b32f 100644 --- a/fuzz/fuzz_targets/structured.rs +++ b/fuzz/fuzz_targets/structured.rs @@ -28,19 +28,19 @@ struct FuzzFile { impl FuzzInput { fn to_txtar(&self) -> String { let mut result = String::new(); - + // Add commands for cmd in &self.commands { // Add condition prefix if let Some(ref condition) = cmd.condition { result.push_str(&format!("[{}] ", condition)); } - + // Add negation if cmd.negated { result.push_str("! "); } - + // Add command name and args result.push_str(&cmd.name); for arg in &cmd.args { @@ -51,15 +51,15 @@ impl FuzzInput { result.push_str(&format!(" {}", arg)); } } - + // Add background indicator if cmd.background { result.push_str(" &"); } - + result.push('\n'); } - + // Add files for file in &self.files { result.push_str(&format!("-- {} --\n", file.name)); @@ -70,7 +70,7 @@ impl FuzzInput { result.push('\n'); } } - + result } } @@ -81,28 +81,31 @@ fuzz_target!(|data: &[u8]| { if let Ok(fuzz_input) = FuzzInput::arbitrary(&mut unstructured) { // Convert to txtar format let txtar_content = fuzz_input.to_txtar(); - + // Test the parser let result = parser::parse(&txtar_content); - + // Verify parser doesn't panic and produces consistent results if let Ok(script) = result { // Validate the parsed structure // Note: parsed commands may be fewer than input due to empty lines, comments, etc. - assert!(script.commands.len() <= fuzz_input.commands.len().max(1000), "Too many commands parsed"); - + assert!( + script.commands.len() <= fuzz_input.commands.len().max(1000), + "Too many commands parsed" + ); + for command in &script.commands { assert!(!command.name.is_empty(), "Empty command name"); assert!(command.line_num > 0, "Invalid line number"); } - + for file in &script.files { assert!(!file.name.is_empty(), "Empty file name"); } } } - + // Also test with raw bytes as backup for edge cases let raw_input = String::from_utf8_lossy(data); let _result = parser::parse(&raw_input); -}); \ No newline at end of file +}); diff --git a/fuzz/fuzz_targets/tokens.rs b/fuzz/fuzz_targets/tokens.rs index 184249a..c0a44ca 100644 --- a/fuzz/fuzz_targets/tokens.rs +++ b/fuzz/fuzz_targets/tokens.rs @@ -5,15 +5,15 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { // Convert bytes to string, handling invalid UTF-8 gracefully let input = String::from_utf8_lossy(data); - + // We need to test the token parsing function, but it's private // So we test it indirectly through the public parser interface // Create minimal txtar content with the fuzzed command line let txtar_content = format!("{}\n", input); - + // Fuzz through the main parser - this will exercise parse_command_tokens let result = testscript_rs::parser::parse(&txtar_content); - + // The parser should never panic // If parsing succeeds, verify basic properties if let Ok(script) = result { @@ -27,7 +27,7 @@ fuzz_target!(|data: &[u8]| { } } } - + // Test with various command prefixes to exercise condition parsing if !input.trim().is_empty() && !input.starts_with('#') { let prefixes = ["", "[unix] ", "[!windows] ", "! "]; @@ -37,4 +37,4 @@ fuzz_target!(|data: &[u8]| { // Should not panic regardless of result } } -}); \ No newline at end of file +});