diff --git a/lib/src/commands/packages/commands/check/commands/licenses.dart b/lib/src/commands/packages/commands/check/commands/licenses.dart index 8c325b0eb..439e83d42 100644 --- a/lib/src/commands/packages/commands/check/commands/licenses.dart +++ b/lib/src/commands/packages/commands/check/commands/licenses.dart @@ -21,7 +21,13 @@ class PackagesCheckLicensesCommand extends Command { Logger? logger, PubLicense? pubLicense, }) : _logger = logger ?? Logger(), - _pubLicense = pubLicense ?? PubLicense(); + _pubLicense = pubLicense ?? PubLicense() { + argParser.addFlag( + 'ignore-failures', + help: 'Ignore any license that failed to be retrieved.', + negatable: false, + ); + } final Logger _logger; @@ -45,6 +51,8 @@ class PackagesCheckLicensesCommand extends Command { usageException('Too many arguments'); } + final ignoreFailures = _argResults['ignore-failures'] as bool; + final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.'; final targetPath = path.normalize(Directory(target).absolute.path); @@ -73,44 +81,41 @@ class PackagesCheckLicensesCommand extends Command { return ExitCode.usage.code; } - final licenses = >{}; + final licenses = ?>{}; for (final dependency in filteredDependencies) { progress.update( 'Collecting licenses of ${licenses.length}/${filteredDependencies.length} packages', ); final dependencyName = dependency.package(); - Set rawLicense; + Set? rawLicense; try { rawLicense = await _pubLicense.getLicense(dependencyName); } on PubLicenseException catch (e) { - progress.cancel(); - _logger.err('[$dependencyName] ${e.message}'); - return ExitCode.unavailable.code; + final errorMessage = '[$dependencyName] ${e.message}'; + if (!ignoreFailures) { + progress.cancel(); + _logger.err(errorMessage); + return ExitCode.unavailable.code; + } + + _logger.err('\n$errorMessage'); } catch (e) { - progress.cancel(); - _logger.err('[$dependencyName] Unexpected failure with error: $e'); - return ExitCode.software.code; + final errorMessage = + '[$dependencyName] Unexpected failure with error: $e'; + if (!ignoreFailures) { + progress.cancel(); + _logger.err(errorMessage); + return ExitCode.software.code; + } + + _logger.err('\n$errorMessage'); + } finally { + licenses[dependencyName] = rawLicense; } - - licenses[dependencyName] = rawLicense; } - final licenseTypes = licenses.values.fold( - {}, - (previousValue, element) => previousValue..addAll(element), - ); - final licenseCount = licenses.values.fold( - 0, - (previousValue, element) => previousValue + element.length, - ); - - final licenseWord = licenseCount == 1 ? 'license' : 'licenses'; - final packageWord = - filteredDependencies.length == 1 ? 'package' : 'packages'; - progress.complete( - '''Retrieved $licenseCount $licenseWord from ${filteredDependencies.length} $packageWord of type: ${licenseTypes.toList().stringify()}.''', - ); + progress.complete(_composeReport(licenses)); return ExitCode.success.code; } @@ -137,6 +142,28 @@ bool _isHostedDirectDependency( return isPubHostedDependency && isDirectDependency; } +/// Composes a human friendly [String] to report the result of the retrieved +/// licenses. +String _composeReport(Map?> licenses) { + final licenseTypes = + licenses.values.fold({}, (previousValue, element) { + if (element == null) return previousValue; + return previousValue..addAll(element); + }); + final licenseCount = licenses.values.fold(0, (previousValue, element) { + if (element == null) return previousValue; + return previousValue + element.length; + }); + + final licenseWord = licenseCount == 1 ? 'license' : 'licenses'; + final packageWord = licenses.length == 1 ? 'package' : 'packages'; + final suffix = licenseTypes.isEmpty + ? '' + : ' of type: ${licenseTypes.toList().stringify()}'; + + return '''Retrieved $licenseCount $licenseWord from ${licenses.length} $packageWord$suffix.'''; +} + extension on List { String stringify() { if (isEmpty) return ''; 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 15bda6f97..226aad7a3 100644 --- a/test/src/commands/packages/commands/check/commands/licenses_test.dart +++ b/test/src/commands/packages/commands/check/commands/licenses_test.dart @@ -16,7 +16,8 @@ const _expectedPackagesCheckLicensesUsage = [ 'Check packages licenses in a Dart or Flutter project.\n' '\n' 'Usage: very_good packages check licenses [arguments]\n' - '-h, --help Print this usage information.\n' + '-h, --help Print this usage information.\n' + ''' --ignore-failures Ignore any license that failed to be retrieved.\n''' '\n' 'Run "very_good help" to see global options.' ]; @@ -138,6 +139,150 @@ void main() { }, ); + group('ignore-failures', () { + const ignoreFailuresArgument = '--ignore-failures'; + + group('reports licenses', () { + test( + 'when a PubLicenseException is thrown', + withRunner( + (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validMultiplePubspecLockContent); + + when(() => logger.progress(any())).thenReturn(progress); + + when(() => pubLicense.getLicense(any())).thenAnswer( + (_) => Future.value({'MIT'}), + ); + const failedDependencyName = 'very_good_test_runner'; + const exception = PubLicenseException('message'); + when(() => pubLicense.getLicense(failedDependencyName)) + .thenThrow(exception); + + final result = await commandRunner.run( + [...commandArguments, ignoreFailuresArgument, tempDirectory.path], + ); + + final errorMessage = + '''\n[$failedDependencyName] ${exception.message}'''; + verify(() => logger.err(errorMessage)).called(1); + + verify( + () => progress.update('Collecting licenses of 0/2 packages'), + ).called(1); + verify( + () => progress.update('Collecting licenses of 1/2 packages'), + ).called(1); + verify( + () => progress.complete( + 'Retrieved 1 license from 2 packages of type: MIT.', + ), + ).called(1); + + expect(result, equals(ExitCode.success.code)); + }), + ); + + test( + 'when an unknown error is thrown', + withRunner( + (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validMultiplePubspecLockContent); + + when(() => logger.progress(any())).thenReturn(progress); + + when(() => pubLicense.getLicense(any())).thenAnswer( + (_) => Future.value({'MIT'}), + ); + const failedDependencyName = 'very_good_test_runner'; + const error = 'error'; + when(() => pubLicense.getLicense(failedDependencyName)) + .thenThrow(error); + + final result = await commandRunner.run( + [...commandArguments, ignoreFailuresArgument, tempDirectory.path], + ); + + const errorMessage = + '''\n[$failedDependencyName] Unexpected failure with error: $error'''; + verify(() => logger.err(errorMessage)).called(1); + + verify( + () => progress.update('Collecting licenses of 0/2 packages'), + ).called(1); + verify( + () => progress.update('Collecting licenses of 1/2 packages'), + ).called(1); + verify( + () => progress.complete( + 'Retrieved 1 license from 2 packages of type: MIT.', + ), + ).called(1); + + expect(result, equals(ExitCode.success.code)); + }), + ); + }); + + test( + 'when all licenses fail to be retrieved', + withRunner( + (commandRunner, logger, pubUpdater, pubLicense, printLogs) async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + File(path.join(tempDirectory.path, pubspecLockBasename)) + .writeAsStringSync(_validMultiplePubspecLockContent); + + when(() => logger.progress(any())).thenReturn(progress); + + const error = 'error'; + when(() => pubLicense.getLicense(any())).thenThrow(error); + + final result = await commandRunner.run( + [...commandArguments, ignoreFailuresArgument, tempDirectory.path], + ); + + final packageNames = verify(() => pubLicense.getLicense(captureAny())) + .captured + .cast(); + + 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); + + verify( + () => progress.update('Collecting licenses of 0/2 packages'), + ).called(1); + verify( + () => progress.update('Collecting licenses of 1/2 packages'), + ).called(1); + verify( + () => progress.complete( + 'Retrieved 0 licenses from 2 packages.', + ), + ).called(1); + + expect(result, equals(ExitCode.success.code)); + }), + ); + }); + group('exits with error', () { test( 'when it did not find a pubspec.lock file at the target path',