| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:async'; |
| 6 | |
| 7 | import 'package:args/command_runner.dart' ; |
| 8 | import 'package:file_testing/file_testing.dart' ; |
| 9 | import 'package:flutter_tools/src/artifacts.dart'; |
| 10 | import 'package:flutter_tools/src/base/file_system.dart'; |
| 11 | import 'package:flutter_tools/src/base/io.dart'; |
| 12 | import 'package:flutter_tools/src/build_info.dart'; |
| 13 | import 'package:flutter_tools/src/cache.dart'; |
| 14 | import 'package:flutter_tools/src/commands/create.dart'; |
| 15 | import 'package:flutter_tools/src/dart/pub.dart'; |
| 16 | import 'package:flutter_tools/src/globals.dart' as globals; |
| 17 | |
| 18 | import '../commands.shard/permeable/utils/project_testing_utils.dart'; |
| 19 | import '../src/common.dart'; |
| 20 | import '../src/context.dart'; |
| 21 | import '../src/test_flutter_command_runner.dart'; |
| 22 | import 'test_utils.dart'; |
| 23 | |
| 24 | void main() { |
| 25 | late Directory tempDir; |
| 26 | late Directory projectDir; |
| 27 | |
| 28 | setUpAll(() async { |
| 29 | Cache.disableLocking(); |
| 30 | await ensureFlutterToolsSnapshot(); |
| 31 | }); |
| 32 | |
| 33 | setUp(() { |
| 34 | tempDir = globals.fs.systemTempDirectory.createTempSync( |
| 35 | 'flutter_tools_generated_plugin_registrant_test.' , |
| 36 | ); |
| 37 | projectDir = tempDir.childDirectory('flutter_project' ); |
| 38 | }); |
| 39 | |
| 40 | tearDown(() { |
| 41 | tryToDelete(tempDir); |
| 42 | }); |
| 43 | |
| 44 | tearDownAll(() async { |
| 45 | await restoreFlutterToolsSnapshot(); |
| 46 | }); |
| 47 | |
| 48 | testUsingContext( |
| 49 | 'generated plugin registrant passes analysis' , |
| 50 | () async { |
| 51 | await _createProject(projectDir, <String>[]); |
| 52 | // We need a dependency so the plugin registrant is not completely empty. |
| 53 | await _editPubspecFile( |
| 54 | projectDir, |
| 55 | _addDependencyEditor('shared_preferences' , version: '^2.0.0' ), |
| 56 | ); |
| 57 | // The plugin registrant is created on build... |
| 58 | await _buildWebProject(projectDir); |
| 59 | |
| 60 | // Find the web_plugin_registrant, now that it lives outside "lib": |
| 61 | final buildDir = |
| 62 | projectDir |
| 63 | .childDirectory('.dart_tool/flutter_build' ) |
| 64 | .listSync() |
| 65 | .firstWhere((FileSystemEntity entity) => entity is Directory) |
| 66 | as Directory; |
| 67 | |
| 68 | // Ensure the file exists, and passes analysis. |
| 69 | final File registrant = buildDir.childFile('web_plugin_registrant.dart' ); |
| 70 | expect(registrant, exists); |
| 71 | await _analyzeEntity(registrant); |
| 72 | |
| 73 | // Ensure the contents match what we expect for a non-empty plugin registrant. |
| 74 | final String contents = registrant.readAsStringSync(); |
| 75 | expect(contents, contains('// @dart = 2.13')); |
| 76 | expect( |
| 77 | contents, |
| 78 | contains("import 'package:shared_preferences_web/shared_preferences_web.dart';" ), |
| 79 | ); |
| 80 | expect(contents, contains('void registerPlugins([final Registrar? pluginRegistrar]) {' )); |
| 81 | expect(contents, contains('SharedPreferencesPlugin.registerWith(registrar);' )); |
| 82 | expect(contents, contains('registrar.registerMessageHandler();' )); |
| 83 | }, |
| 84 | overrides: <Type, Generator>{ |
| 85 | Pub: () => Pub.test( |
| 86 | fileSystem: globals.fs, |
| 87 | logger: globals.logger, |
| 88 | processManager: globals.processManager, |
| 89 | botDetector: globals.botDetector, |
| 90 | platform: globals.platform, |
| 91 | stdio: globals.stdio, |
| 92 | ), |
| 93 | }, |
| 94 | ); |
| 95 | |
| 96 | testUsingContext( |
| 97 | 'generated plugin registrant passes analysis with null safety' , |
| 98 | () async { |
| 99 | await _createProject(projectDir, <String>[]); |
| 100 | // We need a dependency so the plugin registrant is not completely empty. |
| 101 | await _editPubspecFile( |
| 102 | projectDir, |
| 103 | _composeEditors(<PubspecEditor>[ |
| 104 | _addDependencyEditor('shared_preferences' , version: '^2.0.0' ), |
| 105 | |
| 106 | _setDartSDKVersionEditor('>=2.12.0 <4.0.0' ), |
| 107 | ]), |
| 108 | ); |
| 109 | |
| 110 | // Replace main file with a no-op dummy. We aren't testing it in this scenario anyway. |
| 111 | await _replaceMainFile(projectDir, 'void main() {}' ); |
| 112 | |
| 113 | // The plugin registrant is created on build... |
| 114 | await _buildWebProject(projectDir); |
| 115 | |
| 116 | // Find the web_plugin_registrant, now that it lives outside "lib": |
| 117 | final buildDir = |
| 118 | projectDir |
| 119 | .childDirectory('.dart_tool/flutter_build' ) |
| 120 | .listSync() |
| 121 | .firstWhere((FileSystemEntity entity) => entity is Directory) |
| 122 | as Directory; |
| 123 | |
| 124 | // Ensure the file exists, and passes analysis. |
| 125 | final File registrant = buildDir.childFile('web_plugin_registrant.dart' ); |
| 126 | expect(registrant, exists); |
| 127 | await _analyzeEntity(registrant); |
| 128 | |
| 129 | // Ensure the contents match what we expect for a non-empty plugin registrant. |
| 130 | final String contents = registrant.readAsStringSync(); |
| 131 | expect(contents, contains('// @dart = 2.13')); |
| 132 | expect(
|
| 133 | contents,
|
| 134 | contains("import 'package:shared_preferences_web/shared_preferences_web.dart';" ),
|
| 135 | );
|
| 136 | expect(contents, contains('void registerPlugins([final Registrar? pluginRegistrar]) {' ));
|
| 137 | expect(contents, contains('SharedPreferencesPlugin.registerWith(registrar);' ));
|
| 138 | expect(contents, contains('registrar.registerMessageHandler();' ));
|
| 139 | },
|
| 140 | overrides: <Type, Generator>{
|
| 141 | Pub: () => Pub.test(
|
| 142 | fileSystem: globals.fs,
|
| 143 | logger: globals.logger,
|
| 144 | processManager: globals.processManager,
|
| 145 | botDetector: globals.botDetector,
|
| 146 | platform: globals.platform,
|
| 147 | stdio: globals.stdio,
|
| 148 | ),
|
| 149 | },
|
| 150 | );
|
| 151 |
|
| 152 | testUsingContext(
|
| 153 | '(no-op) generated plugin registrant passes analysis' ,
|
| 154 | () async {
|
| 155 | await _createProject(projectDir, <String>[]);
|
| 156 | // No dependencies on web plugins this time!
|
| 157 | await _buildWebProject(projectDir);
|
| 158 |
|
| 159 | // Find the web_plugin_registrant, now that it lives outside "lib":
|
| 160 | final buildDir =
|
| 161 | projectDir
|
| 162 | .childDirectory('.dart_tool/flutter_build' )
|
| 163 | .listSync()
|
| 164 | .firstWhere((FileSystemEntity entity) => entity is Directory)
|
| 165 | as Directory;
|
| 166 |
|
| 167 | // Ensure the file exists, and passes analysis.
|
| 168 | final File registrant = buildDir.childFile('web_plugin_registrant.dart' );
|
| 169 | expect(registrant, exists);
|
| 170 | await _analyzeEntity(registrant);
|
| 171 |
|
| 172 | // Ensure the contents match what we expect for an empty (noop) plugin registrant.
|
| 173 | final String contents = registrant.readAsStringSync();
|
| 174 | expect(contents, contains('void registerPlugins() {}' ));
|
| 175 | },
|
| 176 | overrides: <Type, Generator>{
|
| 177 | Pub: () => Pub.test(
|
| 178 | fileSystem: globals.fs,
|
| 179 | logger: globals.logger,
|
| 180 | processManager: globals.processManager,
|
| 181 | botDetector: globals.botDetector,
|
| 182 | platform: globals.platform,
|
| 183 | stdio: globals.stdio,
|
| 184 | ),
|
| 185 | },
|
| 186 | );
|
| 187 |
|
| 188 | // See: https://github.com/dart-lang/dart-services/pull/874
|
| 189 | testUsingContext(
|
| 190 | 'generated plugin registrant for dartpad is created on pub get' ,
|
| 191 | () async {
|
| 192 | await _createProject(projectDir, <String>[]);
|
| 193 | await _editPubspecFile(
|
| 194 | projectDir,
|
| 195 | _addDependencyEditor('shared_preferences' , version: '^2.0.0' ),
|
| 196 | );
|
| 197 | // The plugin registrant for dartpad is created on flutter pub get.
|
| 198 | await _doFlutterPubGet(projectDir);
|
| 199 |
|
| 200 | final File registrant = projectDir
|
| 201 | .childDirectory('.dart_tool/dartpad' )
|
| 202 | .childFile('web_plugin_registrant.dart' );
|
| 203 |
|
| 204 | // Ensure the file exists, and passes analysis.
|
| 205 | expect(registrant, exists);
|
| 206 | await _analyzeEntity(registrant);
|
| 207 |
|
| 208 | // Assert the full build hasn't happened!
|
| 209 | final Directory buildDir = projectDir.childDirectory('.dart_tool/flutter_build' );
|
| 210 | expect(buildDir, isNot(exists));
|
| 211 | },
|
| 212 | overrides: <Type, Generator>{
|
| 213 | Pub: () => Pub.test(
|
| 214 | fileSystem: globals.fs,
|
| 215 | logger: globals.logger,
|
| 216 | processManager: globals.processManager,
|
| 217 | botDetector: globals.botDetector,
|
| 218 | platform: globals.platform,
|
| 219 | stdio: globals.stdio,
|
| 220 | ),
|
| 221 | },
|
| 222 | );
|
| 223 |
|
| 224 | testUsingContext(
|
| 225 | 'generated plugin registrant ignores lines longer than 80 chars' ,
|
| 226 | () async {
|
| 227 | await _createProject(projectDir, <String>[]);
|
| 228 | await _addAnalysisOptions(projectDir, <String>['lines_longer_than_80_chars' ]);
|
| 229 | await _createProject(tempDir.childDirectory('test_plugin' ), <String>[
|
| 230 | '--template=plugin' ,
|
| 231 | '--platforms=web' ,
|
| 232 | '--project-name' ,
|
| 233 | 'test_web_plugin_with_a_purposefully_extremely_long_package_name' ,
|
| 234 | ]);
|
| 235 | // The line for the test web plugin (` TestWebPluginWithAPurposefullyExtremelyLongPackageNameWeb.registerWith(registrar);`)
|
| 236 | // exceeds 80 chars.
|
| 237 | // With the above lint rule added, we want to ensure that the `generated_plugin_registrant.dart`
|
| 238 | // file does not fail analysis (this is a regression test - an ignore was
|
| 239 | // added to cover this case).
|
| 240 | await _editPubspecFile(
|
| 241 | projectDir,
|
| 242 | _addDependencyEditor(
|
| 243 | 'test_web_plugin_with_a_purposefully_extremely_long_package_name' ,
|
| 244 | path: '../test_plugin' ,
|
| 245 | ),
|
| 246 | );
|
| 247 | // The plugin registrant is only created after a build...
|
| 248 | await _buildWebProject(projectDir);
|
| 249 |
|
| 250 | // Find the web_plugin_registrant, now that it lives outside "lib":
|
| 251 | final buildDir =
|
| 252 | projectDir
|
| 253 | .childDirectory('.dart_tool/flutter_build' )
|
| 254 | .listSync()
|
| 255 | .firstWhere((FileSystemEntity entity) => entity is Directory)
|
| 256 | as Directory;
|
| 257 |
|
| 258 | expect(buildDir.childFile('web_plugin_registrant.dart' ), exists);
|
| 259 | await _analyzeEntity(buildDir.childFile('web_plugin_registrant.dart' ));
|
| 260 | },
|
| 261 | overrides: <Type, Generator>{
|
| 262 | Pub: () => Pub.test(
|
| 263 | fileSystem: globals.fs,
|
| 264 | logger: globals.logger,
|
| 265 | processManager: globals.processManager,
|
| 266 | botDetector: globals.botDetector,
|
| 267 | platform: globals.platform,
|
| 268 | stdio: globals.stdio,
|
| 269 | ),
|
| 270 | },
|
| 271 | );
|
| 272 | }
|
| 273 |
|
| 274 | Future<void> _createProject(Directory dir, List<String> createArgs) async {
|
| 275 | Cache.flutterRoot = '../..' ;
|
| 276 | final command = CreateCommand();
|
| 277 | final CommandRunner<void> runner = createTestCommandRunner(command);
|
| 278 | await runner.run(<String>['create' , ...createArgs, dir.path]);
|
| 279 | }
|
| 280 |
|
| 281 | typedef PubspecEditor = void Function(List<String> pubSpecContents);
|
| 282 |
|
| 283 | Future<void> _editPubspecFile(Directory projectDir, PubspecEditor editor) async {
|
| 284 | final File pubspecYaml = projectDir.childFile('pubspec.yaml' );
|
| 285 | expect(pubspecYaml, exists);
|
| 286 |
|
| 287 | final List<String> lines = await pubspecYaml.readAsLines();
|
| 288 | editor(lines);
|
| 289 | await pubspecYaml.writeAsString(lines.join('\n' ));
|
| 290 | }
|
| 291 |
|
| 292 | Future<void> _replaceMainFile(Directory projectDir, String fileContents) async {
|
| 293 | final File mainFile = projectDir.childDirectory('lib' ).childFile('main.dart' );
|
| 294 | await mainFile.writeAsString(fileContents);
|
| 295 | }
|
| 296 |
|
| 297 | PubspecEditor _addDependencyEditor(String packageToAdd, {String? version, String? path}) {
|
| 298 | assert(version != null || path != null, 'Need to define a source for the package.' );
|
| 299 | assert(
|
| 300 | version == null || path == null,
|
| 301 | 'Cannot only load a package from path or from Pub, not both.' ,
|
| 302 | );
|
| 303 | void editor(List<String> lines) {
|
| 304 | for (var i = 0; i < lines.length; i++) {
|
| 305 | final String line = lines[i];
|
| 306 | if (line.startsWith('dependencies:' )) {
|
| 307 | lines.insert(
|
| 308 | i + 1,
|
| 309 | ' $packageToAdd: ${version ?? '\n'
|
| 310 | ' path: $path' }' ,
|
| 311 | );
|
| 312 | break;
|
| 313 | }
|
| 314 | }
|
| 315 | }
|
| 316 |
|
| 317 | return editor;
|
| 318 | }
|
| 319 |
|
| 320 | PubspecEditor _setDartSDKVersionEditor(String version) {
|
| 321 | void editor(List<String> lines) {
|
| 322 | for (var i = 0; i < lines.length; i++) {
|
| 323 | final String line = lines[i];
|
| 324 | if (line.startsWith('environment:' )) {
|
| 325 | for (i++; i < lines.length; i++) {
|
| 326 | final String innerLine = lines[i];
|
| 327 | final sdkLine = " sdk: ' $version'" ;
|
| 328 | if (innerLine.isNotEmpty && !innerLine.startsWith(' ' )) {
|
| 329 | lines.insert(i, sdkLine);
|
| 330 | break;
|
| 331 | }
|
| 332 | if (innerLine.startsWith(' sdk:' )) {
|
| 333 | lines[i] = sdkLine;
|
| 334 | break;
|
| 335 | }
|
| 336 | }
|
| 337 | break;
|
| 338 | }
|
| 339 | }
|
| 340 | }
|
| 341 |
|
| 342 | return editor;
|
| 343 | }
|
| 344 |
|
| 345 | PubspecEditor _composeEditors(Iterable<PubspecEditor> editors) {
|
| 346 | void composedEditor(List<String> lines) {
|
| 347 | for (final editor in editors) {
|
| 348 | editor(lines);
|
| 349 | }
|
| 350 | }
|
| 351 |
|
| 352 | return composedEditor;
|
| 353 | }
|
| 354 |
|
| 355 | Future<void> _addAnalysisOptions(Directory projectDir, List<String> linterRules) async {
|
| 356 | assert(linterRules.isNotEmpty);
|
| 357 |
|
| 358 | await projectDir.childFile('analysis_options.yaml' ).writeAsString('''
|
| 359 | linter:
|
| 360 | rules:
|
| 361 | ${linterRules.map((String rule) => ' - $rule' ).join('\n' )}
|
| 362 | ''' );
|
| 363 | }
|
| 364 |
|
| 365 | Future<void> _analyzeEntity(FileSystemEntity target) async {
|
| 366 | final String flutterToolsSnapshotPath = globals.fs.path.absolute(
|
| 367 | globals.fs.path.join('..' , '..' , 'bin' , 'cache' , 'flutter_tools.snapshot' ),
|
| 368 | );
|
| 369 |
|
| 370 | final args = <String>[flutterToolsSnapshotPath, 'analyze' , target.path];
|
| 371 |
|
| 372 | final ProcessResult exec = await Process.run(
|
| 373 | globals.artifacts!.getArtifactPath(
|
| 374 | Artifact.engineDartBinary,
|
| 375 | platform: TargetPlatform.web_javascript,
|
| 376 | ),
|
| 377 | args,
|
| 378 | workingDirectory: target is Directory ? target.path : target.dirname,
|
| 379 | );
|
| 380 | expect(exec, const ProcessResultMatcher());
|
| 381 | }
|
| 382 |
|
| 383 | Future<void> _buildWebProject(Directory workingDir) async {
|
| 384 | return _runFlutterSnapshot(<String>['build' , 'web' ], workingDir);
|
| 385 | }
|
| 386 |
|
| 387 | Future<void> _doFlutterPubGet(Directory workingDir) async {
|
| 388 | return _runFlutterSnapshot(<String>['pub' , 'get' ], workingDir);
|
| 389 | }
|
| 390 |
|
| 391 | // Runs a flutter command from a snapshot build.
|
| 392 | // `flutterCommandArgs` are the arguments passed to flutter, like: ['build', 'web']
|
| 393 | // to run `flutter build web`.
|
| 394 | // `workingDir` is the directory on which the flutter command will be run.
|
| 395 | Future<void> _runFlutterSnapshot(List<String> flutterCommandArgs, Directory workingDir) async {
|
| 396 | final String flutterToolsSnapshotPath = globals.fs.path.absolute(
|
| 397 | globals.fs.path.join('..' , '..' , 'bin' , 'cache' , 'flutter_tools.snapshot' ),
|
| 398 | );
|
| 399 |
|
| 400 | final args = <String>[
|
| 401 | globals.artifacts!.getArtifactPath(
|
| 402 | Artifact.engineDartBinary,
|
| 403 | platform: TargetPlatform.web_javascript,
|
| 404 | ),
|
| 405 | flutterToolsSnapshotPath,
|
| 406 | ...flutterCommandArgs,
|
| 407 | ];
|
| 408 |
|
| 409 | final ProcessResult exec = await globals.processManager.run(
|
| 410 | args,
|
| 411 | workingDirectory: workingDir.path,
|
| 412 | );
|
| 413 | expect(exec, const ProcessResultMatcher());
|
| 414 | }
|
| 415 |
|