From 1c24aee5af119513d9aa8a2ba828fc8fc880bddc Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 21 Nov 2023 09:23:35 +0000 Subject: [PATCH] feat!: detect licenses locally (#883) --- lib/src/command_runner.dart | 4 +- .../packages/commands/check/check.dart | 4 +- .../commands/check/commands/licenses.dart | 131 ++- lib/src/commands/packages/packages.dart | 5 +- lib/src/pub_license/pub_license.dart | 137 ---- pubspec.yaml | 4 +- test/helpers/command_helper.dart | 10 +- .../create/commands/dart_cli_test.dart | 3 +- .../create/commands/dart_package_test.dart | 3 +- .../create/commands/docs_site_test.dart | 3 +- .../create/commands/flame_game_test.dart | 3 +- .../create/commands/flutter_app_test.dart | 3 +- .../create/commands/flutter_package_test.dart | 3 +- .../create/commands/flutter_plugin_test.dart | 3 +- test/src/commands/create/create_test.dart | 3 +- .../packages/commands/check/check_test.dart | 3 +- .../check/commands/licenses_test.dart | 756 +++++++++++++----- .../commands/packages/commands/get_test.dart | 33 +- test/src/commands/packages/packages_test.dart | 3 +- test/src/commands/test/test_test.dart | 9 +- test/src/commands/update_test.dart | 15 +- .../generate_pub_license_fixtures.dart | 74 -- .../pub_license/fixtures/multipleLicense.html | 231 ------ test/src/pub_license/fixtures/noLicense.html | 3 - .../pub_license/fixtures/singleLicense.html | 22 - test/src/pub_license/pub_license_test.dart | 223 ------ 26 files changed, 687 insertions(+), 1004 deletions(-) delete mode 100644 lib/src/pub_license/pub_license.dart delete mode 100644 test/src/pub_license/fixtures/generate_pub_license_fixtures.dart delete mode 100644 test/src/pub_license/fixtures/multipleLicense.html delete mode 100644 test/src/pub_license/fixtures/noLicense.html delete mode 100644 test/src/pub_license/fixtures/singleLicense.html delete mode 100644 test/src/pub_license/pub_license_test.dart diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 53418ae2..b3f013ec 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -8,7 +8,6 @@ import 'package:pub_updater/pub_updater.dart'; import 'package:universal_io/io.dart'; import 'package:very_good_cli/src/commands/commands.dart'; import 'package:very_good_cli/src/logger_extension.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; import 'package:very_good_cli/src/version.dart'; /// The package name. @@ -23,7 +22,6 @@ class VeryGoodCommandRunner extends CompletionCommandRunner { Logger? logger, PubUpdater? pubUpdater, Map? environment, - @visibleForTesting PubLicense? pubLicense, }) : _logger = logger ?? Logger(), _pubUpdater = pubUpdater ?? PubUpdater(), _environment = environment ?? Platform.environment, @@ -39,7 +37,7 @@ class VeryGoodCommandRunner extends CompletionCommandRunner { help: 'Noisy logging, including all shell commands executed.', ); addCommand(CreateCommand(logger: _logger)); - addCommand(PackagesCommand(logger: _logger, pubLicense: pubLicense)); + addCommand(PackagesCommand(logger: _logger)); addCommand(TestCommand(logger: _logger)); addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater)); } diff --git a/lib/src/commands/packages/commands/check/check.dart b/lib/src/commands/packages/commands/check/check.dart index 9f9b1f88..07ea1c59 100644 --- a/lib/src/commands/packages/commands/check/check.dart +++ b/lib/src/commands/packages/commands/check/check.dart @@ -1,7 +1,6 @@ import 'package:args/command_runner.dart'; import 'package:mason/mason.dart'; import 'package:very_good_cli/src/commands/packages/commands/check/commands/commands.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; /// {@template packages_check_command} /// `very_good packages check` command for performing checks in a Dart or @@ -11,10 +10,9 @@ class PackagesCheckCommand extends Command { /// {@macro packages_check_command} PackagesCheckCommand({ Logger? logger, - PubLicense? pubLicense, }) { addSubcommand( - PackagesCheckLicensesCommand(logger: logger, pubLicense: pubLicense), + PackagesCheckLicensesCommand(logger: logger), ); } diff --git a/lib/src/commands/packages/commands/check/commands/licenses.dart b/lib/src/commands/packages/commands/check/commands/licenses.dart index 7a902acb..ba2ca85e 100644 --- a/lib/src/commands/packages/commands/check/commands/licenses.dart +++ b/lib/src/commands/packages/commands/check/commands/licenses.dart @@ -2,13 +2,36 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; +import 'package:collection/collection.dart'; import 'package:mason/mason.dart'; import 'package:meta/meta.dart'; +import 'package:package_config/package_config.dart' as package_config; + +// We rely on PANA's license detection algorithm to retrieve licenses from +// packages. +// +// This license detection algorithm is not exposed as a public API, so we have +// to import it directly. +// +// See also: +// +// * [PANA's faster license detection GitHub issue](https://github.com/dart-lang/pana/issues/1277) +// ignore: implementation_imports +import 'package:pana/src/license_detection/license_detector.dart' as detector; import 'package:path/path.dart' as path; import 'package:pubspec_lock/pubspec_lock.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; import 'package:very_good_cli/src/pub_license/spdx_license.gen.dart'; +/// Overrides the [package_config.findPackageConfig] function for testing. +@visibleForTesting +Future Function( + Directory directory, +)? findPackageConfigOverride; + +/// Overrides the [detector.detectLicense] function for testing. +@visibleForTesting +Future Function(String, double)? detectLicenseOverride; + /// The basename of the pubspec lock file. @visibleForTesting const pubspecLockBasename = 'pubspec.lock'; @@ -24,6 +47,16 @@ final licenseDocumentationUri = Uri.parse( 'https://cli.vgv.dev/docs/commands/check_licenses', ); +/// The detection threshold used by [detector.detectLicense]. +/// +/// This value is used to determine the confidence threshold for detecting +/// licenses. The value should match the default value used by PANA. +/// +/// See also: +/// +/// * [PANA's default threshold value](https://github.com/dart-lang/pana/blob/b598d45051ba4e028e9021c2aeb9c04e4335de76/lib/src/license.dart#L48) +const _defaultDetectionThreshold = 0.95; + /// Defines a [Map] with dependencies as keys and their licenses as values. /// /// If a dependency's license failed to be retrieved its license will be `null`. @@ -40,9 +73,7 @@ class PackagesCheckLicensesCommand extends Command { /// {@macro packages_check_licenses_command} PackagesCheckLicensesCommand({ Logger? logger, - PubLicense? pubLicense, - }) : _logger = logger ?? Logger(), - _pubLicense = pubLicense ?? PubLicense() { + }) : _logger = logger ?? Logger() { argParser ..addFlag( 'ignore-retrieval-failures', @@ -80,8 +111,6 @@ class PackagesCheckLicensesCommand extends Command { final Logger _logger; - final PubLicense _pubLicense; - @override String get description => "Check packages' licenses in a Dart or Flutter project."; @@ -128,6 +157,13 @@ class PackagesCheckLicensesCommand extends Command { final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.'; final targetPath = path.normalize(Directory(target).absolute.path); + final targetDirectory = Directory(targetPath); + if (!targetDirectory.existsSync()) { + _logger.err( + '''Could not find directory at $targetPath. Specify a valid path to a Dart or Flutter project.''', + ); + return ExitCode.noInput.code; + } final progress = _logger.progress('Checking licenses on $targetPath'); @@ -169,28 +205,70 @@ class PackagesCheckLicensesCommand extends Command { return ExitCode.usage.code; } + final packageConfig = await _tryFindPackageConfig(targetDirectory); + if (packageConfig == null) { + progress.cancel(); + _logger.err( + '''Could not find a valid package config in $targetPath. Run `dart pub get` or `flutter pub get` to generate one.''', + ); + return ExitCode.noInput.code; + } + final licenses = ?>{}; + final detectLicense = detectLicenseOverride ?? detector.detectLicense; for (final dependency in filteredDependencies) { progress.update( '''Collecting licenses from ${licenses.length + 1} out of ${filteredDependencies.length} ${filteredDependencies.length == 1 ? 'package' : 'packages'}''', ); final dependencyName = dependency.package(); - Set? rawLicense; - try { - rawLicense = await _pubLicense.getLicense(dependencyName); - } on PubLicenseException catch (e) { - final errorMessage = '[$dependencyName] ${e.message}'; + final cachePackageEntry = packageConfig.packages + .firstWhereOrNull((package) => package.name == dependencyName); + if (cachePackageEntry == null) { + final errorMessage = + '''[$dependencyName] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.'''; if (!ignoreFailures) { progress.cancel(); _logger.err(errorMessage); - return ExitCode.unavailable.code; + return ExitCode.noInput.code; } _logger.err('\n$errorMessage'); + licenses[dependencyName] = {SpdxLicense.$unknown.value}; + continue; + } + + final packagePath = path.normalize(cachePackageEntry.root.path); + final packageDirectory = Directory(packagePath); + if (!packageDirectory.existsSync()) { + final errorMessage = + '''[$dependencyName] Could not find package directory at $packagePath.'''; + if (!ignoreFailures) { + progress.cancel(); + _logger.err(errorMessage); + return ExitCode.noInput.code; + } + + _logger.err('\n$errorMessage'); + licenses[dependencyName] = {SpdxLicense.$unknown.value}; + continue; + } + + final licenseFile = File(path.join(packagePath, 'LICENSE')); + if (!licenseFile.existsSync()) { + licenses[dependencyName] = {SpdxLicense.$unknown.value}; + continue; + } + + final licenseFileContent = licenseFile.readAsStringSync(); + + late final detector.Result detectorResult; + try { + detectorResult = + await detectLicense(licenseFileContent, _defaultDetectionThreshold); } catch (e) { final errorMessage = - '[$dependencyName] Unexpected failure with error: $e'; + '''[$dependencyName] Failed to detect license from $packagePath: $e'''; if (!ignoreFailures) { progress.cancel(); _logger.err(errorMessage); @@ -198,9 +276,15 @@ class PackagesCheckLicensesCommand extends Command { } _logger.err('\n$errorMessage'); - } finally { - licenses[dependencyName] = rawLicense; + licenses[dependencyName] = {SpdxLicense.$unknown.value}; + continue; } + + final rawLicense = detectorResult.matches + // ignore: invalid_use_of_visible_for_testing_member + .map((match) => match.license.identifier) + .toSet(); + licenses[dependencyName] = rawLicense; } late final _BannedDependencyLicenseMap? bannedDependencies; @@ -246,6 +330,23 @@ PubspecLock? _tryParsePubspecLock(File pubspecLockFile) { } } +/// Attempts to find a [package_config.PackageConfig] using +/// [package_config.findPackageConfig]. +/// +/// If [package_config.findPackageConfig] fails to find a package config `null` +/// is returned. +Future _tryFindPackageConfig( + Directory directory, +) async { + try { + final findPackageConfig = + findPackageConfigOverride ?? package_config.findPackageConfig; + return await findPackageConfig(directory); + } catch (error) { + return null; + } +} + /// Verifies that all [licenses] are valid license inputs. /// /// Valid license inputs are: diff --git a/lib/src/commands/packages/packages.dart b/lib/src/commands/packages/packages.dart index 6d483192..7afeec60 100644 --- a/lib/src/commands/packages/packages.dart +++ b/lib/src/commands/packages/packages.dart @@ -2,16 +2,15 @@ import 'package:args/command_runner.dart'; import 'package:mason/mason.dart'; import 'package:very_good_cli/src/commands/packages/commands/check/check.dart'; import 'package:very_good_cli/src/commands/packages/commands/commands.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; /// {@template packages_command} /// `very_good packages` command for managing packages. /// {@endtemplate} class PackagesCommand extends Command { /// {@macro packages_command} - PackagesCommand({Logger? logger, PubLicense? pubLicense}) { + PackagesCommand({Logger? logger}) { addSubcommand(PackagesGetCommand(logger: logger)); - addSubcommand(PackagesCheckCommand(logger: logger, pubLicense: pubLicense)); + addSubcommand(PackagesCheckCommand(logger: logger)); } @override diff --git a/lib/src/pub_license/pub_license.dart b/lib/src/pub_license/pub_license.dart deleted file mode 100644 index e31c2bb8..00000000 --- a/lib/src/pub_license/pub_license.dart +++ /dev/null @@ -1,137 +0,0 @@ -/// Enables checking a package's license from pub.dev. -/// -/// This library is intended to be used by Very Good CLI to help extracting -/// license information. The existence of this library is likely to be -/// ephemeral. It may be obsolete once [pub.dev](https://pub.dev/) exposes -/// stable license information in their official API; you may track the -/// progress [here](https://github.com/dart-lang/pub-dev/issues/4717). -library pub_license; - -import 'package:html/dom.dart' as html_dom; -import 'package:html/parser.dart' as html_parser; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; - -/// The pub.dev [Uri] used to retrieve the license of a package. -Uri _pubPackageLicenseUri(String packageName) => - Uri.parse('https://pub.dev/packages/$packageName/license'); - -/// {@template pub_license_exception} -/// An exception thrown by [PubLicense]. -/// {@endtemplate} -class PubLicenseException implements Exception { - /// {@macro pub_license_exception} - const PubLicenseException(String message) - : message = '[pub_license] $message'; - - /// The exception message. - final String message; -} - -/// The function signature for parsing HTML documents. -@visibleForTesting -typedef HtmlDocumentParse = html_dom.Document Function( - dynamic input, { - String? encoding, - bool generateSpans, - String? sourceUrl, -}); - -/// {@template pub_license} -/// Enables checking pub.dev's hosted packages license. -/// {@endtemplate} -class PubLicense { - /// {@macro pub_license} - PubLicense({ - @visibleForTesting http.Client? client, - @visibleForTesting HtmlDocumentParse? parse, - }) : _client = client ?? http.Client(), - _parse = parse ?? html_parser.parse; - - final http.Client _client; - - final html_dom.Document Function( - dynamic input, { - String? encoding, - bool generateSpans, - String? sourceUrl, - }) _parse; - - /// Retrieves the license of a package. - /// - /// Some packages may have multiple licenses, hence a [Set] is returned. - /// - /// It may throw a [PubLicenseException] if: - /// * The response from pub.dev is not successful. - /// * The response body cannot be parsed. - Future> getLicense(String packageName) async { - final response = await _client.get(_pubPackageLicenseUri(packageName)); - - if (response.statusCode != 200) { - throw PubLicenseException( - '''Failed to retrieve the license of the package, received status code: ${response.statusCode}''', - ); - } - - late final html_dom.Document document; - try { - document = _parse(response.body); - } on html_parser.ParseError catch (e) { - throw PubLicenseException( - 'Failed to parse the response body, received error: $e', - ); - } catch (e) { - throw PubLicenseException( - '''An unknown error occurred when trying to parse the response body, received error: $e''', - ); - } - - return _scrapeLicense(document); - } -} - -/// Scrapes the license from the pub.dev's package license page. -/// -/// The expected HTML structure is: -/// ```html -/// -/// ``` -/// -/// It may throw a [PubLicenseException] if: -/// * The detail info box is not found. -/// * The license header is not found. -Set _scrapeLicense(html_dom.Document document) { - final detailInfoBox = document.querySelector('.detail-info-box'); - if (detailInfoBox == null) { - throw const PubLicenseException( - '''Failed to scrape license because `.detail-info-box` was not found.''', - ); - } - - String? rawLicenseText; - for (var i = 0; i < detailInfoBox.children.length; i++) { - final child = detailInfoBox.children[i]; - - final headerText = child.text.trim().toLowerCase(); - if (headerText == 'license') { - rawLicenseText = detailInfoBox.children[i + 1].text.trim(); - break; - } - } - if (rawLicenseText == null) { - throw const PubLicenseException( - '''Failed to scrape license because the license header was not found.''', - ); - } - - final licenseText = rawLicenseText.split('(').first.trim(); - return licenseText.split(',').map((e) => e.trim()).toSet(); -} diff --git a/pubspec.yaml b/pubspec.yaml index 44f03595..699ece35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,12 +15,12 @@ dependencies: cli_completion: ^0.4.0 collection: ^1.17.1 glob: ^2.0.2 - html: ^0.15.4 # This dependency is temporary and should be removed once pub_license is obsolete. - http: ^1.1.0 # This dependency is temporary and should be removed once pub_license is obsolete. lcov_parser: ^0.1.2 mason: 0.1.0-dev.51 mason_logger: ^0.2.2 meta: ^1.3.0 + package_config: ^2.1.0 + pana: 0.21.42 # Very Good CLI is using private PANA's license detector that might break in a minor version update. path: ^1.8.0 pub_updater: ">=0.3.1 <0.5.0" pubspec_lock: ^3.0.2 diff --git a/test/helpers/command_helper.dart b/test/helpers/command_helper.dart index 3bd14286..3920f25f 100644 --- a/test/helpers/command_helper.dart +++ b/test/helpers/command_helper.dart @@ -4,7 +4,6 @@ import 'package:mason/mason.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pub_updater/pub_updater.dart'; import 'package:very_good_cli/src/command_runner.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; class _MockLogger extends Mock implements Logger {} @@ -12,8 +11,6 @@ class _MockProgress extends Mock implements Progress {} class _MockPubUpdater extends Mock implements PubUpdater {} -class _MockPubLicense extends Mock implements PubLicense {} - void Function() _overridePrint(void Function(List) fn) { return () { final printLogs = []; @@ -34,7 +31,6 @@ void Function() withRunner( VeryGoodCommandRunner commandRunner, Logger logger, PubUpdater pubUpdater, - PubLicense pubLicense, List printLogs, ) runnerFn, ) { @@ -42,13 +38,11 @@ void Function() withRunner( final logger = _MockLogger(); final progress = _MockProgress(); final pubUpdater = _MockPubUpdater(); - final pubLicense = _MockPubLicense(); final progressLogs = []; final commandRunner = VeryGoodCommandRunner( logger: logger, pubUpdater: pubUpdater, environment: {'CI': 'true'}, - pubLicense: pubLicense, ); when(() => progress.complete(any())).thenAnswer((_) { @@ -62,9 +56,7 @@ void Function() withRunner( currentVersion: any(named: 'currentVersion'), ), ).thenAnswer((_) => Future.value(true)); - when(() => pubLicense.getLicense(any())) - .thenAnswer((_) => Future.value({'MIT'})); - await runnerFn(commandRunner, logger, pubUpdater, pubLicense, printLogs); + await runnerFn(commandRunner, logger, pubUpdater, printLogs); }); } diff --git a/test/src/commands/create/commands/dart_cli_test.dart b/test/src/commands/create/commands/dart_cli_test.dart index 85ed36d7..a19ed00b 100644 --- a/test/src/commands/create/commands/dart_cli_test.dart +++ b/test/src/commands/create/commands/dart_cli_test.dart @@ -83,8 +83,7 @@ void main() { group('create dart_cli', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'dart_cli', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/commands/dart_package_test.dart b/test/src/commands/create/commands/dart_package_test.dart index 7ca32670..7e69b9ff 100644 --- a/test/src/commands/create/commands/dart_package_test.dart +++ b/test/src/commands/create/commands/dart_package_test.dart @@ -83,8 +83,7 @@ void main() { group('create dart_package', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'dart_package', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/commands/docs_site_test.dart b/test/src/commands/create/commands/docs_site_test.dart index 20ae50ae..9471a613 100644 --- a/test/src/commands/create/commands/docs_site_test.dart +++ b/test/src/commands/create/commands/docs_site_test.dart @@ -82,8 +82,7 @@ void main() { group('create docs_site', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'docs_site', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/commands/flame_game_test.dart b/test/src/commands/create/commands/flame_game_test.dart index b47fbfad..5455663a 100644 --- a/test/src/commands/create/commands/flame_game_test.dart +++ b/test/src/commands/create/commands/flame_game_test.dart @@ -88,8 +88,7 @@ void main() { group('create flame_game', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'flame_game', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/commands/flutter_app_test.dart b/test/src/commands/create/commands/flutter_app_test.dart index 51510beb..4bde545d 100644 --- a/test/src/commands/create/commands/flutter_app_test.dart +++ b/test/src/commands/create/commands/flutter_app_test.dart @@ -94,8 +94,7 @@ void main() { group('create flutter_app', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'flutter_app', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/commands/flutter_package_test.dart b/test/src/commands/create/commands/flutter_package_test.dart index 99ad6655..85ee3b90 100644 --- a/test/src/commands/create/commands/flutter_package_test.dart +++ b/test/src/commands/create/commands/flutter_package_test.dart @@ -81,8 +81,7 @@ void main() { group('create flutter_package', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'flutter_package', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/commands/flutter_plugin_test.dart b/test/src/commands/create/commands/flutter_plugin_test.dart index 96a2e43c..cd41aa94 100644 --- a/test/src/commands/create/commands/flutter_plugin_test.dart +++ b/test/src/commands/create/commands/flutter_plugin_test.dart @@ -94,8 +94,7 @@ void main() { group('create flutter_plugin', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', 'flutter_plugin', '--help']); expect(printLogs, equals(expectedUsage)); diff --git a/test/src/commands/create/create_test.dart b/test/src/commands/create/create_test.dart index 655c86e2..897d7fb3 100644 --- a/test/src/commands/create/create_test.dart +++ b/test/src/commands/create/create_test.dart @@ -32,8 +32,7 @@ void main() { group('create', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['create', '--help']); expect(printLogs, equals(expectedUsage)); expect(result, equals(ExitCode.success.code)); diff --git a/test/src/commands/packages/commands/check/check_test.dart b/test/src/commands/packages/commands/check/check_test.dart index 6a07b5ab..90b8fa55 100644 --- a/test/src/commands/packages/commands/check/check_test.dart +++ b/test/src/commands/packages/commands/check/check_test.dart @@ -27,8 +27,7 @@ void main() { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( [...commandArguments, '--help'], ); diff --git a/test/src/commands/packages/commands/check/commands/licenses_test.dart b/test/src/commands/packages/commands/check/commands/licenses_test.dart index 6a71a313..08a69f5a 100644 --- a/test/src/commands/packages/commands/check/commands/licenses_test.dart +++ b/test/src/commands/packages/commands/check/commands/licenses_test.dart @@ -3,15 +3,30 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:package_config/package_config.dart' as package_config; +// ignore: implementation_imports +import 'package:pana/src/license_detection/license_detector.dart' as detector; import 'package:path/path.dart' as path; import 'package:test/test.dart'; import 'package:very_good_cli/src/commands/packages/commands/check/commands/commands.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; import '../../../../../../helpers/helpers.dart'; class _MockProgress extends Mock implements Progress {} +class _MockResult extends Mock implements detector.Result {} + +// ignore: subtype_of_sealed_class +class _MockLicenseMatch extends Mock implements detector.LicenseMatch {} + +// ignore: subtype_of_sealed_class +class _MockLicenseWithNGrams extends Mock + implements detector.LicenseWithNGrams {} + +class _MockPackageConfig extends Mock implements package_config.PackageConfig {} + +class _MockPackage extends Mock implements package_config.Package {} + const _expectedPackagesCheckLicensesUsage = [ // ignore: no_adjacent_strings_in_list "Check packages' licenses in a Dart or Flutter project.\n" @@ -40,8 +55,18 @@ void main() { const allowedArgument = '--allowed'; late Progress progress; + late detector.Result detectorResult; + late package_config.PackageConfig packageConfig; late Directory tempDirectory; + late detector.LicenseMatch mitLicenseMatch; + late detector.LicenseMatch bsdLicenseMatch; + + late package_config.Package veryGoodTestRunnerConfigPackage; + late package_config.Package cliCompletionConfigPackage; + late package_config.Package yamlConfigPackage; + late package_config.Package veryGoodAnalysisConfigPackage; + setUpAll(() { registerFallbackValue(''); }); @@ -49,14 +74,51 @@ void main() { setUp(() { progress = _MockProgress(); + detectorResult = _MockResult(); + + detectLicenseOverride = (_, __) async => detectorResult; + addTearDown(() => detectLicenseOverride = null); + + packageConfig = _MockPackageConfig(); + + findPackageConfigOverride = (_) async => packageConfig; + addTearDown(() => findPackageConfigOverride = null); + tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + mitLicenseMatch = _MockLicenseMatch(); + final mitLicenseWithNGrams = _MockLicenseWithNGrams(); + when(() => mitLicenseMatch.license).thenReturn(mitLicenseWithNGrams); + when(() => mitLicenseWithNGrams.identifier).thenReturn('MIT'); + + bsdLicenseMatch = _MockLicenseMatch(); + final bsdLicenseWithNGrams = _MockLicenseWithNGrams(); + when(() => bsdLicenseMatch.license).thenReturn(bsdLicenseWithNGrams); + when(() => bsdLicenseWithNGrams.identifier).thenReturn('BSD'); + + final packages = { + 'very_good_test_runner': veryGoodTestRunnerConfigPackage = + _MockPackage(), + 'cli_completion': cliCompletionConfigPackage = _MockPackage(), + 'yaml': yamlConfigPackage = _MockPackage(), + 'very_good_analysis': veryGoodAnalysisConfigPackage = _MockPackage(), + }; + for (final package in packages.entries) { + final name = package.key; + final packageConfig = package.value; + + final licenseFile = File(path.join(tempDirectory.path, name, 'LICENSE')) + ..createSync(recursive: true) + ..writeAsStringSync(name); + when(() => packageConfig.name).thenReturn(name); + when(() => packageConfig.root).thenReturn(licenseFile.parent.uri); + } }); test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( [...commandArguments, '--help'], ); @@ -79,8 +141,7 @@ void main() { group('throws usage exception', () { test( '''when too many rest arguments are provided''', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( [...commandArguments, 'arg1', 'arg2'], ); @@ -90,8 +151,7 @@ void main() { test( '''when allowed and forbidden are used simultaneously''', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( [...commandArguments, '--allowed', 'MIT', '--forbidden', 'BSD'], ); @@ -105,11 +165,14 @@ void main() { () { test( '''when there is a single hosted direct dependency and license''', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn([veryGoodTestRunnerConfigPackage]); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -133,15 +196,18 @@ void main() { test( '''when there are multiple hosted direct dependency and licenses''', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - when(() => pubLicense.getLicense(any())) - .thenAnswer((_) => Future.value({'MIT', 'BSD'})); + when(() => detectorResult.matches) + .thenReturn([mitLicenseMatch, bsdLicenseMatch]); + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); final result = await commandRunner.run( [...commandArguments, tempDirectory.path], @@ -169,11 +235,14 @@ void main() { test( '''when both allowed and forbidden are specified but left empty''', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn([veryGoodTestRunnerConfigPackage]); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -201,6 +270,42 @@ void main() { expect(result, equals(ExitCode.success.code)); }), ); + + test( + 'unknown when no license file is found', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validPubspecLockContent); + + when(() => packageConfig.packages) + .thenReturn([veryGoodTestRunnerConfigPackage]); + final licenseFilePath = path.join( + tempDirectory.path, + veryGoodTestRunnerConfigPackage.name, + 'LICENSE', + ); + File(licenseFilePath).deleteSync(); + + when(() => logger.progress(any())).thenReturn(progress); + + final result = await commandRunner.run( + [...commandArguments, tempDirectory.path], + ); + + verify( + () => progress.update( + 'Collecting licenses from 1 out of 1 package', + ), + ).called(1); + verify( + () => progress.complete( + '''Retrieved 1 license from 1 package of type: unknown (1).''', + ), + ).called(1); + + expect(result, equals(ExitCode.success.code)); + }), + ); }, ); @@ -209,18 +314,31 @@ void main() { group('reports licenses', () { test( - 'when a PubLicenseException is thrown', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + 'when an unknown error is thrown', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); const failedDependencyName = 'very_good_test_runner'; - const exception = PubLicenseException('message'); - when(() => pubLicense.getLicense(failedDependencyName)) - .thenThrow(exception); + const error = 'error'; + + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + + detectLicenseOverride = (name, __) async { + if (name == failedDependencyName) { + // ignore: only_throw_errors + throw error; + } + + final detectorResult = _MockResult(); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + return detectorResult; + }; final result = await commandRunner.run( [ @@ -230,8 +348,12 @@ void main() { ], ); + final packagePath = path.join( + tempDirectory.path, + veryGoodTestRunnerConfigPackage.name, + ); final errorMessage = - '''\n[$failedDependencyName] ${exception.message}'''; + '''\n[$failedDependencyName] Failed to detect license from $packagePath: $error'''; verify(() => logger.err(errorMessage)).called(1); verify( @@ -246,7 +368,7 @@ void main() { ).called(1); verify( () => progress.complete( - 'Retrieved 1 license from 2 packages of type: MIT (1).', + '''Retrieved 2 licenses from 2 packages of type: unknown (1) and MIT (1).''', ), ).called(1); @@ -255,106 +377,156 @@ void main() { ); test( - 'when an unknown error is thrown', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + 'when cached package path cannot be found', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) - .writeAsStringSync(_validMultiplePubspecLockContent); + .writeAsStringSync(_validPubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - const failedDependencyName = 'very_good_test_runner'; - const error = 'error'; - when(() => pubLicense.getLicense(failedDependencyName)) - .thenThrow(error); + when(() => packageConfig.packages).thenReturn({}); + final targetPath = tempDirectory.path; final result = await commandRunner.run( [ ...commandArguments, ignoreRetrievalFailuresArgument, - tempDirectory.path, + targetPath, ], ); - const errorMessage = - '''\n[$failedDependencyName] Unexpected failure with error: $error'''; + final errorMessage = + '''\n[${veryGoodTestRunnerConfigPackage.name}] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.'''; verify(() => logger.err(errorMessage)).called(1); verify( () => progress.update( - 'Collecting licenses from 1 out of 2 packages', + 'Collecting licenses from 1 out of 1 package', ), ).called(1); + verify( + () => progress.complete( + '''Retrieved 1 license from 1 package of type: unknown (1).''', + ), + ).called(1); + + expect(result, equals(ExitCode.success.code)); + }), + ); + + test( + 'when cached package directory cannot be found', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validPubspecLockContent); + + when(() => logger.progress(any())).thenReturn(progress); + + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + + final packagePath = + path.join(tempDirectory.path, 'inexistent', 'nothing'); + when(() => veryGoodTestRunnerConfigPackage.root).thenReturn( + Uri.parse(packagePath), + ); + + final targetPath = tempDirectory.path; + final result = await commandRunner.run( + [ + ...commandArguments, + ignoreRetrievalFailuresArgument, + targetPath, + ], + ); + + final errorMessage = + '''\n[${veryGoodTestRunnerConfigPackage.name}] Could not find package directory at $packagePath.'''; + verify(() => logger.err(errorMessage)).called(1); + verify( () => progress.update( - 'Collecting licenses from 2 out of 2 packages', + 'Collecting licenses from 1 out of 1 package', ), ).called(1); verify( () => progress.complete( - 'Retrieved 1 license from 2 packages of type: MIT (1).', + '''Retrieved 1 license from 1 package of type: unknown (1).''', ), ).called(1); expect(result, equals(ExitCode.success.code)); }), ); - }); - test( - 'when all licenses fail to be retrieved', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { - File(path.join(tempDirectory.path, pubspecLockBasename)) - .writeAsStringSync(_validMultiplePubspecLockContent); + test( + 'when all licenses fail to be retrieved', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validMultiplePubspecLockContent); - when(() => logger.progress(any())).thenReturn(progress); + when(() => logger.progress(any())).thenReturn(progress); - const error = 'error'; - when(() => pubLicense.getLicense(any())).thenThrow(error); + const error = 'error'; + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + detectLicenseOverride = (name, __) async { + // ignore: only_throw_errors + throw error; + }; - final result = await commandRunner.run( - [ - ...commandArguments, - ignoreRetrievalFailuresArgument, - tempDirectory.path, - ], - ); + final result = await commandRunner.run( + [ + ...commandArguments, + ignoreRetrievalFailuresArgument, + tempDirectory.path, + ], + ); - final packageNames = verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast(); + final packageNames = packageConfig.packages.map((package) { + return package.name; + }).toList(); - verify( - () => logger.err( - '''\n[${packageNames[0]}] Unexpected failure with error: $error''', - ), - ).called(1); - verify( - () => logger.err( - '''\n[${packageNames[1]}] Unexpected failure with error: $error''', - ), - ).called(1); + final firstPackageName = packageNames[0]; + final firstPackagePath = + path.join(tempDirectory.path, firstPackageName); + verify( + () => logger.err( + '''\n[$firstPackageName] Failed to detect license from $firstPackagePath: $error''', + ), + ).called(1); - verify( - () => progress.update( - 'Collecting licenses from 1 out of 2 packages', - ), - ).called(1); - verify( - () => progress.update( - 'Collecting licenses from 2 out of 2 packages', - ), - ).called(1); - verify( - () => progress.complete( - 'Retrieved 0 licenses from 2 packages.', - ), - ).called(1); + final secondPackageName = packageNames[1]; + final secondPackagePath = + path.join(tempDirectory.path, secondPackageName); + verify( + () => logger.err( + '''\n[$secondPackageName] Failed to detect license from $secondPackagePath: $error''', + ), + ).called(1); - expect(result, equals(ExitCode.success.code)); - }), - ); + verify( + () => progress.update( + 'Collecting licenses from 1 out of 2 packages', + ), + ).called(1); + verify( + () => progress.update( + 'Collecting licenses from 2 out of 2 packages', + ), + ).called(1); + verify( + () => progress.complete( + '''Retrieved 2 licenses from 2 packages of type: unknown (2).''', + ), + ).called(1); + + expect(result, equals(ExitCode.success.code)); + }), + ); + }); }); group('dependency-type', () { @@ -366,8 +538,7 @@ void main() { group('throws usage exception', () { test( 'when no option is provided', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( [...commandArguments, dependencyTypeArgument], ); @@ -377,8 +548,7 @@ void main() { test( 'when invalid option is provided', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( [...commandArguments, dependencyTypeArgument, 'invalid'], ); @@ -401,22 +571,25 @@ void main() { commandRunner, logger, pubUpdater, - pubLicense, printLogs, ) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + when(() => detectorResult.matches) + .thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( [...commandArguments, tempDirectory.path], ); - final packageNames = - verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast(); + final packageNames = packageConfig.packages.map((package) { + return package.name; + }).toList(); expect( packageNames, @@ -444,12 +617,16 @@ void main() { commandRunner, logger, pubUpdater, - pubLicense, printLogs, ) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + when(() => detectorResult.matches) + .thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -461,10 +638,9 @@ void main() { ], ); - final packageNames = - verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast(); + final packageNames = packageConfig.packages.map((package) { + return package.name; + }).toList(); expect( packageNames, @@ -493,12 +669,15 @@ void main() { commandRunner, logger, pubUpdater, - pubLicense, printLogs, ) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn({veryGoodAnalysisConfigPackage}); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -510,10 +689,9 @@ void main() { ], ); - final packageNames = - verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast(); + final packageNames = packageConfig.packages.map((package) { + return package.name; + }).toList(); expect( packageNames, @@ -541,12 +719,15 @@ void main() { commandRunner, logger, pubUpdater, - pubLicense, printLogs, ) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn({yamlConfigPackage}); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -558,10 +739,9 @@ void main() { ], ); - final packageNames = - verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast(); + final packageNames = packageConfig.packages.map((package) { + return package.name; + }).toList(); expect( packageNames, @@ -589,12 +769,18 @@ void main() { commandRunner, logger, pubUpdater, - pubLicense, printLogs, ) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + veryGoodAnalysisConfigPackage, + yamlConfigPackage, + }); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -610,10 +796,9 @@ void main() { ], ); - final packageNames = - verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast(); + final packageNames = packageConfig.packages.map((package) { + return package.name; + }).toList(); expect( packageNames, @@ -651,11 +836,14 @@ void main() { group('allowed', () { test( 'warns when a license is not recognized', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + when(() => logger.progress(any())).thenReturn(progress); const invalidLicense = 'not_a_valid_license'; @@ -682,15 +870,15 @@ void main() { test( 'exits when a license is not allowed', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - when(() => pubLicense.getLicense(any())) - .thenAnswer((_) => Future.value({'MIT'})); + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); final result = await commandRunner.run( [ @@ -708,25 +896,32 @@ void main() { group('reports', () { test( 'when a single license is not allowed', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - const dependency1Name = 'very_good_test_runner'; - when(() => pubLicense.getLicense(dependency1Name)) - .thenAnswer((_) => Future.value({'MIT'})); - final license1LinkedMessage = link( - uri: pubLicenseUri(dependency1Name), + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + detectLicenseOverride = (String name, _) async { + final detectorResult = _MockResult(); + final licenseMatch = name == veryGoodTestRunnerConfigPackage.name + ? [mitLicenseMatch] + : [bsdLicenseMatch]; + + when(() => detectorResult.matches).thenReturn(licenseMatch); + return detectorResult; + }; + + const forbiddenDependencyName = 'very_good_test_runner'; + final forbiddenDependencyLinkedMessage = link( + uri: pubLicenseUri(forbiddenDependencyName), message: 'MIT', ); - const dependency2Name = 'cli_completion'; - when(() => pubLicense.getLicense(dependency2Name)) - .thenAnswer((_) => Future.value({'BSD'})); - await commandRunner.run( [ ...commandArguments, @@ -737,7 +932,7 @@ void main() { ); final errorMessage = - '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).'''; + '''1 dependency has a banned license: $forbiddenDependencyName ($forbiddenDependencyLinkedMessage).'''; verify( () => logger.err(errorMessage), @@ -747,25 +942,32 @@ void main() { test( 'when a single license is not allowed and forbidden is left empty', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - const dependency1Name = 'very_good_test_runner'; - when(() => pubLicense.getLicense(dependency1Name)) - .thenAnswer((_) => Future.value({'MIT'})); - final license1LinkedMessage = link( - uri: pubLicenseUri(dependency1Name), + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + detectLicenseOverride = (String name, _) async { + final detectorResult = _MockResult(); + final licenseMatch = name == veryGoodTestRunnerConfigPackage.name + ? [mitLicenseMatch] + : [bsdLicenseMatch]; + + when(() => detectorResult.matches).thenReturn(licenseMatch); + return detectorResult; + }; + + const forbiddenDependencyName = 'very_good_test_runner'; + final forbiddenDependencyLinkedMessage = link( + uri: pubLicenseUri(forbiddenDependencyName), message: 'MIT', ); - const dependency2Name = 'cli_completion'; - when(() => pubLicense.getLicense(dependency2Name)) - .thenAnswer((_) => Future.value({'BSD'})); - await commandRunner.run( [ ...commandArguments, @@ -778,7 +980,7 @@ void main() { ); final errorMessage = - '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).'''; + '''1 dependency has a banned license: $forbiddenDependencyName ($forbiddenDependencyLinkedMessage).'''; verify( () => logger.err(errorMessage), @@ -788,24 +990,33 @@ void main() { test( 'when multiple licenses are not allowed', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + detectLicenseOverride = (String name, _) async { + final detectorResult = _MockResult(); + final licenseMatch = name == veryGoodTestRunnerConfigPackage.name + ? [mitLicenseMatch] + : [bsdLicenseMatch]; + + when(() => detectorResult.matches).thenReturn(licenseMatch); + return detectorResult; + }; + const dependency1Name = 'very_good_test_runner'; - when(() => pubLicense.getLicense(dependency1Name)) - .thenAnswer((_) => Future.value({'MIT'})); final license1LinkedMessage = link( uri: pubLicenseUri(dependency1Name), message: 'MIT', ); const dependency2Name = 'cli_completion'; - when(() => pubLicense.getLicense(dependency2Name)) - .thenAnswer((_) => Future.value({'BSD'})); final license2LinkedMessage = link( uri: pubLicenseUri(dependency2Name), message: 'BSD', @@ -834,13 +1045,16 @@ void main() { group('forbidden', () { test( 'warns when a license is not recognized', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + const invalidLicense = 'not_a_valid_license'; await commandRunner.run( [ @@ -865,15 +1079,15 @@ void main() { test( 'exits when a license is forbidden', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - when(() => pubLicense.getLicense(any())) - .thenAnswer((_) => Future.value({'BSD'})); + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + when(() => detectorResult.matches).thenReturn([bsdLicenseMatch]); final result = await commandRunner.run( [ @@ -891,25 +1105,34 @@ void main() { group('report', () { test( 'when a single license is forbidden', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - const dependency1Name = 'very_good_test_runner'; - when(() => pubLicense.getLicense(dependency1Name)) - .thenAnswer((_) => Future.value({'MIT'})); - final license1LinkedMessage = link( - uri: pubLicenseUri(dependency1Name), + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + final packageLicenseMatch = { + veryGoodTestRunnerConfigPackage.name: [mitLicenseMatch], + cliCompletionConfigPackage.name: [bsdLicenseMatch], + }; + detectLicenseOverride = (String name, _) async { + final detectorResult = _MockResult(); + final licenseMatch = packageLicenseMatch[name]!; + + when(() => detectorResult.matches).thenReturn(licenseMatch); + return detectorResult; + }; + + const forbiddenLicenseName = 'very_good_test_runner'; + final forbiddenLicenseLinkMessage = link( + uri: pubLicenseUri(forbiddenLicenseName), message: 'MIT', ); - const dependency2Name = 'cli_completion'; - when(() => pubLicense.getLicense(dependency2Name)) - .thenAnswer((_) => Future.value({'BSD'})); - await commandRunner.run( [ ...commandArguments, @@ -920,7 +1143,7 @@ void main() { ); final errorMessage = - '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).'''; + '''1 dependency has a banned license: $forbiddenLicenseName ($forbiddenLicenseLinkMessage).'''; verify( () => logger.err(errorMessage), @@ -930,25 +1153,34 @@ void main() { test( 'when a single license is forbidden and allowed is left empty', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); - const dependency1Name = 'very_good_test_runner'; - when(() => pubLicense.getLicense(dependency1Name)) - .thenAnswer((_) => Future.value({'MIT'})); - final license1LinkedMessage = link( - uri: pubLicenseUri(dependency1Name), + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + final packageLicenseMatch = { + veryGoodTestRunnerConfigPackage.name: [mitLicenseMatch], + cliCompletionConfigPackage.name: [bsdLicenseMatch], + }; + detectLicenseOverride = (String name, _) async { + final detectorResult = _MockResult(); + final licenseMatch = packageLicenseMatch[name]!; + + when(() => detectorResult.matches).thenReturn(licenseMatch); + return detectorResult; + }; + + const forbiddenLicenseName = 'very_good_test_runner'; + final forbiddenLicenseLinkMessage = link( + uri: pubLicenseUri(forbiddenLicenseName), message: 'MIT', ); - const dependency2Name = 'cli_completion'; - when(() => pubLicense.getLicense(dependency2Name)) - .thenAnswer((_) => Future.value({'BSD'})); - await commandRunner.run( [ ...commandArguments, @@ -961,7 +1193,7 @@ void main() { ); final errorMessage = - '''1 dependency has a banned license: $dependency1Name ($license1LinkedMessage).'''; + '''1 dependency has a banned license: $forbiddenLicenseName ($forbiddenLicenseLinkMessage).'''; verify( () => logger.err(errorMessage), @@ -971,24 +1203,35 @@ void main() { test( 'when multiple licenses are forbidden', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + final packageLicenseMatch = { + veryGoodTestRunnerConfigPackage.name: [mitLicenseMatch], + cliCompletionConfigPackage.name: [bsdLicenseMatch], + }; + detectLicenseOverride = (String name, _) async { + final detectorResult = _MockResult(); + final licenseMatch = packageLicenseMatch[name]!; + + when(() => detectorResult.matches).thenReturn(licenseMatch); + return detectorResult; + }; + const dependency1Name = 'very_good_test_runner'; - when(() => pubLicense.getLicense(dependency1Name)) - .thenAnswer((_) => Future.value({'MIT'})); final license1LinkedMessage = link( uri: pubLicenseUri(dependency1Name), message: 'MIT', ); const dependency2Name = 'cli_completion'; - when(() => pubLicense.getLicense(dependency2Name)) - .thenAnswer((_) => Future.value({'BSD'})); final license2LinkedMessage = link( uri: pubLicenseUri(dependency2Name), message: 'BSD', @@ -1022,13 +1265,18 @@ void main() { group('skips', () { test( 'a single package by name', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + cliCompletionConfigPackage, + }); + when(() => detectorResult.matches).thenReturn([mitLicenseMatch]); + final result = await commandRunner.run( [ ...commandArguments, @@ -1054,8 +1302,7 @@ void main() { test( 'multiple packages by name', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validMultiplePubspecLockContent); @@ -1085,10 +1332,25 @@ void main() { }); group('exits with error', () { + test( + 'when target path does not exist', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + final targetPath = path.join(tempDirectory.path, 'inexistent'); + final result = await commandRunner.run( + [...commandArguments, targetPath], + ); + + final errorMessage = + '''Could not find directory at $targetPath. Specify a valid path to a Dart or Flutter project.'''; + verify(() => logger.err(errorMessage)).called(1); + + expect(result, equals(ExitCode.noInput.code)); + }), + ); + test( 'when it did not find a pubspec.lock file at the target path', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( @@ -1107,8 +1369,7 @@ void main() { test( 'when it failed to parse a pubspec.lock file at the target path', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(''); @@ -1130,8 +1391,7 @@ void main() { test( 'when no dependencies of type are found', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_emptyPubspecLockContent); @@ -1152,65 +1412,119 @@ void main() { ); test( - 'when PubLicense throws a PubLicenseException', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + 'when detectLicense throws an unknown error', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); - when(() => logger.progress(any())).thenReturn(progress); + when(() => packageConfig.packages).thenReturn({ + veryGoodTestRunnerConfigPackage, + }); - const exception = PubLicenseException('message'); - when(() => pubLicense.getLicense('very_good_test_runner')) - .thenThrow(exception); + const error = 'error'; + // ignore: only_throw_errors + detectLicenseOverride = (_, __) => throw error; + + when(() => logger.progress(any())).thenReturn(progress); final result = await commandRunner.run( [...commandArguments, tempDirectory.path], ); - final packageName = verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast() - .first; + final packageName = packageConfig.packages.first.name; + final packagePath = path.join(tempDirectory.path, packageName); + final errorMessage = + '''[$packageName] Failed to detect license from $packagePath: $error'''; - final errorMessage = '[$packageName] ${exception.message}'; verify(() => logger.err(errorMessage)).called(1); verify(() => progress.cancel()).called(1); - expect(result, equals(ExitCode.unavailable.code)); + expect(result, equals(ExitCode.software.code)); }), ); test( - 'when PubLicense throws an unknown error', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + 'when there is no package config file', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { File(path.join(tempDirectory.path, pubspecLockBasename)) .writeAsStringSync(_validPubspecLockContent); + when(() => logger.progress(any())).thenReturn(progress); + const error = 'error'; - when(() => pubLicense.getLicense('very_good_test_runner')) - .thenThrow(error); + // ignore: only_throw_errors + findPackageConfigOverride = (_) async => throw error; + + final targetPath = tempDirectory.path; + final result = await commandRunner.run( + [...commandArguments, targetPath], + ); + + final errorMessage = + '''Could not find a valid package config in $targetPath. Run `dart pub get` or `flutter pub get` to generate one.'''; + verify(() => logger.err(errorMessage)).called(1); + + verify(() => progress.cancel()).called(1); + + expect(result, equals(ExitCode.noInput.code)); + }), + ); + + test( + 'when cached package path cannot be found', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validPubspecLockContent); when(() => logger.progress(any())).thenReturn(progress); + when(() => packageConfig.packages).thenReturn({}); + + final targetPath = tempDirectory.path; final result = await commandRunner.run( - [...commandArguments, tempDirectory.path], + [...commandArguments, targetPath], ); - final packageName = verify(() => pubLicense.getLicense(captureAny())) - .captured - .cast() - .first; + final errorMessage = + '''[${veryGoodTestRunnerConfigPackage.name}] Could not find cached package path. Consider running `dart pub get` or `flutter pub get` to generate a new `package_config.json`.'''; + verify(() => logger.err(errorMessage)).called(1); + + verify(() => progress.cancel()).called(1); + + expect(result, equals(ExitCode.noInput.code)); + }), + ); + + test( + 'when cached package directory cannot be found', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validPubspecLockContent); + + when(() => logger.progress(any())).thenReturn(progress); + + when(() => packageConfig.packages) + .thenReturn({veryGoodTestRunnerConfigPackage}); + + final packagePath = + path.join(tempDirectory.path, 'inexistent', 'nothing'); + when(() => veryGoodTestRunnerConfigPackage.root).thenReturn( + Uri.parse(packagePath), + ); + + final targetPath = tempDirectory.path; + final result = await commandRunner.run( + [...commandArguments, targetPath], + ); final errorMessage = - '[$packageName] Unexpected failure with error: $error'; + '''[${veryGoodTestRunnerConfigPackage.name}] Could not find package directory at $packagePath.'''; verify(() => logger.err(errorMessage)).called(1); verify(() => progress.cancel()).called(1); - expect(result, equals(ExitCode.software.code)); + expect(result, equals(ExitCode.noInput.code)); }), ); }); diff --git a/test/src/commands/packages/commands/get_test.dart b/test/src/commands/packages/commands/get_test.dart index 79e3f9ea..47072b17 100644 --- a/test/src/commands/packages/commands/get_test.dart +++ b/test/src/commands/packages/commands/get_test.dart @@ -22,8 +22,7 @@ void main() { group('packages get', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['packages', 'get', '--help']); expect(printLogs, equals(_expectedPackagesGetUsage)); expect(result, equals(ExitCode.success.code)); @@ -39,8 +38,7 @@ void main() { test( 'throws usage exception ' 'when too many arguments are provided', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( ['packages', 'get', 'arg1', 'arg2'], ); @@ -51,8 +49,7 @@ void main() { test( 'throws pubspec not found exception ' 'when no pubspec.yaml exists', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['packages', 'get', 'test']); expect(result, equals(ExitCode.noInput.code)); verify(() { @@ -64,8 +61,7 @@ void main() { test( 'throws pubspec not found exception ' 'when no pubspec.yaml exists (recursive)', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run( ['packages', 'get', '-r', 'site'], ); @@ -78,8 +74,7 @@ void main() { test( 'throws when installation fails', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -94,8 +89,7 @@ void main() { test( 'ignores .fvm directory', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -123,8 +117,7 @@ void main() { test( 'completes normally ' 'when pubspec.yaml exists', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -152,8 +145,7 @@ void main() { test( 'completes normally ' 'when pubspec.yaml exists (recursive)', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -201,8 +193,7 @@ void main() { test( 'completes normally ' 'when pubspec.yaml exists and directory is not ignored (recursive)', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -253,8 +244,7 @@ void main() { test( 'completes normally ' '''when pubspec.yaml exists and directory is not ignored (recursive) with an empty glob''', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); final directory = Directory( path.join(tempDirectory.path, 'macos_plugin'), @@ -304,8 +294,7 @@ void main() { test( 'completes normally ' 'when pubspec.yaml exists and directory is ignored (recursive)', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); diff --git a/test/src/commands/packages/packages_test.dart b/test/src/commands/packages/packages_test.dart index 7a831213..e16b292e 100644 --- a/test/src/commands/packages/packages_test.dart +++ b/test/src/commands/packages/packages_test.dart @@ -21,8 +21,7 @@ void main() { group('packages', () { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['packages', '--help']); expect(printLogs, equals(_expectedPackagesUsage)); expect(result, equals(ExitCode.success.code)); diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart index a23a957a..046c9567 100644 --- a/test/src/commands/test/test_test.dart +++ b/test/src/commands/test/test_test.dart @@ -111,8 +111,7 @@ void main() { test( 'help', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final result = await commandRunner.run(['test', '--help']); expect(printLogs, equals(expectedTestUsage)); expect(result, equals(ExitCode.success.code)); @@ -128,8 +127,7 @@ void main() { test( 'throws pubspec not found exception ' 'when no pubspec.yaml exists', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -144,8 +142,7 @@ void main() { test( 'completes normally when no pubspec.yaml exists (recursive)', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); diff --git a/test/src/commands/update_test.dart b/test/src/commands/update_test.dart index 1c078c11..6bbf2717 100644 --- a/test/src/commands/update_test.dart +++ b/test/src/commands/update_test.dart @@ -29,8 +29,7 @@ void main() { test( 'handles pub latest version query errors', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { when( () => pubUpdater.getLatestVersion(any()), ).thenThrow(Exception('oops')); @@ -49,8 +48,7 @@ void main() { test( 'handles pub update errors', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { when( () => pubUpdater.getLatestVersion(any()), ).thenAnswer((_) async => latestVersion); @@ -75,8 +73,7 @@ void main() { test( 'handles pub update process errors', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { final errorProcessResult = ProcessResult( 42, 1, @@ -115,8 +112,7 @@ void main() { test( 'updates when newer version exists', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { when( () => pubUpdater.getLatestVersion(any()), ).thenAnswer((_) async => latestVersion); @@ -143,8 +139,7 @@ void main() { test( 'does not update when already on latest version', - withRunner( - (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + withRunner((commandRunner, logger, pubUpdater, printLogs) async { when( () => pubUpdater.getLatestVersion(any()), ).thenAnswer((_) async => packageVersion); diff --git a/test/src/pub_license/fixtures/generate_pub_license_fixtures.dart b/test/src/pub_license/fixtures/generate_pub_license_fixtures.dart deleted file mode 100644 index 476b70fe..00000000 --- a/test/src/pub_license/fixtures/generate_pub_license_fixtures.dart +++ /dev/null @@ -1,74 +0,0 @@ -/// A small script used to generate the fixture for the pub_license test. -/// -/// Fixtures are simply a temporary snapshot of an HTML response from pub.dev. -/// The generated fixtures allow testing pub_license scraping logic without -/// making a request to pub.dev every time the test is run. -/// -/// To run this script, use the following command: -/// ```bash -/// dart test/src/pub_license/fixtures/generate_pub_license_fixtures.dart -/// ``` -/// -/// Or simply use the "Run" CodeLens from VSCode's Dart extension if running -/// from VSCode. -library generate_pub_license_fixtures; - -// ignore_for_file: avoid_print - -import 'dart:io'; - -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; - -/// [Uri] used to test the case where a package has a single license. -final _singleLicenseUri = Uri.parse( - 'https://pub.dev/packages/very_good_cli/license', -); - -/// [Uri] used to test the case where a package has multiple licenses. -final _multipleLicenseUri = Uri.parse( - 'https://pub.dev/packages/just_audio/license', -); - -/// [Uri] used to test the case where a package has no license. -final _noLicenseUri = Uri.parse( - 'https://pub.dev/packages/music_control_notification/license', -); - -Future main() async { - final fixtureUris = { - 'singleLicense': _singleLicenseUri, - 'multipleLicense': _multipleLicenseUri, - 'noLicense': _noLicenseUri, - }; - - final httpClient = http.Client(); - - for (final entry in fixtureUris.entries) { - final name = entry.key; - final uri = entry.value; - - final response = await httpClient.get(uri); - - if (response.statusCode != 200) { - print( - '''Failed to generate a fixture for $name, received status code: ${response.statusCode}''', - ); - continue; - } - - final fixturePath = path.joinAll([ - Directory.current.path, - 'test', - 'src', - 'pub_license', - 'fixtures', - '$name.html', - ]); - File(fixturePath) - ..createSync(recursive: true) - ..writeAsStringSync(response.body); - - print('Fixture generated at $fixturePath'); - } -} diff --git a/test/src/pub_license/fixtures/multipleLicense.html b/test/src/pub_license/fixtures/multipleLicense.html deleted file mode 100644 index 1b3902bb..00000000 --- a/test/src/pub_license/fixtures/multipleLicense.html +++ /dev/null @@ -1,231 +0,0 @@ - -just_audio | Flutter Package
large Flutter Favorite logosmall Flutter Favorite logo

just_audio 0.9.35 icon indicating copy to clipboard operation
just_audio: ^0.9.35 copied to clipboard

A feature-rich audio player for Flutter. Loop, clip and concatenate any sound from any source (asset/file/URL/stream) in a variety of audio formats with gapless playback.

License

MIT License
-
-Copyright (c) 2019-2020 Ryan Heise and the project contributors.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-==============================================================================
-
-This software includes the ExoPlayer library which is licensed under the Apache
-License, Version 2.0.
-
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-
\ No newline at end of file diff --git a/test/src/pub_license/fixtures/noLicense.html b/test/src/pub_license/fixtures/noLicense.html deleted file mode 100644 index bd818452..00000000 --- a/test/src/pub_license/fixtures/noLicense.html +++ /dev/null @@ -1,3 +0,0 @@ - -music_control_notification | Flutter Package

music_control_notification 0.0.1+1 icon indicating copy to clipboard operation
music_control_notification: ^0.0.1+1 copied to clipboard

Android通知栏音乐控制

License

TODO: Add your license here.
-
\ No newline at end of file diff --git a/test/src/pub_license/fixtures/singleLicense.html b/test/src/pub_license/fixtures/singleLicense.html deleted file mode 100644 index f64e3a38..00000000 --- a/test/src/pub_license/fixtures/singleLicense.html +++ /dev/null @@ -1,22 +0,0 @@ - -very_good_cli | Dart Package

very_good_cli 0.16.0 icon indicating copy to clipboard operation
very_good_cli: ^0.16.0 copied to clipboard

A Very Good Command-Line Interface for Dart created by Very Good Ventures.

License

MIT License
-
-Copyright (c) 2021 Very Good Ventures
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file diff --git a/test/src/pub_license/pub_license_test.dart b/test/src/pub_license/pub_license_test.dart deleted file mode 100644 index 9b346e18..00000000 --- a/test/src/pub_license/pub_license_test.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'dart:io'; - -import 'package:html/dom.dart' as html_dom; -import 'package:html/parser.dart' as html_parser; -import 'package:http/http.dart' as http; -import 'package:mocktail/mocktail.dart'; -import 'package:path/path.dart' as path; -import 'package:test/test.dart'; -import 'package:very_good_cli/src/pub_license/pub_license.dart'; - -class _MockClient extends Mock implements http.Client {} - -class _MockResponse extends Mock implements http.Response {} - -class _MockParseError extends Mock implements html_parser.ParseError { - @override - String toString({dynamic color}) => super.toString(); -} - -class _MockDocument extends Mock implements html_dom.Document {} - -class _MockElement extends Mock implements html_dom.Element {} - -void main() { - group('PubLicense', () { - late http.Client client; - late http.Response response; - - setUpAll(() { - registerFallbackValue(Uri.parse('https://vgv.dev/')); - }); - - setUp(() { - response = _MockResponse(); - when(() => response.statusCode).thenReturn(200); - when(() => response.body).thenReturn(''); - - client = _MockClient(); - when(() => client.get(any())).thenAnswer((_) async => response); - }); - - test('can be instantiated', () { - expect(PubLicense(), isA()); - }); - - group('getLicense', () { - group('returns as expected', () { - String fixturePath(String name) => path.joinAll([ - Directory.current.path, - 'test', - 'src', - 'pub_license', - 'fixtures', - '$name.html', - ]); - - test('when parsing a single license fixture', () async { - final fixture = File(fixturePath('singleLicense')).readAsStringSync(); - when(() => response.body).thenReturn(fixture); - - final pubLicense = PubLicense(client: client); - final license = await pubLicense.getLicense('very_good_cli'); - - expect(license.length, equals(1)); - expect(license.first, equals('MIT')); - }); - - test('when parsing a multiple license fixture', () async { - final fixture = - File(fixturePath('multipleLicense')).readAsStringSync(); - when(() => response.body).thenReturn(fixture); - - final pubLicense = PubLicense(client: client); - final license = await pubLicense.getLicense('just_audio'); - - expect(license.length, equals(2)); - expect(license.first, equals('Apache-2.0')); - expect(license.last, equals('MIT')); - }); - - test('when parsing a no license fixture', () async { - final fixture = File(fixturePath('noLicense')).readAsStringSync(); - when(() => response.body).thenReturn(fixture); - - final pubLicense = PubLicense(client: client); - final license = await pubLicense.getLicense( - 'music_control_notification', - ); - - expect(license.length, equals(1)); - expect(license.first, equals('unknown')); - }); - }); - - group('throws a PubLicenseException', () { - test('when statusCode is not 200', () async { - when(() => response.statusCode).thenReturn(404); - - final pubLicense = PubLicense(client: client); - - final errorMessage = - '''[pub_license] Failed to retrieve the license of the package, received status code: ${response.statusCode}'''; - await expectLater( - () => pubLicense.getLicense('very_good_cli'), - throwsA( - isA().having( - (exception) => exception.message, - 'message', - equals(errorMessage), - ), - ), - ); - }); - - group('when parsing fails', () { - test('with a ParseError', () async { - final parseError = _MockParseError(); - final pubLicense = PubLicense( - client: client, - parse: (input, {encoding, generateSpans = true, sourceUrl}) => - throw parseError, - ); - - final errorMessage = - '''[pub_license] Failed to parse the response body, received error: $parseError'''; - await expectLater( - () => pubLicense.getLicense('very_good_cli'), - throwsA( - isA().having( - (exception) => exception.message, - 'message', - equals(errorMessage), - ), - ), - ); - }); - - test('with an unexpected error', () async { - const error = 'unexpected error'; - final pubLicense = PubLicense( - client: client, - parse: (input, {encoding, generateSpans = true, sourceUrl}) => - // ignore: only_throw_errors - throw error, - ); - - const errorMessage = - '''[pub_license] An unknown error occurred when trying to parse the response body, received error: $error'''; - await expectLater( - () => pubLicense.getLicense('very_good_cli'), - throwsA( - isA().having( - (exception) => exception.message, - 'message', - equals(errorMessage), - ), - ), - ); - }); - }); - - group('when scraping fails', () { - late html_dom.Document document; - late html_dom.Element element; - - setUp(() { - document = _MockDocument(); - element = _MockElement(); - }); - - test('due to missing `.detail-info-box`', () async { - when(() => document.querySelector('.detail-info-box')) - .thenReturn(null); - - final pubLicense = PubLicense( - client: client, - parse: (input, {encoding, generateSpans = true, sourceUrl}) => - document, - ); - - const errorMessage = - '''[pub_license] Failed to scrape license because `.detail-info-box` was not found.'''; - await expectLater( - () => pubLicense.getLicense('very_good_cli'), - throwsA( - isA().having( - (exception) => exception.message, - 'message', - equals(errorMessage), - ), - ), - ); - }); - - test('due to missing license header', () async { - when(() => document.querySelector('.detail-info-box')) - .thenReturn(element); - when(() => element.children).thenReturn([]); - - final pubLicense = PubLicense( - client: client, - parse: (input, {encoding, generateSpans = true, sourceUrl}) => - document, - ); - - const errorMessage = - '''[pub_license] Failed to scrape license because the license header was not found.'''; - await expectLater( - () => pubLicense.getLicense('very_good_cli'), - throwsA( - isA().having( - (exception) => exception.message, - 'message', - equals(errorMessage), - ), - ), - ); - }); - }); - }); - }); - }); -}