From 106c3816bb43d08b96c54f8e2a211fbc47411a16 Mon Sep 17 00:00:00 2001 From: Bohdan Zhuravel Date: Thu, 6 Nov 2025 22:26:12 +0200 Subject: [PATCH] Add exclude glob option --- README.md | 14 ++++++ src/main.rs | 18 ++++++-- tests/integration_test.rs | 95 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 777cbf8..4895952 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,20 @@ You'll get: Please be sure to increment `--node-index` arg. +You can exclude specific test files from being split using the `--tests-exclude-glob` option: + +``` +$ split-test --junit-xml-report-dir report --node-index 0 --node-total 2 --tests-glob 'spec/**/*_spec.rb' --tests-exclude-glob 'spec/system/**/*_spec.rb' +``` + +This will include all files matching `spec/**/*_spec.rb` but exclude any files matching `spec/system/**/*_spec.rb`. This is useful when you want to run system specs separately from unit tests, as system specs typically take longer and may require different setup. + +You can also specify multiple exclude patterns by using the `--tests-exclude-glob` option multiple times: + +``` +$ split-test --junit-xml-report-dir report --node-index 0 --node-total 2 --tests-glob 'spec/**/*_spec.rb' --tests-exclude-glob 'spec/system/**/*_spec.rb' --tests-exclude-glob 'spec/integration/**/*_spec.rb' +``` + You can use `--debug` option to make sure how it's grouped: ``` diff --git a/src/main.rs b/src/main.rs index 00cc901..39b465a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,8 @@ struct Opt { #[structopt(long, required = true)] tests_glob: Vec, #[structopt(long)] + tests_exclude_glob: Vec, + #[structopt(long)] node_index: usize, #[structopt(long)] node_total: usize, @@ -63,7 +65,7 @@ impl<'a> Node<'a> { } } -fn expand_globs(patterns: &Vec) -> Result> { +fn expand_globs(patterns: &Vec, exclude_patterns: &Vec) -> Result> { let mut files = HashSet::new(); for pattern in patterns { @@ -72,6 +74,16 @@ fn expand_globs(patterns: &Vec) -> Result> { } } + if !exclude_patterns.is_empty() { + let mut exclude_files = HashSet::new(); + for exclude_pattern in exclude_patterns { + for path in glob(&exclude_pattern)? { + exclude_files.insert(canonicalize(path?)?); + } + } + files = files.difference(&exclude_files).cloned().collect(); + } + let mut files = files.into_iter().collect::>(); files.sort(); @@ -87,7 +99,7 @@ fn get_test_file_results(junit_xml_report_dir: &PathBuf) -> Result Result<()> { let test_file_results = get_test_file_results(&args.junit_xml_report_dir)?; - let test_files = expand_globs(&args.tests_glob)?; + let test_files = expand_globs(&args.tests_glob, &args.tests_exclude_glob)?; if test_files.len() == 0 { bail!("Test file is not found. pattern: {:?}", args.tests_glob); } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index dec8187..98ab589 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -240,3 +240,98 @@ fn test_invalid_report() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_exclude_glob() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("split-test")?; + + // Test excluding specific files - should include a_test.rb and c_test.rb, exclude b_test.rb + cmd.current_dir("tests/fixtures/minitest") + .arg("--junit-xml-report-dir") + .arg("report") + .arg("--node-index") + .arg("0") + .arg("--node-total") + .arg("1") + .arg("--tests-glob") + .arg("*_test.rb") + .arg("--tests-exclude-glob") + .arg("b_test.rb"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("a_test.rb")) + .stdout(predicate::str::contains("b_test.rb").not()) + .stdout(predicate::str::contains("c_test.rb")) + .stderr(predicate::str::is_empty()); + + // Test excluding with wildcard - exclude all files starting with 'a' + cmd = Command::cargo_bin("split-test")?; + + cmd.current_dir("tests/fixtures/minitest") + .arg("--junit-xml-report-dir") + .arg("report") + .arg("--node-index") + .arg("0") + .arg("--node-total") + .arg("1") + .arg("--tests-glob") + .arg("*_test.rb") + .arg("--tests-exclude-glob") + .arg("a*_test.rb"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("a_test.rb").not()) + .stdout(predicate::str::contains("b_test.rb")) + .stdout(predicate::str::contains("c_test.rb")) + .stderr(predicate::str::is_empty()); + + // Test with split across nodes and exclude + cmd = Command::cargo_bin("split-test")?; + + cmd.current_dir("tests/fixtures/minitest") + .arg("--junit-xml-report-dir") + .arg("report") + .arg("--node-index") + .arg("0") + .arg("--node-total") + .arg("2") + .arg("--tests-glob") + .arg("*_test.rb") + .arg("--tests-exclude-glob") + .arg("c_test.rb"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("a_test.rb")) + .stdout(predicate::str::contains("b_test.rb").not()) + .stdout(predicate::str::contains("c_test.rb").not()) + .stderr(predicate::str::is_empty()); + + // Test with multiple exclude patterns - exclude both a_test.rb and c_test.rb + cmd = Command::cargo_bin("split-test")?; + + cmd.current_dir("tests/fixtures/minitest") + .arg("--junit-xml-report-dir") + .arg("report") + .arg("--node-index") + .arg("0") + .arg("--node-total") + .arg("1") + .arg("--tests-glob") + .arg("*_test.rb") + .arg("--tests-exclude-glob") + .arg("a_test.rb") + .arg("--tests-exclude-glob") + .arg("c_test.rb"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("a_test.rb").not()) + .stdout(predicate::str::contains("b_test.rb")) + .stdout(predicate::str::contains("c_test.rb").not()) + .stderr(predicate::str::is_empty()); + + Ok(()) +}