Files
packages/script/tool/test/pubspec_check_command_test.dart
stuartmorgan b4985e25fe [all] Add topics to pubspecs (#4771)
Adds [topics](https://dart.dev/tools/pub/pubspec#topics) to all
packages, supporting the new pub feature for categorizing packages. The
heuristics I used were:
- Try to use existing topics from https://pub.dev/topics where
applicable
- Add new topics as necessary to cover things that seemed like obvious
relevant topics
- Include the plugin name as a topic for all federated plugin packages,
for grouping (since pub doesn't inherently group or cross-link
implementations)

This is not an attempt to be exhaustive; as topics evolve I expect we
will add more or adjust.

Also updates the repo tooling to enforce topics, so that we don't forget
to add them to new packages. The enforced rule is:
- All packages must have at least one topic. We could potentially change
this to allow an empty `topics` section so that we are enforcing that we
didn't just forget to add the section, but in practice even for packages
that we don't expect people to be likely to use, I didn't have any issue
coming up with at least one relevant topic.
- Federated plugin packages must contain the plugin name as a topic.

While this isn't time-critical, I chose to include version bumps so that
we aren't rolling out topics in a piecemeal way (e.g., with only a
random subset of a federated plugin's packages having topics on pub.dev
based on what has happened to have a bugfix).
2023-08-29 13:31:23 -04:00

1524 lines
45 KiB
Dart

// 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/pubspec_check_command.dart';
import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
/// Returns the top section of a pubspec.yaml for a package named [name].
///
/// By default it will create a header that includes all of the expected
/// values, elements can be changed via arguments to create incorrect
/// entries.
///
/// If [includeRepository] is true, by default the path in the link will
/// be "packages/[name]"; a different "packages"-relative path can be
/// provided with [repositoryPackagesDirRelativePath].
String _headerSection(
String name, {
String repository = 'flutter/packages',
bool includeRepository = true,
String repositoryBranch = 'main',
String? repositoryPackagesDirRelativePath,
bool includeHomepage = false,
bool includeIssueTracker = true,
bool publishable = true,
String? description,
}) {
final String repositoryPath = repositoryPackagesDirRelativePath ?? name;
final List<String> repoLinkPathComponents = <String>[
repository,
'tree',
repositoryBranch,
'packages',
repositoryPath,
];
final String repoLink =
'https://github.com/${repoLinkPathComponents.join('/')}';
final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?'
'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22';
description ??= 'A test package for validating that the pubspec.yaml '
'follows repo best practices.';
return '''
name: $name
description: $description
${includeRepository ? 'repository: $repoLink' : ''}
${includeHomepage ? 'homepage: $repoLink' : ''}
${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''}
version: 1.0.0
${publishable ? '' : "publish_to: 'none'"}
''';
}
String _environmentSection({
String dartConstraint = '>=2.17.0 <4.0.0',
String? flutterConstraint = '>=3.0.0',
}) {
return <String>[
'environment:',
' sdk: "$dartConstraint"',
if (flutterConstraint != null) ' flutter: "$flutterConstraint"',
'',
].join('\n');
}
String _flutterSection({
bool isPlugin = false,
String? implementedPackage,
Map<String, Map<String, String>> pluginPlatformDetails =
const <String, Map<String, String>>{},
}) {
String pluginEntry = '''
plugin:
${implementedPackage == null ? '' : ' implements: $implementedPackage'}
platforms:
''';
for (final MapEntry<String, Map<String, String>> platform
in pluginPlatformDetails.entries) {
pluginEntry += '''
${platform.key}:
''';
for (final MapEntry<String, String> detail in platform.value.entries) {
pluginEntry += '''
${detail.key}: ${detail.value}
''';
}
}
return '''
flutter:
${isPlugin ? pluginEntry : ''}
''';
}
String _dependenciesSection(
[List<String> extraDependencies = const <String>[]]) {
return '''
dependencies:
flutter:
sdk: flutter
${extraDependencies.map((String dep) => ' $dep').join('\n')}
''';
}
String _devDependenciesSection(
[List<String> extraDependencies = const <String>[]]) {
return '''
dev_dependencies:
flutter_test:
sdk: flutter
${extraDependencies.map((String dep) => ' $dep').join('\n')}
''';
}
String _topicsSection([List<String> topics = const <String>['a-topic']]) {
return '''
topics:
${topics.map((String topic) => ' - $topic').join('\n')}
''';
}
String _falseSecretsSection() {
return '''
false_secrets:
- /lib/main.dart
''';
}
void main() {
group('test pubspec_check_command', () {
late CommandRunner<void> runner;
late RecordingProcessRunner processRunner;
late FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
setUp(() {
fileSystem = MemoryFileSystem();
mockPlatform = MockPlatform();
packagesDir = fileSystem.currentDirectory.childDirectory('packages');
createPackagesDirectory(parentDir: packagesDir.parent);
processRunner = RecordingProcessRunner();
final PubspecCheckCommand command = PubspecCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
);
runner = CommandRunner<void>(
'pubspec_check_command', 'Test for pubspec_check_command');
runner.addCommand(command);
});
test('passes for a plugin following conventions', () async {
final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection()}
${_falseSecretsSection()}
''');
plugin.getExamples().first.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin_example',
publishable: false,
includeRepository: false,
includeIssueTracker: false,
)}
${_environmentSection()}
${_dependenciesSection()}
${_flutterSection()}
''');
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for plugin...'),
contains('Running for plugin/example...'),
contains('No issues found!'),
]),
);
});
test('passes for a Flutter package following conventions', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection()}
${_devDependenciesSection()}
${_flutterSection()}
${_topicsSection()}
${_falseSecretsSection()}
''');
package.getExamples().first.pubspecFile.writeAsStringSync('''
${_headerSection(
'a_package',
publishable: false,
includeRepository: false,
includeIssueTracker: false,
)}
${_environmentSection()}
${_dependenciesSection()}
${_flutterSection()}
''');
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('Running for a_package/example...'),
contains('No issues found!'),
]),
);
});
test('passes for a minimal package following conventions', () async {
final RepositoryPackage package =
createFakePackage('package', packagesDir, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('package')}
${_environmentSection()}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package...'),
contains('No issues found!'),
]),
);
});
test('fails when homepage is included', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', includeHomepage: true)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Found a "homepage" entry; only "repository" should be used.'),
]),
);
});
test('fails when repository is missing', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', includeRepository: false)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Missing "repository"'),
]),
);
});
test('fails when homepage is given instead of repository', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', includeHomepage: true, includeRepository: false)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Found a "homepage" entry; only "repository" should be used.'),
]),
);
});
test('fails when repository package name is incorrect', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', repositoryPackagesDirRelativePath: 'different_plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The "repository" link should end with the package path.'),
]),
);
});
test('fails when repository uses master instead of main', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', repositoryBranch: 'master')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The "repository" link should start with the repository\'s '
'main tree: "https://github.com/flutter/packages/tree/main"'),
]),
);
});
test('fails when repository is not flutter/packages', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', repository: 'flutter/plugins')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The "repository" link should start with the repository\'s '
'main tree: "https://github.com/flutter/packages/tree/main"'),
]),
);
});
test('fails when issue tracker is missing', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', includeIssueTracker: false)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('A package should have an "issue_tracker" link'),
]),
);
});
test('fails when description is too short', () async {
final RepositoryPackage plugin = createFakePlugin(
'a_plugin', packagesDir.childDirectory('a_plugin'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', description: 'Too short')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('"description" is too short. pub.dev recommends package '
'descriptions of 60-180 characters.'),
]),
);
});
test(
'allows short descriptions for non-app-facing parts of federated plugins',
() async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', description: 'Too short')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('"description" is too short. pub.dev recommends package '
'descriptions of 60-180 characters.'),
]),
);
});
test('fails when description is too long', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
const String description = 'This description is too long. It just goes '
'on and on and on and on and on. pub.dev will down-score it because '
'there is just too much here. Someone shoul really cut this down to just '
'the core description so that search results are more useful and the '
'package does not lose pub points.';
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin', description: description)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('"description" is too long. pub.dev recommends package '
'descriptions of 60-180 characters.'),
]),
);
});
test('fails when topics section is missing', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('A published package should include "topics".'),
]),
);
});
test('fails when topics section is empty', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>[])}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('A published package should include "topics".'),
]),
);
});
test('fails when federated plugin topics do not include plugin name',
() async {
final RepositoryPackage plugin = createFakePlugin(
'some_plugin_ios', packagesDir.childDirectory('some_plugin'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'A federated plugin package should include its plugin name as a topic. '
'Add "some-plugin" to the "topics" section.'),
]),
);
});
test('fails when environment section is out of order', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_environmentSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Major sections should follow standard repository ordering:'),
]),
);
});
test('fails when flutter section is out of order', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_flutterSection(isPlugin: true)}
${_environmentSection()}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Major sections should follow standard repository ordering:'),
]),
);
});
test('fails when dependencies section is out of order', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_devDependenciesSection()}
${_dependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Major sections should follow standard repository ordering:'),
]),
);
});
test('fails when dev_dependencies section is out of order', () async {
final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_devDependenciesSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Major sections should follow standard repository ordering:'),
]),
);
});
test('fails when false_secrets section is out of order', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_falseSecretsSection()}
${_devDependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Major sections should follow standard repository ordering:'),
]),
);
});
test('fails when an implemenation package is missing "implements"',
() async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a_foo', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin_a_foo')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Missing "implements: plugin_a" in "plugin" section.'),
]),
);
});
test('fails when an implemenation package has the wrong "implements"',
() async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a_foo', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin_a_foo')}
${_environmentSection()}
${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Expecetd "implements: plugin_a"; '
'found "implements: plugin_a_foo".'),
]),
);
});
test('passes for a correct implemenation package', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a_foo', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin_a_foo',
repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo',
)}
${_environmentSection()}
${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a')}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin-a'])}
''');
final List<String> output =
await runCapturingPrint(runner, <String>['pubspec-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for plugin_a_foo...'),
contains('No issues found!'),
]),
);
});
test('fails when a "default_package" looks incorrect', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin_a',
repositoryPackagesDirRelativePath: 'plugin_a/plugin_a',
)}
${_environmentSection()}
${_flutterSection(
isPlugin: true,
pluginPlatformDetails: <String, Map<String, String>>{
'android': <String, String>{'default_package': 'plugin_b_android'}
},
)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'"plugin_b_android" is not an expected implementation name for "plugin_a"'),
]),
);
});
test(
'fails when a "default_package" does not have a corresponding dependency',
() async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin_a',
repositoryPackagesDirRelativePath: 'plugin_a/plugin_a',
)}
${_environmentSection()}
${_flutterSection(
isPlugin: true,
pluginPlatformDetails: <String, Map<String, String>>{
'android': <String, String>{'default_package': 'plugin_a_android'}
},
)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following default_packages are missing corresponding '
'dependencies:\n plugin_a_android'),
]),
);
});
test('passes for an app-facing package without "implements"', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin_a',
repositoryPackagesDirRelativePath: 'plugin_a/plugin_a',
)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin-a'])}
''');
final List<String> output =
await runCapturingPrint(runner, <String>['pubspec-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for plugin_a/plugin_a...'),
contains('No issues found!'),
]),
);
});
test('passes for a platform interface package without "implements"',
() async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
plugin.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin_a_platform_interface',
repositoryPackagesDirRelativePath:
'plugin_a/plugin_a_platform_interface',
)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin-a'])}
''');
final List<String> output =
await runCapturingPrint(runner, <String>['pubspec-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for plugin_a_platform_interface...'),
contains('No issues found!'),
]),
);
});
test('validates some properties even for unpublished packages', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin_a_foo', packagesDir.childDirectory('plugin_a'),
examples: <String>[]);
// Environment section is in the wrong location.
// Missing 'implements'.
plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin_a_foo', publishable: false)}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_environmentSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Major sections should follow standard repository ordering:'),
contains('Missing "implements: plugin_a" in "plugin" section.'),
]),
);
});
test('ignores some checks for unpublished packages', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
// Missing metadata that is only useful for published packages, such as
// repository and issue tracker.
plugin.pubspecFile.writeAsStringSync('''
${_headerSection(
'plugin',
publishable: false,
includeRepository: false,
includeIssueTracker: false,
)}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
''');
final List<String> output =
await runCapturingPrint(runner, <String>['pubspec-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for plugin...'),
contains('No issues found!'),
]),
);
});
test('fails when a Flutter package has a too-low minimum Flutter version',
() async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
isFlutter: true, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(flutterConstraint: '>=2.10.0')}
${_dependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
'--min-min-flutter-version',
'3.0.0'
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Minimum allowed Flutter version 2.10.0 is less than 3.0.0'),
]),
);
});
test(
'passes when a Flutter package requires exactly the minimum Flutter version',
() async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
isFlutter: true, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(flutterConstraint: '>=3.3.0', dartConstraint: '>=2.18.0 <4.0.0')}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner,
<String>['pubspec-check', '--min-min-flutter-version', '3.3.0']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test(
'passes when a Flutter package requires a higher minimum Flutter version',
() async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
isFlutter: true, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(flutterConstraint: '>=3.7.0', dartConstraint: '>=2.19.0 <4.0.0')}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner,
<String>['pubspec-check', '--min-min-flutter-version', '3.3.0']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('fails when a non-Flutter package has a too-low minimum Dart version',
() async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(dartConstraint: '>=2.14.0 <4.0.0', flutterConstraint: null)}
${_dependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
'--min-min-flutter-version',
'3.0.0'
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Minimum allowed Dart version 2.14.0 is less than 2.17.0'),
]),
);
});
test(
'passes when a non-Flutter package requires exactly the minimum Dart version',
() async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
isFlutter: true, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(dartConstraint: '>=2.18.0 <4.0.0', flutterConstraint: null)}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner,
<String>['pubspec-check', '--min-min-flutter-version', '3.3.0']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test(
'passes when a non-Flutter package requires a higher minimum Dart version',
() async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
isFlutter: true, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(dartConstraint: '>=2.18.0 <4.0.0', flutterConstraint: null)}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner,
<String>['pubspec-check', '--min-min-flutter-version', '3.0.0']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('fails when a Flutter->Dart SDK version mapping is missing', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
'--min-min-flutter-version',
'2.0.0'
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Dart SDK version for Fluter SDK version 2.0.0 is unknown'),
]),
);
});
test(
'fails when a Flutter package has a too-low minimum Dart version for '
'the corresponding minimum Flutter version', () async {
final RepositoryPackage package = createFakePackage(
'a_package', packagesDir,
isFlutter: true, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection(flutterConstraint: '>=3.3.0', dartConstraint: '>=2.16.0 <4.0.0')}
${_dependenciesSection()}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The minimum Dart version is 2.16.0, but the '
'minimum Flutter version of 3.3.0 shipped with '
'Dart 2.18.0. Please use consistent lower SDK '
'bounds'),
]),
);
});
group('dependency check', () {
test('passes for local dependencies', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
final RepositoryPackage dependencyPackage =
createFakePackage('local_dependency', packagesDir);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection(<String>['local_dependency: ^1.0.0'])}
${_topicsSection()}
''');
dependencyPackage.pubspecFile.writeAsStringSync('''
${_headerSection('local_dependency')}
${_environmentSection()}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output =
await runCapturingPrint(runner, <String>['pubspec-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('fails when an unexpected dependency is found', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection(<String>['bad_dependency: ^1.0.0'])}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'The following unexpected non-local dependencies were found:\n'
' bad_dependency\n'
'Please see https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#Dependencies '
'for more information and next steps.'),
]),
);
});
test('fails when an unexpected dev dependency is found', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection()}
${_devDependenciesSection(<String>['bad_dependency: ^1.0.0'])}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'The following unexpected non-local dependencies were found:\n'
' bad_dependency\n'
'Please see https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#Dependencies '
'for more information and next steps.'),
]),
);
});
test('passes when a dependency is on the allow list', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection(<String>['allowed: ^1.0.0'])}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner,
<String>['pubspec-check', '--allow-dependencies', 'allowed']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('passes when a pinned dependency is on the pinned allow list',
() async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection(<String>['allow_pinned: 1.0.0'])}
${_topicsSection()}
''');
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
'--allow-pinned-dependencies',
'allow_pinned'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for a_package...'),
contains('No issues found!'),
]),
);
});
test('fails when an allowed-when-pinned dependency is unpinned',
() async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.pubspecFile.writeAsStringSync('''
${_headerSection('a_package')}
${_environmentSection()}
${_dependenciesSection(<String>['allow_pinned: ^1.0.0'])}
${_topicsSection()}
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'pubspec-check',
'--allow-pinned-dependencies',
'allow_pinned'
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'The following unexpected non-local dependencies were found:\n'
' allow_pinned\n'
'Please see https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#Dependencies '
'for more information and next steps.'),
]),
);
});
});
});
group('test pubspec_check_command on Windows', () {
late CommandRunner<void> runner;
late RecordingProcessRunner processRunner;
late FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
setUp(() {
fileSystem = MemoryFileSystem(style: FileSystemStyle.windows);
mockPlatform = MockPlatform(isWindows: true);
packagesDir = fileSystem.currentDirectory.childDirectory('packages');
createPackagesDirectory(parentDir: packagesDir.parent);
processRunner = RecordingProcessRunner();
final PubspecCheckCommand command = PubspecCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
);
runner = CommandRunner<void>(
'pubspec_check_command', 'Test for pubspec_check_command');
runner.addCommand(command);
});
test('repository check works', () async {
final RepositoryPackage package =
createFakePackage('package', packagesDir, examples: <String>[]);
package.pubspecFile.writeAsStringSync('''
${_headerSection('package')}
${_environmentSection()}
${_dependenciesSection()}
${_topicsSection()}
''');
final List<String> output =
await runCapturingPrint(runner, <String>['pubspec-check']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package...'),
contains('No issues found!'),
]),
);
});
});
}