blob: f2e205e38019e195ea399b0797f5b9b37a30a283 [file] [log] [blame]
// Copyright 2014 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 'dart:async';
import 'package:args/command_runner.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../src/common.dart';
import '../src/context.dart';
import '../src/test_flutter_command_runner.dart';
void main() {
late Directory tempDir;
late Directory projectDir;
setUpAll(() async {
Cache.disableLocking();
await _ensureFlutterToolsSnapshot();
});
setUp(() {
tempDir = globals.fs.systemTempDirectory
.createTempSync('flutter_tools_generated_plugin_registrant_test.');
projectDir = tempDir.childDirectory('flutter_project');
});
tearDown(() {
tryToDelete(tempDir);
});
tearDownAll(() async {
await _restoreFlutterToolsSnapshot();
});
testUsingContext('generated plugin registrant passes analysis', () async {
await _createProject(projectDir, <String>[]);
// We need a dependency so the plugin registrant is not completely empty.
await _editPubspecFile(projectDir, _addDependencyEditor('shared_preferences',
version: '^2.0.0'));
// The plugin registrant is created on build...
await _buildWebProject(projectDir);
// Find the web_plugin_registrant, now that it lives outside "lib":
final Directory buildDir = projectDir
.childDirectory('.dart_tool/flutter_build')
.listSync()
.firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;
// Ensure the file exists, and passes analysis.
final File registrant = buildDir.childFile('web_plugin_registrant.dart');
expect(registrant, exists);
await _analyzeEntity(registrant);
// Ensure the contents match what we expect for a non-empty plugin registrant.
final String contents = registrant.readAsStringSync();
expect(contents, contains('// @dart = 2.13'));
expect(contents, contains("import 'package:shared_preferences_web/shared_preferences_web.dart';"));
expect(contents, contains('void registerPlugins([final Registrar? pluginRegistrar]) {'));
expect(contents, contains('SharedPreferencesPlugin.registerWith(registrar);'));
expect(contents, contains('registrar.registerMessageHandler();'));
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: globals.stdio,
),
});
testUsingContext('generated plugin registrant passes analysis without null safety', () async {
await _createProject(projectDir, <String>[]);
// We need a dependency so the plugin registrant is not completely empty.
await _editPubspecFile(projectDir,
_composeEditors(<PubspecEditor>[
_addDependencyEditor('shared_preferences', version: '^2.0.0'),
// This turns null safety off
_setDartSDKVersionEditor('>=2.11.0 <3.0.0'),
]));
// The generated main.dart file has a bunch of stuff that is invalid without null safety, so
// replace it with a no-op dummy main file. We aren't testing it in this scenario anyway.
await _replaceMainFile(projectDir, 'void main() {}');
// The plugin registrant is created on build...
await _buildWebProject(projectDir);
// Find the web_plugin_registrant, now that it lives outside "lib":
final Directory buildDir = projectDir
.childDirectory('.dart_tool/flutter_build')
.listSync()
.firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;
// Ensure the file exists, and passes analysis.
final File registrant = buildDir.childFile('web_plugin_registrant.dart');
expect(registrant, exists);
await _analyzeEntity(registrant);
// Ensure the contents match what we expect for a non-empty plugin registrant.
final String contents = registrant.readAsStringSync();
expect(contents, contains('// @dart = 2.13'));
expect(contents, contains("import 'package:shared_preferences_web/shared_preferences_web.dart';"));
expect(contents, contains('void registerPlugins([final Registrar? pluginRegistrar]) {'));
expect(contents, contains('SharedPreferencesPlugin.registerWith(registrar);'));
expect(contents, contains('registrar.registerMessageHandler();'));
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: globals.stdio,
),
});
testUsingContext('(no-op) generated plugin registrant passes analysis', () async {
await _createProject(projectDir, <String>[]);
// No dependencies on web plugins this time!
await _buildWebProject(projectDir);
// Find the web_plugin_registrant, now that it lives outside "lib":
final Directory buildDir = projectDir
.childDirectory('.dart_tool/flutter_build')
.listSync()
.firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;
// Ensure the file exists, and passes analysis.
final File registrant = buildDir.childFile('web_plugin_registrant.dart');
expect(registrant, exists);
await _analyzeEntity(registrant);
// Ensure the contents match what we expect for an empty (noop) plugin registrant.
final String contents = registrant.readAsStringSync();
expect(contents, contains('void registerPlugins() {}'));
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: globals.stdio,
),
});
// See: https://github.com/dart-lang/dart-services/pull/874
testUsingContext('generated plugin registrant for dartpad is created on pub get', () async {
await _createProject(projectDir, <String>[]);
await _editPubspecFile(projectDir,
_addDependencyEditor('shared_preferences', version: '^2.0.0'));
// The plugin registrant for dartpad is created on flutter pub get.
await _doFlutterPubGet(projectDir);
final File registrant = projectDir
.childDirectory('.dart_tool/dartpad')
.childFile('web_plugin_registrant.dart');
// Ensure the file exists, and passes analysis.
expect(registrant, exists);
await _analyzeEntity(registrant);
// Assert the full build hasn't happened!
final Directory buildDir = projectDir.childDirectory('.dart_tool/flutter_build');
expect(buildDir, isNot(exists));
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: globals.stdio,
),
});
testUsingContext(
'generated plugin registrant ignores lines longer than 80 chars',
() async {
await _createProject(projectDir, <String>[]);
await _addAnalysisOptions(
projectDir, <String>['lines_longer_than_80_chars']);
await _createProject(tempDir.childDirectory('test_plugin'), <String>[
'--template=plugin',
'--platforms=web',
'--project-name',
'test_web_plugin_with_a_purposefully_extremely_long_package_name',
]);
// The line for the test web plugin (` TestWebPluginWithAPurposefullyExtremelyLongPackageNameWeb.registerWith(registrar);`)
// exceeds 80 chars.
// With the above lint rule added, we want to ensure that the `generated_plugin_registrant.dart`
// file does not fail analysis (this is a regression test - an ignore was
// added to cover this case).
await _editPubspecFile(
projectDir,
_addDependencyEditor(
'test_web_plugin_with_a_purposefully_extremely_long_package_name',
path: '../test_plugin',
)
);
// The plugin registrant is only created after a build...
await _buildWebProject(projectDir);
// Find the web_plugin_registrant, now that it lives outside "lib":
final Directory buildDir = projectDir
.childDirectory('.dart_tool/flutter_build')
.listSync()
.firstWhere((FileSystemEntity entity) => entity is Directory) as Directory;
expect(
buildDir.childFile('web_plugin_registrant.dart'),
exists,
);
await _analyzeEntity(buildDir.childFile('web_plugin_registrant.dart'));
}, overrides: <Type, Generator>{
Pub: () => Pub(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: globals.stdio,
),
});
}
Future<void> _ensureFlutterToolsSnapshot() async {
final String flutterToolsPath = globals.fs.path.absolute(globals.fs.path.join(
'bin',
'flutter_tools.dart',
));
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
),
);
final String dotPackages = globals.fs.path.absolute(globals.fs.path.join(
'.dart_tool/package_config.json',
));
final File snapshotFile = globals.fs.file(flutterToolsSnapshotPath);
if (snapshotFile.existsSync()) {
snapshotFile.renameSync('$flutterToolsSnapshotPath.bak');
}
final List<String> snapshotArgs = <String>[
'--snapshot=$flutterToolsSnapshotPath',
'--packages=$dotPackages',
flutterToolsPath,
];
final ProcessResult snapshotResult = await Process.run(
'../../bin/cache/dart-sdk/bin/dart',
snapshotArgs,
);
printOnFailure('Output of dart ${snapshotArgs.join(" ")}:');
printOnFailure(snapshotResult.stdout.toString());
printOnFailure(snapshotResult.stderr.toString());
expect(snapshotResult.exitCode, 0);
}
Future<void> _restoreFlutterToolsSnapshot() async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
),
);
final File snapshotBackup =
globals.fs.file('$flutterToolsSnapshotPath.bak');
if (!snapshotBackup.existsSync()) {
// No backup to restore.
return;
}
snapshotBackup.renameSync(flutterToolsSnapshotPath);
}
Future<void> _createProject(Directory dir, List<String> createArgs) async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'create',
...createArgs,
dir.path,
]);
}
typedef PubspecEditor = void Function(List<String> pubSpecContents);
Future<void> _editPubspecFile(
Directory projectDir,
PubspecEditor editor,
) async {
final File pubspecYaml = projectDir.childFile('pubspec.yaml');
expect(pubspecYaml, exists);
final List<String> lines = await pubspecYaml.readAsLines();
editor(lines);
await pubspecYaml.writeAsString(lines.join('\n'));
}
Future<void> _replaceMainFile(Directory projectDir, String fileContents) async {
final File mainFile = projectDir.childDirectory('lib').childFile('main.dart');
await mainFile.writeAsString(fileContents);
}
PubspecEditor _addDependencyEditor(String packageToAdd, {String? version, String? path}) {
assert(version != null || path != null,
'Need to define a source for the package.');
assert(version == null || path == null,
'Cannot only load a package from path or from Pub, not both.');
void editor(List<String> lines) {
for (int i = 0; i < lines.length; i++) {
final String line = lines[i];
if (line.startsWith('dependencies:')) {
lines.insert(
i + 1,
' $packageToAdd: ${version ?? '\n'
' path: $path'}');
break;
}
}
}
return editor;
}
PubspecEditor _setDartSDKVersionEditor(String version) {
void editor(List<String> lines) {
for (int i = 0; i < lines.length; i++) {
final String line = lines[i];
if (line.startsWith('environment:')) {
for (i++; i < lines.length; i++) {
final String innerLine = lines[i];
final String sdkLine = " sdk: '$version'";
if(innerLine.isNotEmpty && !innerLine.startsWith(' ')) {
lines.insert(i, sdkLine);
break;
}
if(innerLine.startsWith(' sdk:')) {
lines[i] = sdkLine;
break;
}
}
break;
}
}
}
return editor;
}
PubspecEditor _composeEditors(Iterable<PubspecEditor> editors) {
void composedEditor(List<String> lines) {
for (final PubspecEditor editor in editors) {
editor(lines);
}
}
return composedEditor;
}
Future<void> _addAnalysisOptions(
Directory projectDir, List<String> linterRules) async {
assert(linterRules.isNotEmpty);
await projectDir.childFile('analysis_options.yaml').writeAsString('''
linter:
rules:
${linterRules.map((String rule) => ' - $rule').join('\n')}
''');
}
Future<void> _analyzeEntity(FileSystemEntity target) async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
),
);
final List<String> args = <String>[
flutterToolsSnapshotPath,
'analyze',
target.path,
];
final ProcessResult exec = await Process.run(
globals.artifacts!.getHostArtifact(HostArtifact.engineDartBinary).path,
args,
workingDirectory: target is Directory ? target.path : target.dirname,
);
printOnFailure('Output of flutter analyze:');
printOnFailure(exec.stdout.toString());
printOnFailure(exec.stderr.toString());
expect(exec.exitCode, 0);
}
Future<void> _buildWebProject(Directory workingDir) async {
return _runFlutterSnapshot(<String>['build', 'web'], workingDir);
}
Future<void> _doFlutterPubGet(Directory workingDir) async {
return _runFlutterSnapshot(<String>['pub', 'get'], workingDir);
}
// Runs a flutter command from a snapshot build.
// `flutterCommandArgs` are the arguments passed to flutter, like: ['build', 'web']
// to run `flutter build web`.
// `workingDir` is the directory on which the flutter command will be run.
Future<void> _runFlutterSnapshot(List<String> flutterCommandArgs, Directory workingDir) async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
),
);
final List<String> args = <String>[
flutterToolsSnapshotPath,
...flutterCommandArgs
];
final ProcessResult exec = await Process.run(
globals.artifacts!.getHostArtifact(HostArtifact.engineDartBinary).path,
args,
workingDirectory: workingDir.path,
);
printOnFailure('Output of flutter ${flutterCommandArgs.join(" ")}:');
printOnFailure(exec.stdout.toString());
printOnFailure(exec.stderr.toString());
expect(exec.exitCode, 0);
}