| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:args/command_runner.dart'; |
| import 'package:file/file.dart'; |
| import 'package:file/memory.dart'; |
| import 'package:flutter_plugin_tools/src/common/core.dart'; |
| import 'package:flutter_plugin_tools/src/license_check_command.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:platform/platform.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'common/package_command_test.mocks.dart'; |
| import 'mocks.dart'; |
| import 'util.dart'; |
| |
| void main() { |
| group('LicenseCheckCommand', () { |
| late CommandRunner<void> runner; |
| late FileSystem fileSystem; |
| late Platform platform; |
| late Directory root; |
| |
| setUp(() { |
| fileSystem = MemoryFileSystem(); |
| platform = MockPlatformWithSeparator(); |
| final Directory packagesDir = |
| fileSystem.currentDirectory.childDirectory('packages'); |
| root = packagesDir.parent; |
| |
| final MockGitDir gitDir = MockGitDir(); |
| when(gitDir.path).thenReturn(packagesDir.parent.path); |
| |
| final LicenseCheckCommand command = LicenseCheckCommand( |
| packagesDir, |
| platform: platform, |
| gitDir: gitDir, |
| ); |
| runner = |
| CommandRunner<void>('license_test', 'Test for $LicenseCheckCommand'); |
| runner.addCommand(command); |
| }); |
| |
| /// Writes a copyright+license block to [file], defaulting to a standard |
| /// block for this repository. |
| /// |
| /// [commentString] is added to the start of each line. |
| /// [prefix] is added to the start of the entire block. |
| /// [suffix] is added to the end of the entire block. |
| void writeLicense( |
| File file, { |
| String comment = '// ', |
| String prefix = '', |
| String suffix = '', |
| String copyright = |
| 'Copyright 2013 The Flutter Authors. All rights reserved.', |
| List<String> license = const <String>[ |
| 'Use of this source code is governed by a BSD-style license that can be', |
| 'found in the LICENSE file.', |
| ], |
| bool useCrlf = false, |
| }) { |
| final List<String> lines = <String>['$prefix$comment$copyright']; |
| for (final String line in license) { |
| lines.add('$comment$line'); |
| } |
| final String newline = useCrlf ? '\r\n' : '\n'; |
| file.writeAsStringSync(lines.join(newline) + suffix + newline); |
| } |
| |
| test('looks at only expected extensions', () async { |
| final Map<String, bool> extensions = <String, bool>{ |
| 'c': true, |
| 'cc': true, |
| 'cpp': true, |
| 'dart': true, |
| 'h': true, |
| 'html': true, |
| 'java': true, |
| 'json': false, |
| 'kt': true, |
| 'm': true, |
| 'md': false, |
| 'mm': true, |
| 'png': false, |
| 'swift': true, |
| 'sh': true, |
| 'yaml': false, |
| }; |
| |
| const String filenameBase = 'a_file'; |
| for (final String fileExtension in extensions.keys) { |
| root.childFile('$filenameBase.$fileExtension').createSync(); |
| } |
| |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| // Ignore failure; the files are empty so the check is expected to fail, |
| // but this test isn't for that behavior. |
| }); |
| |
| extensions.forEach((String fileExtension, bool shouldCheck) { |
| final Matcher logLineMatcher = |
| contains('Checking $filenameBase.$fileExtension'); |
| expect(output, shouldCheck ? logLineMatcher : isNot(logLineMatcher)); |
| }); |
| }); |
| |
| test('ignore list overrides extension matches', () async { |
| final List<String> ignoredFiles = <String>[ |
| // Ignored base names. |
| 'flutter_export_environment.sh', |
| 'GeneratedPluginRegistrant.java', |
| 'GeneratedPluginRegistrant.m', |
| 'generated_plugin_registrant.cc', |
| 'generated_plugin_registrant.cpp', |
| // Ignored path suffixes. |
| 'foo.g.dart', |
| 'foo.mocks.dart', |
| // Ignored files. |
| 'resource.h', |
| ]; |
| |
| for (final String name in ignoredFiles) { |
| root.childFile(name).createSync(); |
| } |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| for (final String name in ignoredFiles) { |
| expect(output, isNot(contains('Checking $name'))); |
| } |
| }); |
| |
| test('ignores submodules', () async { |
| const String submoduleName = 'a_submodule'; |
| |
| final File submoduleSpec = root.childFile('.gitmodules'); |
| submoduleSpec.writeAsStringSync(''' |
| [submodule "$submoduleName"] |
| path = $submoduleName |
| url = https://github.com/foo/$submoduleName |
| '''); |
| |
| const List<String> submoduleFiles = <String>[ |
| '$submoduleName/foo.dart', |
| '$submoduleName/a/b/bar.dart', |
| '$submoduleName/LICENSE', |
| ]; |
| for (final String filePath in submoduleFiles) { |
| root.childFile(filePath).createSync(recursive: true); |
| } |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| for (final String filePath in submoduleFiles) { |
| expect(output, isNot(contains('Checking $filePath'))); |
| } |
| }); |
| |
| test('passes if all checked files have license blocks', () async { |
| final File checked = root.childFile('checked.cc'); |
| checked.createSync(); |
| writeLicense(checked); |
| final File notChecked = root.childFile('not_checked.md'); |
| notChecked.createSync(); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check a file. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking checked.cc'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('passes correct license blocks on Windows', () async { |
| final File checked = root.childFile('checked.cc'); |
| checked.createSync(); |
| writeLicense(checked, useCrlf: true); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check a file. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking checked.cc'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('handles the comment styles for all supported languages', () async { |
| final File fileA = root.childFile('file_a.cc'); |
| fileA.createSync(); |
| writeLicense(fileA); |
| final File fileB = root.childFile('file_b.sh'); |
| fileB.createSync(); |
| writeLicense(fileB, comment: '# '); |
| final File fileC = root.childFile('file_c.html'); |
| fileC.createSync(); |
| writeLicense(fileC, comment: '', prefix: '<!-- ', suffix: ' -->'); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check the files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking file_a.cc'), |
| contains('Checking file_b.sh'), |
| contains('Checking file_c.html'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('fails if any checked files are missing license blocks', () async { |
| final File goodA = root.childFile('good.cc'); |
| goodA.createSync(); |
| writeLicense(goodA); |
| final File goodB = root.childFile('good.h'); |
| goodB.createSync(); |
| writeLicense(goodB); |
| root.childFile('bad.cc').createSync(); |
| root.childFile('bad.h').createSync(); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| // Failure should give information about the problematic files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'The license block for these files is missing or incorrect:'), |
| contains(' bad.cc'), |
| contains(' bad.h'), |
| ])); |
| // Failure shouldn't print the success message. |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('fails if any checked files are missing just the copyright', () async { |
| final File good = root.childFile('good.cc'); |
| good.createSync(); |
| writeLicense(good); |
| final File bad = root.childFile('bad.cc'); |
| bad.createSync(); |
| writeLicense(bad, copyright: ''); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| // Failure should give information about the problematic files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'The license block for these files is missing or incorrect:'), |
| contains(' bad.cc'), |
| ])); |
| // Failure shouldn't print the success message. |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('fails if any checked files are missing just the license', () async { |
| final File good = root.childFile('good.cc'); |
| good.createSync(); |
| writeLicense(good); |
| final File bad = root.childFile('bad.cc'); |
| bad.createSync(); |
| writeLicense(bad, license: <String>[]); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| // Failure should give information about the problematic files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'The license block for these files is missing or incorrect:'), |
| contains(' bad.cc'), |
| ])); |
| // Failure shouldn't print the success message. |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('fails if any third-party code is not in a third_party directory', |
| () async { |
| final File thirdPartyFile = root.childFile('third_party.cc'); |
| thirdPartyFile.createSync(); |
| writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| // Failure should give information about the problematic files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'The license block for these files is missing or incorrect:'), |
| contains(' third_party.cc'), |
| ])); |
| // Failure shouldn't print the success message. |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('succeeds for third-party code in a third_party directory', () async { |
| final File thirdPartyFile = root |
| .childDirectory('a_plugin') |
| .childDirectory('lib') |
| .childDirectory('src') |
| .childDirectory('third_party') |
| .childFile('file.cc'); |
| thirdPartyFile.createSync(recursive: true); |
| writeLicense(thirdPartyFile, |
| copyright: 'Copyright 2017 Workiva Inc.', |
| license: <String>[ |
| 'Licensed under the Apache License, Version 2.0 (the "License");', |
| 'you may not use this file except in compliance with the License.' |
| ]); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check the file. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking a_plugin/lib/src/third_party/file.cc'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('allows first-party code in a third_party directory', () async { |
| final File firstPartyFileInThirdParty = root |
| .childDirectory('a_plugin') |
| .childDirectory('lib') |
| .childDirectory('src') |
| .childDirectory('third_party') |
| .childFile('first_party.cc'); |
| firstPartyFileInThirdParty.createSync(recursive: true); |
| writeLicense(firstPartyFileInThirdParty); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check the file. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking a_plugin/lib/src/third_party/first_party.cc'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('fails for licenses that the tool does not expect', () async { |
| final File good = root.childFile('good.cc'); |
| good.createSync(); |
| writeLicense(good); |
| final File bad = root.childDirectory('third_party').childFile('bad.cc'); |
| bad.createSync(recursive: true); |
| writeLicense(bad, license: <String>[ |
| 'This program is free software: you can redistribute it and/or modify', |
| 'it under the terms of the GNU General Public License', |
| ]); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| // Failure should give information about the problematic files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'No recognized license was found for the following third-party files:'), |
| contains(' third_party/bad.cc'), |
| ])); |
| // Failure shouldn't print the success message. |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('Apache is not recognized for new authors without validation changes', |
| () async { |
| final File good = root.childFile('good.cc'); |
| good.createSync(); |
| writeLicense(good); |
| final File bad = root.childDirectory('third_party').childFile('bad.cc'); |
| bad.createSync(recursive: true); |
| writeLicense( |
| bad, |
| copyright: 'Copyright 2017 Some New Authors.', |
| license: <String>[ |
| 'Licensed under the Apache License, Version 2.0 (the "License");', |
| 'you may not use this file except in compliance with the License.' |
| ], |
| ); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| // Failure should give information about the problematic files. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'No recognized license was found for the following third-party files:'), |
| contains(' third_party/bad.cc'), |
| ])); |
| // Failure shouldn't print the success message. |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('passes if all first-party LICENSE files are correctly formatted', |
| () async { |
| final File license = root.childFile('LICENSE'); |
| license.createSync(); |
| license.writeAsStringSync(_correctLicenseFileText); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check the file. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking LICENSE'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('passes correct LICENSE files on Windows', () async { |
| final File license = root.childFile('LICENSE'); |
| license.createSync(); |
| license |
| .writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n')); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // Sanity check that the test did actually check the file. |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking LICENSE'), |
| contains('All files passed validation!'), |
| ])); |
| }); |
| |
| test('fails if any first-party LICENSE files are incorrectly formatted', |
| () async { |
| final File license = root.childFile('LICENSE'); |
| license.createSync(); |
| license.writeAsStringSync(_incorrectLicenseFileText); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect(output, isNot(contains(contains('All files passed validation!')))); |
| }); |
| |
| test('ignores third-party LICENSE format', () async { |
| final File license = |
| root.childDirectory('third_party').childFile('LICENSE'); |
| license.createSync(recursive: true); |
| license.writeAsStringSync(_incorrectLicenseFileText); |
| |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['license-check']); |
| |
| // The file shouldn't be checked. |
| expect(output, isNot(contains(contains('Checking third_party/LICENSE')))); |
| }); |
| |
| test('outputs all errors at the end', () async { |
| root.childFile('bad.cc').createSync(); |
| root |
| .childDirectory('third_party') |
| .childFile('bad.cc') |
| .createSync(recursive: true); |
| final File license = root.childFile('LICENSE'); |
| license.createSync(); |
| license.writeAsStringSync(_incorrectLicenseFileText); |
| |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['license-check'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Checking LICENSE'), |
| contains('Checking bad.cc'), |
| contains('Checking third_party/bad.cc'), |
| contains( |
| 'The following LICENSE files do not follow the expected format:'), |
| contains(' LICENSE'), |
| contains( |
| 'The license block for these files is missing or incorrect:'), |
| contains(' bad.cc'), |
| contains( |
| 'No recognized license was found for the following third-party files:'), |
| contains(' third_party/bad.cc'), |
| ])); |
| }); |
| }); |
| } |
| |
| class MockPlatformWithSeparator extends MockPlatform { |
| @override |
| String get pathSeparator => isWindows ? r'\' : '/'; |
| } |
| |
| const String _correctLicenseFileText = ''' |
| Copyright 2013 The Flutter Authors. All rights reserved. |
| |
| Redistribution and use in source and binary forms, with or without modification, |
| are permitted provided that the following conditions are met: |
| |
| * Redistributions of source code must retain the above copyright |
| notice, this list of conditions and the following disclaimer. |
| * Redistributions in binary form must reproduce the above |
| copyright notice, this list of conditions and the following |
| disclaimer in the documentation and/or other materials provided |
| with the distribution. |
| * Neither the name of Google Inc. nor the names of its |
| contributors may be used to endorse or promote products derived |
| from this software without specific prior written permission. |
| |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
| ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR |
| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| '''; |
| |
| // A common incorrect version created by copying text intended for a code file, |
| // with comment markers. |
| const String _incorrectLicenseFileText = ''' |
| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| '''; |