diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index 6598bb2a5..94a1095cb 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -1,3 +1,6 @@ +## Unreleased +- Fix `--exclude` not working. (#2990) + ## 4.4.0 - Fix iOS Simulator test crash on Xcode 26.4+ caused by missing platform frameworks path in xctestrun. @@ -15,6 +18,7 @@ - Bump `patrol_log` to `^0.8.0`. - Refactor develop command into reusable components and expose public API for programmatic usage. - Reflect failed tests in Playwright report. (#2970) +- Fix `--exclude` not working. (#2990) ## 4.2.0 diff --git a/packages/patrol_cli/lib/src/commands/build_android.dart b/packages/patrol_cli/lib/src/commands/build_android.dart index 7dda2e44e..00fdc9976 100644 --- a/packages/patrol_cli/lib/src/commands/build_android.dart +++ b/packages/patrol_cli/lib/src/commands/build_android.dart @@ -93,12 +93,13 @@ class BuildAndroidCommand extends PatrolCommand { } final testFinder = _testFinderFactory.create(testDirectory); + final excludes = stringsArg('exclude').toSet(); final target = stringsArg('target'); final targets = target.isNotEmpty - ? testFinder.findTests(target, testFileSuffix) + ? testFinder.findTests(target, testFileSuffix, excludes) : testFinder.findAllTests( - excludes: stringsArg('exclude').toSet(), + excludes: excludes, testFileSuffix: testFileSuffix, ); diff --git a/packages/patrol_cli/lib/src/commands/build_ios.dart b/packages/patrol_cli/lib/src/commands/build_ios.dart index 03418de93..f8fa7a71d 100644 --- a/packages/patrol_cli/lib/src/commands/build_ios.dart +++ b/packages/patrol_cli/lib/src/commands/build_ios.dart @@ -95,12 +95,13 @@ class BuildIOSCommand extends PatrolCommand { } final testFinder = _testFinderFactory.create(testDirectory); + final excludes = stringsArg('exclude').toSet(); final target = stringsArg('target'); final targets = target.isNotEmpty - ? testFinder.findTests(target, testFileSuffix) + ? testFinder.findTests(target, testFileSuffix, excludes) : testFinder.findAllTests( - excludes: stringsArg('exclude').toSet(), + excludes: excludes, testFileSuffix: testFileSuffix, ); diff --git a/packages/patrol_cli/lib/src/commands/build_macos.dart b/packages/patrol_cli/lib/src/commands/build_macos.dart index f40e7c650..850ce8003 100644 --- a/packages/patrol_cli/lib/src/commands/build_macos.dart +++ b/packages/patrol_cli/lib/src/commands/build_macos.dart @@ -90,12 +90,13 @@ class BuildMacOSCommand extends PatrolCommand { } final testFinder = _testFinderFactory.create(testDirectory); + final excludes = stringsArg('exclude').toSet(); final target = stringsArg('target'); final targets = target.isNotEmpty - ? testFinder.findTests(target, testFileSuffix) + ? testFinder.findTests(target, testFileSuffix, excludes) : testFinder.findAllTests( - excludes: stringsArg('exclude').toSet(), + excludes: excludes, testFileSuffix: testFileSuffix, ); diff --git a/packages/patrol_cli/lib/src/commands/test.dart b/packages/patrol_cli/lib/src/commands/test.dart index fee02e249..0dccad529 100644 --- a/packages/patrol_cli/lib/src/commands/test.dart +++ b/packages/patrol_cli/lib/src/commands/test.dart @@ -106,12 +106,13 @@ class TestCommand extends PatrolCommand { final testFileSuffix = config.testFileSuffix; final testFinder = _testFinderFactory.create(testDirectory); + final excludes = stringsArg('exclude').toSet(); final target = stringsArg('target'); final targets = target.isNotEmpty - ? testFinder.findTests(target, testFileSuffix) + ? testFinder.findTests(target, testFileSuffix, excludes) : testFinder.findAllTests( - excludes: stringsArg('exclude').toSet(), + excludes: excludes, testFileSuffix: testFileSuffix, ); diff --git a/packages/patrol_cli/lib/src/test_finder.dart b/packages/patrol_cli/lib/src/test_finder.dart index 0daef0b3d..61ce21c33 100644 --- a/packages/patrol_cli/lib/src/test_finder.dart +++ b/packages/patrol_cli/lib/src/test_finder.dart @@ -41,8 +41,10 @@ class TestFinder { List findTests( List targets, [ String testFileSuffix = _kDefaultTestFileSuffix, + Set excludes = const {}, ]) { final testFiles = []; + final absoluteExcludes = _toAbsoluteExcludes(excludes); for (final target in targets) { if (target.endsWith(testFileSuffix)) { @@ -50,10 +52,14 @@ class TestFinder { if (!isFile) { throwToolExit('target file $target does not exist'); } - testFiles.add(_fs.file(target).absolute.path); + final absoluteTargetPath = _fs.file(target).absolute.path; + if (!_isExcluded(absoluteTargetPath, absoluteExcludes)) { + testFiles.add(absoluteTargetPath); + } } else if (_fs.isDirectorySync(target)) { final foundTargets = findAllTests( - directory: _fs.directory(target), + directory: _fs.directory(target).absolute, + excludes: excludes, testFileSuffix: testFileSuffix, ); if (foundTargets.isEmpty) { @@ -86,16 +92,7 @@ class TestFinder { throwToolExit("Directory ${directory.path} doesn't exist"); } - final absoluteExcludes = excludes.map((exclude) { - if (_fs.path.isAbsolute(exclude)) { - return exclude; - } - // Resolve relative to rootDir (not the process CWD). - // _rootDir.path is absolute in production (findRootDirectory returns - // an absolute directory), so avoid calling .absolute which would - // re-resolve against CWD. - return _fs.path.join(_rootDir.path, exclude); - }).toSet(); + final absoluteExcludes = _toAbsoluteExcludes(excludes); return directory .listSync(recursive: true, followLinks: false) @@ -110,28 +107,47 @@ class TestFinder { .where((fileSystemEntity) { final filePath = fileSystemEntity.path; - for (final exclude in absoluteExcludes) { - // Check if the file exactly matches an excluded file - if (filePath == exclude) { - return false; - } - - // Check if the file is inside an excluded directory - // Need to add path separator to avoid matching prefixes - // e.g., "patrol_test/permissions" shouldn't match "patrol_test/permissions_other" - final excludeWithSeparator = exclude.endsWith(_fs.path.separator) - ? exclude - : exclude + _fs.path.separator; - if (filePath.startsWith(excludeWithSeparator)) { - return false; - } - } - - return true; + return !_isExcluded(filePath, absoluteExcludes); }) .map((entity) => entity.absolute.path) .toList(); } + + /// Converts `excludes` to absolute paths. + /// + /// Pass file paths or directory paths in `excludes`, either absolute or + /// relative to [_rootDir]. Absolute paths are kept as-is, and relative paths + /// are resolved against [_rootDir] (not the process working directory). + Set _toAbsoluteExcludes(Set excludes) { + return excludes.map((exclude) { + if (_fs.path.isAbsolute(exclude)) { + return exclude; + } + return _fs.path.join(_rootDir.path, exclude); + }).toSet(); + } + + /// Returns `true` when [filePath] should be filtered out by exclusions. + /// + /// A file is excluded if it exactly matches an excluded path, or if it is + /// located under an excluded directory path. + bool _isExcluded(String filePath, Set absoluteExcludes) { + for (final exclude in absoluteExcludes) { + // Check if the file exactly matches an excluded file. + if (filePath == exclude) { + return true; + } + + final excludeWithSeparator = exclude.endsWith(_fs.path.separator) + ? exclude + : exclude + _fs.path.separator; + if (filePath.startsWith(excludeWithSeparator)) { + return true; + } + } + + return false; + } } class TestFinderFactory { diff --git a/packages/patrol_cli/test/general/test_finder_test.dart b/packages/patrol_cli/test/general/test_finder_test.dart index 4abbea898..31805fb91 100644 --- a/packages/patrol_cli/test/general/test_finder_test.dart +++ b/packages/patrol_cli/test/general/test_finder_test.dart @@ -178,6 +178,49 @@ void _test(Platform platform) { ); }); + test('applies excludes when target is a directory', () { + // given + final included = fs.path.join( + 'patrol_test', + 'permissions', + 'other_test.dart', + ); + final excluded = fs.path.join( + 'patrol_test', + 'permissions', + 'test', + 'excluded_test.dart', + ); + fs.file(included).createSync(recursive: true); + fs.file(excluded).createSync(recursive: true); + + // when + final found = testFinder.findTests( + [fs.path.join('patrol_test', 'permissions')], + '_test.dart', + {fs.path.join('patrol_test', 'permissions', 'test', '')}, + ); + + // then + expect(found, equals([fs.path.join(fs.currentDirectory.path, included)])); + }); + + test('applies excludes when target is a file', () { + // given + final targetFile = fs.path.join('patrol_test', 'app_test.dart'); + fs.file(targetFile).createSync(recursive: true); + + // when + final found = testFinder.findTests( + [targetFile], + '_test.dart', + {targetFile}, + ); + + // then + expect(found, isEmpty); + }); + test('finds tests when targets are files and directories', () { // given final testRoot = fs.directory('patrol_test')..createSync();