// 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 _addDependency(projectDir, '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("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, ), }); 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, ), }); // 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 _addDependency(projectDir, '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, ), }); 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 _addDependency( projectDir, '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, ), }); } 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, ]); } Future<void> _addDependency( Directory projectDir, String package, { String? version, String? path, }) async { 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.'); final File pubspecYaml = projectDir.childFile('pubspec.yaml'); expect(pubspecYaml, exists); final List<String> lines = await pubspecYaml.readAsLines(); for (int i = 0; i < lines.length; i++) { final String line = lines[i]; if (line.startsWith('dependencies:')) { lines.insert( i + 1, ' $package: ${version ?? '\n' ' path: $path'}'); break; } } await pubspecYaml.writeAsString(lines.join('\n')); } 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); }