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
5import 'dart:async';
6
7import 'package:args/command_runner.dart';
8import 'package:file_testing/file_testing.dart';
9import 'package:flutter_tools/src/artifacts.dart';
10import 'package:flutter_tools/src/base/file_system.dart';
11import 'package:flutter_tools/src/base/io.dart';
12import 'package:flutter_tools/src/build_info.dart';
13import 'package:flutter_tools/src/cache.dart';
14import 'package:flutter_tools/src/commands/create.dart';
15import 'package:flutter_tools/src/dart/pub.dart';
16import 'package:flutter_tools/src/globals.dart' as globals;
17
18import '../commands.shard/permeable/utils/project_testing_utils.dart';
19import '../src/common.dart';
20import '../src/context.dart';
21import '../src/test_flutter_command_runner.dart';
22import 'test_utils.dart';
23
24void 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
274Future<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
281typedef PubspecEditor = void Function(List<String> pubSpecContents);
282
283Future<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
292Future<void> _replaceMainFile(Directory projectDir, String fileContents) async {
293 final File mainFile = projectDir.childDirectory('lib').childFile('main.dart');
294 await mainFile.writeAsString(fileContents);
295}
296
297PubspecEditor _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
320PubspecEditor _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
345PubspecEditor _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
355Future<void> _addAnalysisOptions(Directory projectDir, List<String> linterRules) async {
356 assert(linterRules.isNotEmpty);
357
358 await projectDir.childFile('analysis_options.yaml').writeAsString('''
359linter:
360 rules:
361${linterRules.map((String rule) => ' - $rule').join('\n')}
362 ''');
363}
364
365Future<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
383Future<void> _buildWebProject(Directory workingDir) async {
384 return _runFlutterSnapshot(<String>['build', 'web'], workingDir);
385}
386
387Future<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.
395Future<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