Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cbc6c9b
feat: include hidden check licenses command
alestiago Oct 4, 2023
bd6e631
coverage
alestiago Oct 5, 2023
9dfc476
typo
alestiago Oct 5, 2023
2c416f7
analyzer
alestiago Oct 5, 2023
6eeb7a4
Merge branch 'main' into alestiago/include-hidden-check-command
alestiago Oct 5, 2023
5b9fe36
feat: allow fetching licenses
alestiago Oct 5, 2023
31cc3a0
full stop
alestiago Oct 5, 2023
d13d6dc
_isHostedDirectDependency
alestiago Oct 6, 2023
5cf0b0b
Merge branch 'main' into alestiago/licenses-fetch
alestiago Oct 6, 2023
924c51b
Merge remote-tracking branch 'origin' into alestiago/licenses-fetch
alestiago Oct 9, 2023
fe5a5dd
testing
alestiago Oct 9, 2023
19414b1
licenses and packages singular
alestiago Oct 9, 2023
5f0af9d
included TODOs
alestiago Oct 9, 2023
d434f6f
test progress
alestiago Oct 10, 2023
225837c
Merge branch 'main' into alestiago/licenses-fetch
alestiago Oct 10, 2023
71c965b
refactor to "dependencyName"
alestiago Oct 10, 2023
754728b
refactor _tryParsePubspecLock
alestiago Oct 10, 2023
d88047d
remove old ignore
alestiago Oct 10, 2023
ba9c203
missing cancel
alestiago Oct 10, 2023
ad09a3c
remove commented code
alestiago Oct 10, 2023
4da6306
words
alestiago Oct 10, 2023
6d5a23d
words
alestiago Oct 10, 2023
cf25b30
removed argResults override
alestiago Oct 10, 2023
94657b5
feat: allow ignoring failures when checking licenses
alestiago Oct 10, 2023
3a03531
used const
alestiago Oct 10, 2023
eec1abf
testing
alestiago Oct 10, 2023
928e699
more tests and fixes
alestiago Oct 10, 2023
6fac91b
test progress update
alestiago Oct 10, 2023
f75c0d5
removed TODO
alestiago Oct 10, 2023
b1c49e4
refactor _composeReport
alestiago Oct 10, 2023
3b97198
usage exception
alestiago Oct 10, 2023
4c42853
Merge branch 'alestiago/licenses-fetch' into alestiago/licenses-ignor…
alestiago Oct 10, 2023
cd321eb
format
alestiago Oct 10, 2023
d3318bb
Merge remote-tracking branch 'origin' into alestiago/licenses-ignore-…
alestiago Oct 11, 2023
0b49949
update flag description
alestiago Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 53 additions & 26 deletions lib/src/commands/packages/commands/check/commands/licenses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ class PackagesCheckLicensesCommand extends Command<int> {
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;

Expand All @@ -45,6 +51,8 @@ class PackagesCheckLicensesCommand extends Command<int> {
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);

Expand Down Expand Up @@ -73,44 +81,41 @@ class PackagesCheckLicensesCommand extends Command<int> {
return ExitCode.usage.code;
}

final licenses = <String, Set<String>>{};
final licenses = <String, Set<String>?>{};
for (final dependency in filteredDependencies) {
progress.update(
'Collecting licenses of ${licenses.length}/${filteredDependencies.length} packages',
);

final dependencyName = dependency.package();
Set<String> rawLicense;
Set<String>? 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(
<String>{},
(previousValue, element) => previousValue..addAll(element),
);
final licenseCount = licenses.values.fold<int>(
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;
}
Expand All @@ -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<String, Set<String>?> licenses) {
final licenseTypes =
licenses.values.fold(<String>{}, (previousValue, element) {
if (element == null) return previousValue;
return previousValue..addAll(element);
});
final licenseCount = licenses.values.fold<int>(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<Object> {
String stringify() {
if (isEmpty) return '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
];
Expand Down Expand Up @@ -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<String>();

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',
Expand Down