diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 40b52fd..dd3194c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,14 +9,15 @@ on: jobs: build: uses: Workiva/gha-dart-oss/.github/workflows/build.yaml@v0.1.7 + with: + sdk: stable checks: uses: Workiva/gha-dart-oss/.github/workflows/checks.yaml@v0.1.7 + with: + sdk: stable unit-tests: - strategy: - matrix: - sdk: [2.19.6, stable] uses: Workiva/gha-dart-oss/.github/workflows/test-unit.yaml@v0.1.7 with: - sdk: ${{ matrix.sdk }} \ No newline at end of file + sdk: stable diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f978e2e..34a91e1 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -12,4 +12,6 @@ permissions: jobs: publish: - uses: Workiva/gha-dart-oss/.github/workflows/publish.yaml@v0.1.7 \ No newline at end of file + uses: Workiva/gha-dart-oss/.github/workflows/publish.yaml@v0.1.7 + with: + sdk: stable diff --git a/CHANGELOG.md b/CHANGELOG.md index c065297..1a307d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 5.0.0 + +- **Breaking change**: Requires Dart 3.0 or above +- Added support for Pub Workspaces (monorepos) + # 4.1.1 - Update the output of parse failures to include the path to the file which failed to parse diff --git a/README.md b/README.md index 6f08707..dc4a9e1 100644 --- a/README.md +++ b/README.md @@ -46,5 +46,24 @@ ignore: - analyzer ``` -> Note: Previously this configuration lived in the `pubspec.yaml`, but that +> [!Note] +> Previously this configuration lived in the `pubspec.yaml`, but that > option was deprecated because `pub publish` warns about unrecognized keys. + +## Pub Workspaces (monorepos) + +This package supports [Pub Workspaces](https://dart.dev/tools/pub/workspaces), a collection of packages in one repository. Workspaces allow Pub to share dependencies between your packages. Your top-level package's `pubspec.yaml` should have a `workspace` field that indicates which sub-packages should be included, like this: + +```yaml +workspace: + - pkg1 + - pkg2 +``` + +and your sub-packages should have `resolution: workspace` in their `pubspec.yaml`s. For more information, see the linked documentation. + +**Running `dependency_validator` will always validate the package your terminal is in**. If you run the tool on the top-level workspace package, it will analyze the workspace package _and_ its sub-packages. To just analyze a sub-package, run the tool in its folder, or pass the `-C` argument: + +```bash +$ dart run dependency_validator -C pkg1 +``` diff --git a/bin/dependency_validator.dart b/bin/dependency_validator.dart index bc762f8..f90b97a 100644 --- a/bin/dependency_validator.dart +++ b/bin/dependency_validator.dart @@ -21,6 +21,7 @@ import 'package:logging/logging.dart'; const String helpArg = 'help'; const String verboseArg = 'verbose'; +const String rootDirArg = 'directory'; const String helpMessage = '''Dependency Validator 2.0 is configured statically via the pubspec.yaml example: @@ -45,6 +46,12 @@ final ArgParser argParser = ArgParser() verboseArg, defaultsTo: false, help: 'Display extra information for debugging.', + ) + ..addOption( + rootDirArg, + abbr: "C", + help: 'Validate dependencies in a subdirectory', + defaultsTo: '.', ); void showHelpAndExit({ExitCode exitCode = ExitCode.success}) { @@ -78,5 +85,8 @@ void main(List args) async { Logger.root.level = Level.ALL; } - await run(); + Logger.root.info(''); + final rootDir = argResults.option(rootDirArg) ?? '.'; + final result = await checkPackage(root: rootDir); + exit(result ? 0 : 1); } diff --git a/example/example.md b/example/example.md index 32be703..22ce7db 100644 --- a/example/example.md +++ b/example/example.md @@ -1,4 +1,13 @@ -Either globally activate this package or add it as a dev_dependency. Then run: +Either globally activate this package or add it as a dev_dependency: +```bash +# Install as a dev dependency on the project -- shared with all collaborators +$ dart pub add --dev dependency_validator + +# Install globally on your system -- does not impact the project +$ dart pub global activate dependency_validator +``` + +Then run: ```bash # If installed as a dependency: @@ -8,16 +17,17 @@ $ dart run dependency_validator $ dart pub global run dependency_validator ``` -If needed, configure dependency_validator in your `pubspec.yaml`: +If needed, add a configuration in `dart_dependency_validator.yaml`: ```yaml -# pubsec.yaml -dependency_validator: - # Exclude one or more paths from being scanned. - # Supports glob syntax. - exclude: - - "app/**" - # Ignore one or more packages. - ignore: - - analyzer +# Exclude one or more paths from being scanned. Supports glob syntax. +exclude: + - "app/**" + +# Ignore one or more packages. +ignore: + - analyzer + +# Allow dependencies to be pinned to a specific version instead of a range +allowPins: true ``` diff --git a/lib/src/dependency_validator.dart b/lib/src/dependency_validator.dart index 926fe85..6a669d3 100644 --- a/lib/src/dependency_validator.dart +++ b/lib/src/dependency_validator.dart @@ -16,7 +16,6 @@ import 'dart:io'; import 'package:build_config/build_config.dart'; import 'package:dependency_validator/src/import_export_ast_visitor.dart'; -import 'package:glob/glob.dart'; import 'package:io/ansi.dart'; import 'package:logging/logging.dart'; import 'package:package_config/package_config.dart'; @@ -28,24 +27,22 @@ import 'pubspec_config.dart'; import 'utils.dart'; /// Check for missing, under-promoted, over-promoted, and unused dependencies. -Future run() async { - if (!File('pubspec.yaml').existsSync()) { +Future checkPackage({required String root}) async { + var result = true; + if (!File('$root/pubspec.yaml').existsSync()) { logger.shout(red.wrap('pubspec.yaml not found')); - exit(1); - } - if (!File('.dart_tool/package_config.json').existsSync()) { - logger.shout(red.wrap( - 'No .dart_tool/package_config.json file found, please run "pub get" first.')); - exit(1); + logger.fine('Path: $root/pubspec.yaml'); + return false; } DepValidatorConfig config; - final configFile = File('dart_dependency_validator.yaml'); + final configFile = File('$root/dart_dependency_validator.yaml'); if (configFile.existsSync()) { config = DepValidatorConfig.fromYaml(configFile.readAsStringSync()); } else { final pubspecConfig = PubspecDepValidatorConfig.fromYaml( - File('pubspec.yaml').readAsStringSync()); + File('$root/pubspec.yaml').readAsStringSync(), + ); if (pubspecConfig.isNotEmpty) { logger.warning(yellow.wrap( 'Configuring dependency_validator in pubspec.yaml is deprecated.\n' @@ -53,29 +50,40 @@ Future run() async { } config = pubspecConfig.dependencyValidator; } + final excludes = config.exclude .map((s) { try { - return Glob(s); + return makeGlob("$root/$s"); } catch (_, __) { logger.shout(yellow.wrap('invalid glob syntax: "$s"')); return null; } }) - .where((g) => g != null) - .cast() + .nonNulls .toList(); logger.fine('excludes:\n${bulletItems(excludes.map((g) => g.pattern))}\n'); final ignoredPackages = config.ignore; logger.fine('ignored packages:\n${bulletItems(ignoredPackages)}\n'); // Read and parse the analysis_options.yaml in the current working directory. - final optionsIncludePackage = getAnalysisOptionsIncludePackage(); + final optionsIncludePackage = getAnalysisOptionsIncludePackage(path: root); // Read and parse the pubspec.yaml in the current working directory. - final pubspecFile = File('pubspec.yaml'); - final pubspec = - Pubspec.parse(pubspecFile.readAsStringSync(), sourceUrl: pubspecFile.uri); + final pubspecFile = File('$root/pubspec.yaml'); + final pubspec = Pubspec.parse( + pubspecFile.readAsStringSync(), + sourceUrl: pubspecFile.uri, + ); + + var subResult = true; + if (pubspec.isWorkspaceRoot) { + logger.fine('In a workspace. Recursing through sub-packages...'); + for (final package in pubspec.workspace ?? []) { + subResult &= await checkPackage(root: '$root/$package'); + logger.info(''); + } + } logger.info('Validating dependencies for ${pubspec.name}...'); @@ -92,7 +100,8 @@ Future run() async { logger.fine('dev_dependencies:\n' '${bulletItems(devDeps)}\n'); - final publicDirs = ['bin/', 'lib/']; + final publicDirs = ['$root/bin/', '$root/lib/']; + logger.fine("Excluding: $excludes"); final publicDartFiles = [ for (final dir in publicDirs) ...listDartFilesIn(dir, excludes), ]; @@ -132,14 +141,29 @@ Future run() async { logger.fine('packages used in public facing files:\n' '${bulletItems(packagesUsedInPublicFiles)}\n'); - final publicDirGlobs = [for (final dir in publicDirs) Glob('$dir**')]; + final publicDirGlobs = [ + for (final dir in publicDirs) makeGlob('$dir**'), + ]; + + final subpackageGlobs = [ + for (final subpackage in pubspec.workspace ?? []) + makeGlob('$root/$subpackage**'), + ]; + + logger.fine('subpackage globs: $subpackageGlobs'); - final nonPublicDartFiles = - listDartFilesIn('./', [...excludes, ...publicDirGlobs]); - final nonPublicScssFiles = - listScssFilesIn('./', [...excludes, ...publicDirGlobs]); - final nonPublicLessFiles = - listLessFilesIn('./', [...excludes, ...publicDirGlobs]); + final nonPublicDartFiles = listDartFilesIn( + '$root/', + [...excludes, ...publicDirGlobs, ...subpackageGlobs], + ); + final nonPublicScssFiles = listScssFilesIn( + '$root/', + [...excludes, ...publicDirGlobs, ...subpackageGlobs], + ); + final nonPublicLessFiles = listLessFilesIn( + '$root/', + [...excludes, ...publicDirGlobs, ...subpackageGlobs], + ); logger ..fine('non-public dart files:\n' @@ -193,7 +217,7 @@ Future run() async { 'These packages are used in lib/ but are not dependencies:', missingDependencies, ); - exitCode = 1; + result = false; } // Packages that are used outside lib/ but are not dev_dependencies. @@ -215,7 +239,7 @@ Future run() async { 'These packages are used outside lib/ but are not dev_dependencies:', missingDevDependencies, ); - exitCode = 1; + result = false; } // Packages that are not used in lib/, but are used elsewhere, that are @@ -235,7 +259,7 @@ Future run() async { 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:', overPromotedDependencies, ); - exitCode = 1; + result = false; } // Packages that are used in lib/, but are dev_dependencies. @@ -251,7 +275,7 @@ Future run() async { 'These packages are used in lib/ and should be promoted to actual dependencies:', underPromotedDependencies, ); - exitCode = 1; + result = false; } // Packages that are not used anywhere but are dependencies. @@ -269,8 +293,7 @@ Future run() async { if (packageConfig == null) { logger.severe(red.wrap( 'Could not find package config. Make sure you run `dart pub get` first.')); - exitCode = 1; - return; + return false; } // Remove deps that provide builders that will be applied @@ -285,30 +308,39 @@ Future run() async { .any((key) => key.startsWith('$dependencyName:')); final packagesWithConsumedBuilders = Set(); - for (final package in unusedDependencies.map((name) => packageConfig[name])) { + for (final name in unusedDependencies) { + final package = packageConfig[name]; + if (package == null) continue; // Check if a builder is used from this package - if (rootPackageReferencesDependencyInBuildYaml(package!.name) || + if (rootPackageReferencesDependencyInBuildYaml(package.name) || await dependencyDefinesAutoAppliedBuilder(p.fromUri(package.root))) { packagesWithConsumedBuilders.add(package.name); } } logIntersection( - Level.FINE, - 'The following packages contain builders that are auto-applied or referenced in "build.yaml"', - unusedDependencies, - packagesWithConsumedBuilders); + Level.FINE, + 'The following packages contain builders that are auto-applied or referenced in "build.yaml"', + unusedDependencies, + packagesWithConsumedBuilders, + ); unusedDependencies.removeAll(packagesWithConsumedBuilders); // Remove deps that provide executables, those are assumed to be used - final packagesWithExecutables = unusedDependencies.where((name) { + bool providesExecutable(String name) { final package = packageConfig[name]; - final binDir = Directory(p.join(p.fromUri(package!.root), 'bin')); + if (package == null) return false; + final binDir = Directory(p.join(p.fromUri(package.root), 'bin')); if (!binDir.existsSync()) return false; // Search for executables, if found we assume they are used return binDir.listSync().any((entity) => entity.path.endsWith('.dart')); - }).toSet(); + } + + final packagesWithExecutables = { + for (final package in unusedDependencies) + if (providesExecutable(package)) package, + }; final nonDevPackagesWithExecutables = packagesWithExecutables.where(pubspec.dependencies.containsKey).toSet(); @@ -319,7 +351,7 @@ Future run() async { unusedDependencies, nonDevPackagesWithExecutables, ); - exitCode = 1; + result = false; } logIntersection( @@ -347,12 +379,13 @@ Future run() async { 'These packages may be unused, or you may be using assets from these packages:', unusedDependencies, ); - exitCode = 1; + result = false; } - if (exitCode == 0) { + if (result) { logger.info(green.wrap('✓ No dependency issues found!')); } + return result && subResult; } /// Whether a dependency at [path] defines an auto applied builder. diff --git a/lib/src/pubspec_config.dart b/lib/src/pubspec_config.dart index 3b0aad1..f89ead5 100644 --- a/lib/src/pubspec_config.dart +++ b/lib/src/pubspec_config.dart @@ -30,7 +30,7 @@ class PubspecDepValidatorConfig { @JsonSerializable( anyMap: true, checked: true, - createToJson: false, + createToJson: true, fieldRename: FieldRename.snake) class DepValidatorConfig { @JsonKey(defaultValue: []) @@ -55,4 +55,6 @@ class DepValidatorConfig { checkedYamlDecode( yamlContent, (m) => DepValidatorConfig.fromJson(m ?? {}), allowNull: true, sourceUrl: sourceUrl); + + Map toJson() => _$DepValidatorConfigToJson(this); } diff --git a/lib/src/pubspec_config.g.dart b/lib/src/pubspec_config.g.dart index d2b7949..3ea887d 100644 --- a/lib/src/pubspec_config.g.dart +++ b/lib/src/pubspec_config.g.dart @@ -41,3 +41,10 @@ DepValidatorConfig _$DepValidatorConfigFromJson(Map json) => $checkedCreate( }, fieldKeyMap: const {'allowPins': 'allow_pins'}, ); + +Map _$DepValidatorConfigToJson(DepValidatorConfig instance) => + { + 'exclude': instance.exclude, + 'ignore': instance.ignore, + 'allow_pins': instance.allowPins, + }; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index b83ee4a..6610243 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -93,7 +93,7 @@ Iterable listFilesWithExtensionIn( /// Logs the given [message] at [level] and lists all of the given [dependencies]. void log(Level level, String message, Iterable dependencies) { final sortedDependencies = dependencies.toList()..sort(); - var combined = [message, bulletItems(sortedDependencies), ''].join('\n'); + var combined = [message, bulletItems(sortedDependencies)].join('\n'); if (level >= Level.SEVERE) { combined = red.wrap(combined)!; } else if (level >= Level.WARNING) { @@ -183,3 +183,19 @@ DependencyPinEvaluation inspectVersionForPins(VersionConstraint constraint) { return DependencyPinEvaluation.emptyPin; } + +/// Utilities for Pubspec objects. +extension PubspecUtils on Pubspec { + /// Whether this package is the root of a Pub Workspace. + bool get isWorkspaceRoot => workspace != null; + + /// Whether this package is a sub-package in a Pub Workspace. + bool get isInWorkspace => resolution == 'workspace'; +} + +/// Makes a glob object for the given path. +/// +/// This function removes `./` paths and replaces all `\` with `/`. +Glob makeGlob(String path) => Glob( + p.posix.normalize(path.replaceAll(r'\', '/')), + ); diff --git a/pubspec.yaml b/pubspec.yaml index 9d6916e..bf815de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: dependency_validator -version: 4.1.2 +version: 5.0.0 description: Checks for missing, under-promoted, over-promoted, and unused dependencies. homepage: https://github.com/Workiva/dependency_validator environment: - sdk: '>=2.19.0 <3.0.0' + sdk: '^3.0.0' dependencies: analyzer: '>=5.4.0 <7.0.0' @@ -18,7 +18,7 @@ dependencies: package_config: ^2.0.0 path: ^1.8.0 pub_semver: ^2.0.0 - pubspec_parse: ^1.0.0 + pubspec_parse: ^1.5.0 yaml: ^3.1.0 dev_dependencies: diff --git a/test/executable_test.dart b/test/executable_test.dart index e9f8093..22c0fca 100644 --- a/test/executable_test.dart +++ b/test/executable_test.dart @@ -15,443 +15,257 @@ @TestOn('vm') import 'dart:io'; +import 'package:dependency_validator/src/pubspec_config.dart'; import 'package:io/io.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; -/// `master` on build_config has a min sdk bound of dart 3.0.0. -/// Since dependency_validator is still designed to be used on dart 2 -/// code, we still want to run unit tests using this older version -/// -/// The following ref, is the last commit in build_config that allowed -/// dart 2 as a dependency -const buildConfigRef = 'e2c837b48bd3c4428cb40e2bc1a6cf47d45df8cc'; - -ProcessResult checkProject(String projectPath, - {List optionalArgs = const []}) { - final pubGetResult = - Process.runSync('dart', ['pub', 'get'], workingDirectory: projectPath); - if (pubGetResult.exitCode != 0) { - return pubGetResult; - } - - final args = [ - 'run', - 'dependency_validator', - // This makes it easier to print(result.stdout) for debugging tests - '--verbose', - ...optionalArgs, - ]; - - return Process.runSync('dart', args, workingDirectory: projectPath); -} - -/// Removes indentation from `'''` string blocks. -String unindent(String multilineString) { - var indent = RegExp(r'^( *)').firstMatch(multilineString)![1]; - assert(indent != null && indent.isNotEmpty); - return multilineString.replaceAll('$indent', ''); -} +import 'utils.dart'; void main() { group('dependency_validator', () { late ProcessResult result; - setUp(() async { - // Create fake project that any test may use - final fakeProjectPubspec = unindent(''' - name: fake_project - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - final fakeProjectBuild = unindent(''' - builders: - someBuilder: - import: "package:fake_project/builder.dart" - builder_factories: ["someBuilder"] - build_extensions: {".dart" : [".woot.g.dart"]} - auto_apply: none - build_to: cache - '''); - - await d.dir('fake_project', [ - d.dir('lib', [ - d.file('fake.dart', 'bool fake = true;'), - d.file('builder.dart', 'bool fake = true;'), - ]), - d.file('pubspec.yaml', fakeProjectPubspec), - d.file('build.yaml', fakeProjectBuild), - ]).create(); - }); - tearDown(() { printOnFailure(result.stdout); printOnFailure(result.stderr); }); test('fails with incorrect usage', () async { - final pubspec = unindent(''' - name: common_binaries - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - '''); - - await d.dir('common_binaries', [ - d.dir('lib', [ - d.file('fake.dart', 'bool fake = true;'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - - result = checkProject('${d.sandbox}/common_binaries', - optionalArgs: ['-x', 'tool/wdesk_sdk']); - + result = await checkProject( + args: ['-x', 'tool/wdesk_sdk'], + ); expect(result.exitCode, ExitCode.usage.code); }); group('fails when there are packages missing from the pubspec', () { - setUp(() async { - final pubspec = unindent(''' - name: missing - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('missing', [ - d.dir('lib', [ - d.file('missing.dart', 'import \'package:yaml/yaml.dart\';'), - d.file('missing.scss', '@import \'package:somescsspackage/foo\';'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - }); - - test('', () { - result = checkProject('${d.sandbox}/missing'); + final project = [ + d.dir('lib', [ + d.file('missing.dart', 'import "package:yaml/yaml.dart";'), + d.file('missing.scss', '@import "package:some_scss_package/foo";'), + ]), + ]; - expect(result.exitCode, equals(1)); + test('', () async { + result = await checkProject(project: project); + expect(result.exitCode, 1); expect( - result.stderr, - contains( - 'These packages are used in lib/ but are not dependencies:')); + result.stderr, + contains('These packages are used in lib/ but are not dependencies:'), + ); expect(result.stderr, contains('yaml')); - expect(result.stderr, contains('somescsspackage')); + expect(result.stderr, contains('some_scss_package')); }); - test('except when the lib directory is excluded', () async { - await d.dir('missing', [ - d.file('dart_dependency_validator.yaml', unindent(''' - exclude: - - 'lib/**' - ''')) - ]).create(); - result = checkProject('${d.sandbox}/missing'); - expect(result.exitCode, equals(0)); + final excludeLib = DepValidatorConfig(exclude: ['lib/**']); + final ignorePackages = DepValidatorConfig( + ignore: ['yaml', 'some_scss_package'], + ); + + test('except when lib is excluded', () async { + result = await checkProject( + project: project, + config: excludeLib, + ); + expect(result.exitCode, 0); expect(result.stderr, isEmpty); }); - test( - 'except when the lib directory is excluded (deprecated pubspec method)', - () { - final dependencyValidatorSection = unindent(''' - dependency_validator: - exclude: - - 'lib/**' - '''); - - File('${d.sandbox}/missing/pubspec.yaml').writeAsStringSync( - dependencyValidatorSection, - mode: FileMode.append, - flush: true); - - result = checkProject('${d.sandbox}/missing'); + test('except when lib is excluded (deprecated pubspec method)', () async { + result = await checkProject( + project: project, + config: excludeLib, + embedConfigInPubspec: true, + ); expect(result.exitCode, equals(0)); expect( - result.stderr, - contains( - 'Configuring dependency_validator in pubspec.yaml is deprecated')); + result.stderr, + contains( + 'Configuring dependency_validator in pubspec.yaml is deprecated', + ), + ); }); test('except when they are ignored', () async { - await d.dir('missing', [ - d.file('dart_dependency_validator.yaml', unindent(''' - ignore: - - yaml - - somescsspackage - ''')) - ]).create(); - result = checkProject('${d.sandbox}/missing'); + result = await checkProject( + project: project, + config: ignorePackages, + ); expect(result.exitCode, 0); }); - test('except when they are ignored (deprecated pubspec method)', () { - final dependencyValidatorSection = unindent(''' - dependency_validator: - ignore: - - yaml - - somescsspackage - '''); - - File('${d.sandbox}/missing/pubspec.yaml').writeAsStringSync( - dependencyValidatorSection, - mode: FileMode.append); - - result = checkProject('${d.sandbox}/missing'); + test('except when they are ignored (deprecated pubspec method)', + () async { + result = await checkProject( + project: project, + config: ignorePackages, + embedConfigInPubspec: true, + ); expect(result.exitCode, 0); }); }); group('fails when there are over promoted packages', () { - setUp(() async { - final pubspec = unindent(''' - name: over_promoted - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dependencies: - path: any - yaml: any - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('over_promoted', [ - d.dir('web', [ - d.file('main.dart', 'import \'package:path/path.dart\';'), - d.file('over_promoted.scss', '@import \'package:yaml/foo\';'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - }); - - test('', () { - result = checkProject('${d.sandbox}/over_promoted'); + final project = [ + d.dir('web', [ + d.file('main.dart', 'import "package:path/path.dart";'), + d.file('over_promoted.scss', '@import "package:yaml/foo";'), + ]), + ]; + final dependencies = { + "path": hostedAny, + "yaml": hostedAny, + }; + final config = DepValidatorConfig(ignore: ['path', 'yaml']); + + test('', () async { + result = await checkProject( + project: project, + dependencies: dependencies, + ); expect(result.exitCode, 1); expect( - result.stderr, - contains( - 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:')); + result.stderr, + contains( + 'These packages are only used outside lib/ and should be downgraded to dev_dependencies:', + ), + ); expect(result.stderr, contains('path')); expect(result.stderr, contains('yaml')); }); test('except when they are ignored', () async { - await d.dir('over_promoted', [ - d.file('dart_dependency_validator.yaml', unindent(''' - ignore: - - path - - yaml - ''')) - ]).create(); - result = checkProject('${d.sandbox}/over_promoted'); + result = await checkProject( + project: project, + dependencies: dependencies, + config: config, + ); expect(result.exitCode, 0); }); - test('except when they are ignored (deprecated pubspec method)', () { - final dependencyValidatorSection = unindent(''' - dependency_validator: - ignore: - - path - - yaml - '''); - - File('${d.sandbox}/over_promoted/pubspec.yaml').writeAsStringSync( - dependencyValidatorSection, - mode: FileMode.append); - - result = checkProject('${d.sandbox}/over_promoted'); + test('except when they are ignored (deprecated pubspec method)', + () async { + result = await checkProject( + project: project, + dependencies: dependencies, + config: config, + embedConfigInPubspec: true, + ); expect(result.exitCode, 0); }); }); group('fails when there are under promoted packages', () { - setUp(() async { - final pubspec = unindent(''' - name: under_promoted - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - logging: any - yaml: any - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('under_promoted', [ - d.dir('lib', [ - d.file('under_promoted.dart', - 'import \'package:logging/logging.dart\';'), - d.file('under_promoted.scss', '@import \'package:yaml/foo\';'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - }); + final devDependencies = { + "logging": hostedAny, + "yaml": hostedAny, + }; + + final project = [ + d.dir('lib', [ + d.file('main.dart', 'import "package:logging/logging.dart";'), + d.file('main.scss', '@import "package:yaml/foo";'), + ]), + ]; - test('', () { - result = checkProject('${d.sandbox}/under_promoted'); + final config = DepValidatorConfig(ignore: ['logging', 'yaml']); + test('', () async { + result = await checkProject( + project: project, + devDependencies: devDependencies, + ); expect(result.exitCode, 1); expect( - result.stderr, - contains( - 'These packages are used in lib/ and should be promoted to actual dependencies:')); + result.stderr, + contains( + 'These packages are used in lib/ and should be promoted to actual dependencies:', + ), + ); expect(result.stderr, contains('logging')); expect(result.stderr, contains('yaml')); }); test('except when they are ignored', () async { - await d.dir('under_promoted', [ - d.file('dart_dependency_validator.yaml', unindent(''' - ignore: - - logging - - yaml - ''')) - ]).create(); - result = checkProject('${d.sandbox}/under_promoted'); + result = await checkProject( + project: project, + devDependencies: devDependencies, + config: config, + ); expect(result.exitCode, 0); }); - test('except when they are ignored (deprecated pubspec method)', () { - final dependencyValidatorSection = unindent(''' - dependency_validator: - ignore: - - logging - - yaml - '''); - - File('${d.sandbox}/under_promoted/pubspec.yaml').writeAsStringSync( - dependencyValidatorSection, - mode: FileMode.append); - - result = checkProject('${d.sandbox}/under_promoted'); + test('except when they are ignored (deprecated pubspec method)', + () async { + result = await checkProject( + project: project, + devDependencies: devDependencies, + config: config, + embedConfigInPubspec: true, + ); expect(result.exitCode, 0); }); }); group('fails when there are unused packages', () { - setUp(() async { - final unusedPubspec = unindent(''' - name: unused - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - fake_project: - path: ${d.sandbox}/fake_project - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('unused', [ - d.file('pubspec.yaml', unusedPubspec), - ]).create(); - }); + final devDependencies = { + 'yaml': hostedAny, + }; + + final config = DepValidatorConfig(ignore: ['yaml']); - test('', () { - result = checkProject('${d.sandbox}/unused'); + test('', () async { + result = await checkProject( + devDependencies: devDependencies, + ); expect(result.exitCode, 1); expect( - result.stderr, - contains( - 'These packages may be unused, or you may be using assets from these packages:')); - expect(result.stderr, contains('fake_project')); + result.stderr, + contains( + 'These packages may be unused, or you may be using assets from these packages:', + ), + ); + expect(result.stderr, contains('yaml')); }); test('and import is commented out', () async { - await d.dir('unused', [ - d.dir('lib', [ - d.file('commented_out.dart', - '// import \'package:other_project/other.dart\';'), // commented out import - ]), - d.dir('test', [ - d.file('valid.dart', 'import \'package:fake_project/fake.dart\';'), - ]) - ]).create(); - result = checkProject('${d.sandbox}/unused'); + result = await checkProject( + devDependencies: devDependencies, + project: [ + d.dir('lib', [ + d.file( + 'main.dart', + '// import "package:other_project/other.dart";', + ), + ]), + d.dir('test', [ + d.file('valid.dart', 'import "package:yaml/fake.dart";'), + ]) + ], + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); test('except when they are ignored', () async { - await d.dir('unused', [ - d.file('dart_dependency_validator.yaml', unindent(''' - ignore: - - fake_project - - yaml - ''')) - ]).create(); - result = checkProject('${d.sandbox}/unused'); + result = await checkProject( + devDependencies: devDependencies, + config: config, + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); - test('except when they are ignored (deprecated pubspec method)', () { - final dependencyValidatorSection = unindent(''' - dependency_validator: - ignore: - - fake_project - - yaml - '''); - - File('${d.sandbox}/unused/pubspec.yaml').writeAsStringSync( - dependencyValidatorSection, - mode: FileMode.append); - - result = checkProject('${d.sandbox}/unused'); + test('except when they are ignored (deprecated pubspec method)', + () async { + result = await checkProject( + devDependencies: devDependencies, + config: config, + embedConfigInPubspec: true, + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); @@ -459,119 +273,68 @@ void main() { test('warns when the analyzer package is depended on but not used', () async { - final pubspec = unindent(''' - name: analyzer_dep - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dependencies: - analyzer: any - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('project', [ - d.dir('lib', [ - d.file('analyzer_dep.dart', ''), - ]), - d.file('dart_dependency_validator.yaml', unindent(''' - ignore: - - analyzer - ''')), - d.file('pubspec.yaml', pubspec), - ]).create(); - - result = checkProject('${d.sandbox}/project'); - + result = await checkProject( + dependencies: { + "analyzer": hostedAny, + }, + project: [ + d.dir('lib', [ + d.file('main.dart', ''), + ]), + ], + config: DepValidatorConfig(ignore: ['analyzer']), + ); expect(result.exitCode, 0); expect( - result.stderr, - contains( - 'You do not need to depend on `analyzer` to run the Dart analyzer.')); + result.stderr, + contains( + 'You do not need to depend on `analyzer` to run the Dart analyzer.', + ), + ); }); test('passes when all dependencies are used and valid', () async { - final pubspec = unindent(''' - name: valid - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dependencies: - logging: any - yaml: any - fake_project: - path: ${d.sandbox}/fake_project - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - pedantic: any - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - final validDotDart = '' - 'import \'package:logging/logging.dart\';' - 'import \'package:fake_project/fake.dart\';' - '// import \'package:does_not_exist/fake.dart\''; // commented out and unused - - await d.dir('valid', [ - d.dir('lib', [ - d.file('valid.dart', validDotDart), - d.file('valid.scss', '@import \'package:yaml/foo\';'), - ]), - d.file('pubspec.yaml', pubspec), - d.file('analysis_options.yaml', - 'include: package:pedantic/analysis_options.1.8.0.yaml'), - ]).create(); - - result = checkProject('${d.sandbox}/valid'); + result = await checkProject( + dependencies: { + "logging": hostedAny, + "yaml": hostedAny, + }, + devDependencies: { + "pedantic": hostedAny, + }, + project: [ + d.dir('lib', [ + d.file('main.dart', unindent(''' + import 'package:logging/logging.dart'; + import 'package:yaml/yaml.dart'; + // import 'package:does_not_exist/fake.dart'; + ''')), + d.file('main.scss', '@import "package:yaml/foo";'), + ]), + d.file( + 'analysis_options.yaml', + 'include: package:pedantic/analysis_options.1.8.0.yaml', + ), + ], + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); test('passes when dependencies not used provide executables', () async { - final pubspec = unindent(''' - name: common_binaries - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - build_runner: ^2.3.3 - coverage: any - dart_style: ^2.3.2 - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('common_binaries', [ - d.dir('lib', [ - d.file('fake.dart', 'bool fake = true;'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - - result = checkProject('${d.sandbox}/common_binaries'); + result = await checkProject( + devDependencies: { + "build_runner": hostedCompatibleWith('2.3.3'), + 'coverage': hostedAny, + 'dart_style': hostedCompatibleWith('2.3.2'), + }, + project: [ + d.dir('lib', [ + d.file('main.dart', 'book fake = true;'), + ]), + ], + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); @@ -580,73 +343,43 @@ void main() { test( 'fails when dependencies not used provide executables, but are not dev_dependencies', () async { - final pubspec = unindent(''' - name: common_binaries - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dependencies: - build_runner: ^2.3.3 - coverage: any - dart_style: ^2.3.2 - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('common_binaries', [ - d.dir('lib', [ - d.file('fake.dart', 'bool fake = true;'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - - result = checkProject('${d.sandbox}/common_binaries'); + result = await checkProject( + dependencies: { + "build_runner": hostedCompatibleWith('2.3.3'), + "coverage": hostedAny, + "dart_style": hostedCompatibleWith('2.3.2'), + }, + project: [ + d.dir('lib', [ + d.file('main.dart', 'bool fake = true;'), + ]), + ], + ); expect(result.exitCode, 1); expect( - result.stderr, - contains( - 'The following packages contain executables, and are only used outside of lib/. These should be downgraded to dev_dependencies')); + result.stderr, + contains( + 'The following packages contain executables, and are only used outside of lib/. These should be downgraded to dev_dependencies', + ), + ); }); test( 'passes when dependencies are not imported but provide auto applied builders', () async { - final pubspec = unindent(''' - name: common_binaries - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - build_test: ^2.0.1 - build_vm_compilers: ^1.0.3 - build_web_compilers: ^3.2.7 - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('common_binaries', [ - d.dir('lib', [ - d.file('fake.dart', 'bool fake = true;'), - ]), - d.file('pubspec.yaml', pubspec), - ]).create(); - - result = checkProject('${d.sandbox}/common_binaries'); + result = await checkProject( + devDependencies: { + 'build_test': hostedCompatibleWith('2.0.1'), + 'build_vm_compilers': hostedCompatibleWith('1.0.3'), + 'build_web_compilers': hostedCompatibleWith('3.2.7'), + }, + project: [ + d.dir('lib', [ + d.file('main.dart', 'book fake = true;'), + ]), + ], + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); @@ -654,108 +387,68 @@ void main() { test('passes when dependencies are not imported but provide used builders', () async { - final pubspec = unindent(''' - name: common_binaries - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dev_dependencies: - fake_project: - path: ${d.sandbox}/fake_project - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - final build = unindent(r''' - targets: - $default: - builders: - fake_project|someBuilder: - options: {} - '''); - - await d.dir('common_binaries', [ - d.dir('lib', [ - d.file('nope.dart', 'bool nope = true;'), - ]), - d.file('pubspec.yaml', pubspec), - d.file('build.yaml', build), - ]).create(); - - result = checkProject('${d.sandbox}/common_binaries'); + result = await checkProject( + devDependencies: { + 'yaml': hostedAny, + }, + project: [ + d.dir('lib', [ + d.file('main.dart', 'bool fake = true;'), + ]), + d.file( + 'build.yaml', + unindent(r''' + targets: + $default: + builders: + yaml|someBuilder: + options: {} + '''), + ), + ], + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); group('when a dependency is pinned', () { - setUp(() async { - final pubspec = unindent(''' - name: dependency_pins - version: 0.0.0 - private: true - environment: - sdk: '>=2.12.0 <4.0.0' - dependencies: - logging: 1.0.2 - dev_dependencies: - dependency_validator: - path: ${Directory.current.path} - dependency_overrides: - build_config: - git: - url: https://github.com/dart-lang/build.git - path: build_config - ref: $buildConfigRef - '''); - - await d.dir('dependency_pins', [ - d.file('pubspec.yaml', pubspec), - ]).create(); - }); + final dependencies = { + 'logging': hostedPinned('1.0.2'), + }; + + final allowPins = DepValidatorConfig(allowPins: true); + final ignorePackage = DepValidatorConfig(ignore: ['logging']); - test('fails if pins are not ignored', () { - result = checkProject('${d.sandbox}/dependency_pins'); + test('fails if pins are not ignored', () async { + result = await checkProject(dependencies: dependencies); expect(result.exitCode, 1); expect( - result.stderr, - contains( - 'These packages are pinned in pubspec.yaml:\n * logging')); + result.stderr, + contains('These packages are pinned in pubspec.yaml:\n * logging'), + ); }); test('should not fails if package is pinned but pins allowed', () async { - await d.dir('dependency_pins', [ - d.dir('lib', [ - d.file('test.dart', unindent(''' - import 'package:logging/logging.dart'; - final log = Logger('ExampleLogger'); - ''')), - ]), - d.file('dart_dependency_validator.yaml', unindent(''' - allow_pins: true - ''')) - ]).create(); - result = checkProject('${d.sandbox}/dependency_pins'); + result = await checkProject( + dependencies: dependencies, + config: allowPins, + project: [ + d.dir('lib', [ + d.file('main.dart', 'import "package:logging/logging.dart";'), + ]), + ], + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); test('ignores infractions if the package is ignored', () async { - await d.dir('dependency_pins', [ - d.file('dart_dependency_validator.yaml', unindent(''' - ignore: - - logging - ''')) - ]).create(); - result = checkProject('${d.sandbox}/dependency_pins'); + result = await checkProject( + dependencies: dependencies, + config: ignorePackage, + ); expect(result.exitCode, 0); expect(result.stdout, contains('No dependency issues found!')); }); diff --git a/test/pubspec_to_json.dart b/test/pubspec_to_json.dart new file mode 100644 index 0000000..3fdb7df --- /dev/null +++ b/test/pubspec_to_json.dart @@ -0,0 +1,58 @@ +import "package:pubspec_parse/pubspec_parse.dart"; + +extension on Map { + Iterable<(K, V)> get records sync* { + for (final entry in entries) { + yield (entry.key, entry.value); + } + } +} + +typedef Json = Map; + +extension on Dependency { + Json toJson() => switch (this) { + SdkDependency(:final sdk, :final version) => { + "sdk": sdk, + "version": version.toString(), + }, + HostedDependency(:final hosted, :final version) => { + if (hosted != null) "hosted": hosted.url.toString(), + "version": version.toString(), + }, + GitDependency(:final url, :final ref, :final path) => { + "git": { + "url": url.toString(), + if (path != null) "ref": ref, + if (path != null) "path": path, + }, + }, + PathDependency(:final path) => { + "path": path.replaceAll(r'\', '/'), + }, + }; +} + +/// An as-needed implementation of `Pubspec.toJson` for testing. +/// +/// See: https://github.com/dart-lang/tools/issues/1801 +extension PubspecToJson on Pubspec { + Json toJson() => { + "name": name, + "environment": { + for (final (sdk, version) in environment.records) + sdk: version.toString(), + }, + if (resolution != null) "resolution": resolution, + if (workspace != null) "workspace": workspace, + "dependencies": { + for (final (name, dependency) in dependencies.records) + name: dependency.toJson(), + }, + "dev_dependencies": { + for (final (name, dependency) in devDependencies.records) + name: dependency.toJson(), + } + // ... + }; +} diff --git a/test/utils.dart b/test/utils.dart new file mode 100644 index 0000000..7e287de --- /dev/null +++ b/test/utils.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dependency_validator/src/dependency_validator.dart'; +import 'package:dependency_validator/src/pubspec_config.dart'; +import 'package:logging/logging.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +export 'package:logging/logging.dart' show Level; + +import 'pubspec_to_json.dart'; + +Future checkProject({ + DepValidatorConfig? config, + Map dependencies = const {}, + Map devDependencies = const {}, + List project = const [], + List args = const [], + bool embedConfigInPubspec = false, +}) async { + final pubspec = Pubspec( + 'project', + environment: requireDart36, + dependencies: dependencies, + devDependencies: { + ...devDependencies, + 'dependency_validator': PathDependency(Directory.current.absolute.path), + }, + ); + final pubspecJson = pubspec.toJson(); + if (embedConfigInPubspec && config != null) { + pubspecJson['dependency_validator'] = config.toJson(); + } + final dir = d.dir('project', [ + ...project, + d.file('pubspec.yaml', jsonEncode(pubspecJson)), + if (config != null && !embedConfigInPubspec) + d.file('dart_dependency_validator.yaml', jsonEncode(config.toJson())), + ]); + await dir.create(); + final path = '${d.sandbox}/project'; + final commandArgs = ['run', 'dependency_validator', '--verbose', ...args]; + return await Process.run('dart', commandArgs, workingDirectory: path); +} + +Dependency hostedCompatibleWith(String version) => HostedDependency( + version: VersionConstraint.compatibleWith(Version.parse(version)), + ); + +Dependency hostedPinned(String version) => HostedDependency( + version: Version.parse(version), + ); + +final hostedAny = HostedDependency(version: VersionConstraint.any); + +/// Removes indentation from `'''` string blocks. +String unindent(String multilineString) { + var indent = RegExp(r'^( *)').firstMatch(multilineString)![1]; + assert(indent != null && indent.isNotEmpty); + return multilineString.replaceAll('$indent', ''); +} + +void initLogs() => + Logger.root.onRecord.map((record) => record.message).listen(print); + +final requireDart36 = { + "sdk": VersionConstraint.compatibleWith(Version.parse('3.6.0')), +}; + +Future checkWorkspace({ + required Map workspaceDeps, + required Map subpackageDeps, + required List workspace, + required List subpackage, + DepValidatorConfig? workspaceConfig, + DepValidatorConfig? subpackageConfig, + Level logLevel = Level.OFF, + Matcher matcher = isTrue, +}) async { + final workspacePubspec = Pubspec( + 'workspace', + environment: requireDart36, + dependencies: workspaceDeps, + workspace: ['subpackage'], + ); + final subpackagePubspec = Pubspec( + 'subpackage', + environment: requireDart36, + dependencies: subpackageDeps, + resolution: 'workspace', + ); + final dir = d.dir( + 'workspace', + [ + ...workspace, + d.file('pubspec.yaml', jsonEncode(workspacePubspec.toJson())), + if (workspaceConfig != null) + d.file( + 'dart_dependency_validator.yaml', + jsonEncode(workspaceConfig.toJson()), + ), + d.dir('subpackage', [ + ...subpackage, + d.file('pubspec.yaml', jsonEncode(subpackagePubspec.toJson())), + if (subpackageConfig != null) + d.file( + 'dart_dependency_validator.yaml', + jsonEncode(subpackageConfig.toJson()), + ), + ]), + ], + ); + await dir.create(); + Logger.root.level = logLevel; + final result = await checkPackage(root: '${d.sandbox}/workspace'); + expect(result, matcher); +} diff --git a/test/workspace_test.dart b/test/workspace_test.dart new file mode 100644 index 0000000..69acd34 --- /dev/null +++ b/test/workspace_test.dart @@ -0,0 +1,149 @@ +import 'package:dependency_validator/src/pubspec_config.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import 'utils.dart'; + +final usesHttp = [ + d.dir('lib', [ + d.file('main.dart', 'import "package:http/http.dart";'), + ]), +]; + +final dependsOnHttp = { + 'http': HostedDependency( + version: VersionConstraint.any, + ), +}; + +final usesMeta = [ + d.dir('lib', [ + d.file('main.dart', 'import "package:meta/meta.dart";'), + ]), +]; + +final dependsOnMeta = { + "meta": HostedDependency(version: VersionConstraint.any), +}; + +final excludeMain = DepValidatorConfig( + exclude: ['lib/main.dart'], +); + +void main() => group('Workspaces', () { + initLogs(); + test( + 'works in the trivial case', + () => checkWorkspace( + workspaceDeps: {}, + workspace: [], + subpackage: [], + subpackageDeps: {}, + )); + + test( + 'works in a basic case', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: usesHttp, + subpackageDeps: dependsOnHttp, + )); + + test( + 'works when the packages have different dependencies', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: usesMeta, + subpackageDeps: dependsOnMeta, + )); + + group('fails when the root has an issue', () { + test( + '(sub-package is okay)', + () => checkWorkspace( + workspace: [], + workspaceDeps: {}, + subpackage: usesHttp, + subpackageDeps: dependsOnHttp, + )); + + test( + 'even when it shares a dependency with the subpackage', + () => checkWorkspace( + workspaceDeps: dependsOnHttp, + workspace: [], + subpackageDeps: dependsOnHttp, + subpackage: usesHttp, + matcher: isFalse, + )); + }); + + group('fails when the subpackage has an issue', () { + test( + '(root is okay)', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: [], + subpackageDeps: {}, + )); + + test( + 'even when it shares a dependency with the subpackage', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: dependsOnHttp, + subpackage: usesHttp, + subpackageDeps: {}, + matcher: isFalse, + )); + }); + + group('handles configs', () { + test( + 'at the root', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: {}, + workspaceConfig: excludeMain, + subpackage: [], + subpackageDeps: {}, + )); + + test( + 'and fails at root when config is in subpackage', + () => checkWorkspace( + workspace: usesHttp, + workspaceDeps: {}, + subpackage: [], + subpackageDeps: {}, + subpackageConfig: excludeMain, + matcher: isFalse, + )); + + test( + 'in a subpackage', + () => checkWorkspace( + workspace: [], + workspaceDeps: {}, + subpackage: usesHttp, + subpackageDeps: {}, + subpackageConfig: excludeMain, + )); + + test( + 'and fails in subpackage when config is in root', + () => checkWorkspace( + workspace: [], + workspaceDeps: {}, + workspaceConfig: excludeMain, + subpackage: usesHttp, + subpackageDeps: {}, + matcher: isFalse, + )); + }); + });