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// Runs the tests for the flutter/flutter repository.
6//
7// By default, test output is filtered and only errors are shown. (If a
8// particular test takes longer than _quietTimeout in utils.dart, the output is
9// shown then also, in case something has hung.)
10//
11// --verbose stops the output cleanup and just outputs everything verbatim.
12//
13// By default, errors are non-fatal; all tests are executed and the output
14// ends with a summary of the errors that were detected.
15//
16// Exit code is 1 if there was an error.
17//
18// --abort-on-error causes the script to exit immediately when hitting an error.
19//
20// By default, all tests are run. However, the tests support being split by
21// shard and subshard. Inspect the code to see what shards and subshards are
22// supported, or run with `--dry-run` to get a list of tests that _would_ have
23// been executed.
24//
25// For local testing you can just set the SHARD and SUBSHARD environment
26// variables. For example, to run all the framework tests you can just set
27// SHARD=framework_tests. Some shards support named subshards, like
28// SHARD=framework_tests SUBSHARD=widgets. Others support arbitrary numbered
29// subsharding, like SHARD=build_tests SUBSHARD=1_2 (where 1_2 means "one of
30// two" as in run the first half of the tests).
31//
32// So for example to run specifically the third subshard of the Web tests you
33// would set SHARD=web_tests SUBSHARD=2 (it's zero-based).
34//
35// By default, where supported, tests within a shard are executed in a random
36// order to (eventually) catch inter-test dependencies.
37//
38// --test-randomize-ordering-seed= sets the shuffle seed for reproducing runs.
39//
40// All other arguments are treated as arguments to pass to the flutter tool when
41// running tests.
42
43import 'dart:convert';
44import 'dart:core' as system show print;
45import 'dart:core' hide print;
46import 'dart:io' as system show exit;
47import 'dart:io' hide exit;
48import 'dart:math' as math;
49
50import 'package:path/path.dart' as path;
51
52import 'run_command.dart';
53import 'suite_runners/run_add_to_app_life_cycle_tests.dart';
54import 'suite_runners/run_analyze_tests.dart';
55import 'suite_runners/run_android_engine_tests.dart';
56import 'suite_runners/run_android_java11_integration_tool_tests.dart';
57import 'suite_runners/run_android_preview_integration_tool_tests.dart';
58import 'suite_runners/run_customer_testing_tests.dart';
59import 'suite_runners/run_docs_tests.dart';
60import 'suite_runners/run_flutter_packages_tests.dart';
61import 'suite_runners/run_framework_coverage_tests.dart';
62import 'suite_runners/run_framework_tests.dart';
63import 'suite_runners/run_fuchsia_precache.dart';
64import 'suite_runners/run_skp_generator_tests.dart';
65import 'suite_runners/run_test_harness_tests.dart';
66import 'suite_runners/run_verify_binaries_codesigned_tests.dart';
67import 'suite_runners/run_web_tests.dart';
68import 'utils.dart';
69
70typedef ShardRunner = Future<void> Function();
71
72/// Environment variables to override the local engine when running `pub test`,
73/// if such flags are provided to `test.dart`.
74final Map<String, String> localEngineEnv = <String, String>{};
75
76/// When you call this, you can pass additional arguments to pass custom
77/// arguments to flutter test. For example, you might want to call this
78/// script with the parameter --local-engine=host_debug_unopt to
79/// use your own build of the engine.
80///
81/// To run the tool_tests part, run it with SHARD=tool_tests
82///
83/// Examples:
84/// SHARD=tool_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart
85/// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt --local-engine-host=host_debug_unopt
86Future<void> main(List<String> args) async {
87 try {
88 printProgress('STARTING ANALYSIS');
89 bool dryRunArgSet = false;
90 for (final String arg in args) {
91 if (arg.startsWith('--local-engine=')) {
92 localEngineEnv['FLUTTER_LOCAL_ENGINE'] = arg.substring('--local-engine='.length);
93 flutterTestArgs.add(arg);
94 } else if (arg.startsWith('--local-engine-host=')) {
95 localEngineEnv['FLUTTER_LOCAL_ENGINE_HOST'] = arg.substring('--local-engine-host='.length);
96 flutterTestArgs.add(arg);
97 } else if (arg.startsWith('--local-engine-src-path=')) {
98 localEngineEnv['FLUTTER_LOCAL_ENGINE_SRC_PATH'] = arg.substring(
99 '--local-engine-src-path='.length,
100 );
101 flutterTestArgs.add(arg);
102 } else if (arg.startsWith('--test-randomize-ordering-seed=')) {
103 shuffleSeed = arg.substring('--test-randomize-ordering-seed='.length);
104 } else if (arg.startsWith('--verbose')) {
105 print = (Object? message) {
106 system.print(message);
107 };
108 } else if (arg.startsWith('--abort-on-error')) {
109 onError = () {
110 system.exit(1);
111 };
112 } else if (arg == '--dry-run') {
113 dryRunArgSet = true;
114 printProgress('--dry-run enabled. Tests will not actually be executed.');
115 } else {
116 flutterTestArgs.add(arg);
117 }
118 }
119 if (dryRunArgSet) {
120 enableDryRun();
121 }
122 final WebTestsSuite webTestsSuite = WebTestsSuite(flutterTestArgs);
123 await selectShard(<String, ShardRunner>{
124 'add_to_app_life_cycle_tests': addToAppLifeCycleRunner,
125 'build_tests': _runBuildTests,
126 'framework_coverage': frameworkCoverageRunner,
127 'framework_tests': frameworkTestsRunner,
128 'tool_tests': _runToolTests,
129 'web_tool_tests': _runWebToolTests,
130 'tool_integration_tests': _runIntegrationToolTests,
131 'android_preview_tool_integration_tests': androidPreviewIntegrationToolTestsRunner,
132 'android_java11_tool_integration_tests': androidJava11IntegrationToolTestsRunner,
133 'tool_host_cross_arch_tests': _runToolHostCrossArchTests,
134 // All the unit/widget tests run using `flutter test --platform=chrome`
135 'web_canvaskit_tests': webTestsSuite.runWebCanvasKitUnitTests,
136 // All the unit/widget tests run using `flutter test --platform=chrome --wasm`
137 'web_skwasm_tests': webTestsSuite.runWebSkwasmUnitTests,
138 // All web integration tests
139 'web_long_running_tests': webTestsSuite.webLongRunningTestsRunner,
140 'android_engine_vulkan_tests': () =>
141 runAndroidEngineTests(impellerBackend: ImpellerBackend.vulkan),
142 'android_engine_opengles_tests': () =>
143 runAndroidEngineTests(impellerBackend: ImpellerBackend.opengles),
144 'flutter_plugins': flutterPackagesRunner,
145 'skp_generator': skpGeneratorTestsRunner,
146 'customer_testing': customerTestingRunner,
147 'analyze': analyzeRunner,
148 'fuchsia_precache': fuchsiaPrecacheRunner,
149 'snippets': _runSnippetsTests,
150 'docs': docsRunner,
151 'verify_binaries_codesigned': verifyCodesignedTestRunner,
152 'verify_binaries_pre_codesigned': verifyPreCodesignedTestRunner,
153 kTestHarnessShardName:
154 testHarnessTestsRunner, // Used for testing this script; also run as part of SHARD=framework_tests, SUBSHARD=misc.
155 });
156 } catch (error, stackTrace) {
157 foundError(<String>[
158 'UNEXPECTED ERROR!',
159 error.toString(),
160 ...stackTrace.toString().split('\n'),
161 'The test.dart script should be corrected to catch this error and call foundError().',
162 '${yellow}Some tests are likely to have been skipped.$reset',
163 ]);
164 system.exit(255);
165 }
166 if (hasError) {
167 reportErrorsAndExit('${bold}Test failed.$reset');
168 }
169 reportSuccessAndExit('${bold}Test successful.$reset');
170}
171
172final String _toolsPath = path.join(flutterRoot, 'packages', 'flutter_tools');
173
174Future<void> _runGeneralToolTests() async {
175 await runDartTest(
176 _toolsPath,
177 testPaths: <String>[path.join('test', 'general.shard')],
178 enableFlutterToolAsserts: false,
179
180 // Detect unit test time regressions (poor time delay handling, etc).
181 // This overrides the 15 minute default for tools tests.
182 // See the README.md and dart_test.yaml files in the flutter_tools package.
183 perTestTimeout: const Duration(seconds: 2),
184 );
185}
186
187Future<void> _runCommandsToolTests() async {
188 await runDartTest(
189 _toolsPath,
190 forceSingleCore: true,
191 testPaths: <String>[path.join('test', 'commands.shard')],
192 );
193}
194
195Future<void> _runWebToolTests() async {
196 final List<File> allFiles = Directory(
197 path.join(_toolsPath, 'test', 'web.shard'),
198 ).listSync(recursive: true).whereType<File>().toList();
199 final List<String> allTests = <String>[];
200 for (final File file in allFiles) {
201 if (file.path.endsWith('_test.dart')) {
202 allTests.add(file.path);
203 }
204 }
205 await runDartTest(
206 _toolsPath,
207 forceSingleCore: true,
208 testPaths: selectIndexOfTotalSubshard<String>(allTests),
209 includeLocalEngineEnv: true,
210 );
211}
212
213Future<void> _runToolHostCrossArchTests() {
214 return runDartTest(
215 _toolsPath,
216 // These are integration tests
217 forceSingleCore: true,
218 testPaths: <String>[path.join('test', 'host_cross_arch.shard')],
219 );
220}
221
222Future<void> _runIntegrationToolTests() async {
223 final List<String> allTests = Directory(path.join(_toolsPath, 'test', 'integration.shard'))
224 .listSync(recursive: true)
225 .whereType<File>()
226 .map<String>((FileSystemEntity entry) => path.relative(entry.path, from: _toolsPath))
227 .where((String testPath) => path.basename(testPath).endsWith('_test.dart'))
228 .toList();
229
230 await runDartTest(
231 _toolsPath,
232 forceSingleCore: true,
233 testPaths: selectIndexOfTotalSubshard<String>(allTests),
234 collectMetrics: true,
235 );
236}
237
238Future<void> _runWidgetPreviewScaffoldToolTests() async {
239 await runFlutterTest(
240 path.join(_toolsPath, 'test', 'widget_preview_scaffold.shard', 'widget_preview_scaffold'),
241 );
242}
243
244Future<void> _runToolTests() async {
245 await selectSubshard(<String, ShardRunner>{
246 'general': _runGeneralToolTests,
247 'commands': _runCommandsToolTests,
248 'widget_preview_scaffold': _runWidgetPreviewScaffoldToolTests,
249 });
250}
251
252Future<void> _runSnippetsTests() async {
253 final String snippetsPath = path.join(flutterRoot, 'dev', 'snippets');
254 final List<String> allTests = Directory(path.join(snippetsPath, 'test'))
255 .listSync(recursive: true)
256 .whereType<File>()
257 .map<String>((FileSystemEntity entry) => path.relative(entry.path, from: _toolsPath))
258 .where((String testPath) => path.basename(testPath).endsWith('_test.dart'))
259 .toList();
260
261 await runDartTest(
262 snippetsPath,
263 forceSingleCore: true,
264 testPaths: selectIndexOfTotalSubshard<String>(allTests),
265 collectMetrics: true,
266 );
267}
268
269Future<void> runForbiddenFromReleaseTests() async {
270 // Build a release APK to get the snapshot json.
271 final Directory tempDirectory = Directory.systemTemp.createTempSync('flutter_forbidden_imports.');
272 final List<String> command = <String>[
273 'build',
274 'apk',
275 '--target-platform',
276 'android-arm64',
277 '--release',
278 '--analyze-size',
279 '--code-size-directory',
280 tempDirectory.path,
281 '-v',
282 ];
283
284 await runCommand(
285 flutter,
286 command,
287 workingDirectory: path.join(flutterRoot, 'examples', 'hello_world'),
288 );
289
290 // First, a smoke test.
291 final List<String> smokeTestArgs = <String>[
292 path.join(flutterRoot, 'dev', 'forbidden_from_release_tests', 'bin', 'main.dart'),
293 '--snapshot',
294 path.join(tempDirectory.path, 'snapshot.arm64-v8a.json'),
295 '--package-config',
296 path.join(flutterRoot, '.dart_tool', 'package_config.json'),
297 '--forbidden-type',
298 'package:flutter/src/widgets/framework.dart::Widget',
299 ];
300 await runCommand(dart, smokeTestArgs, workingDirectory: flutterRoot, expectNonZeroExit: true);
301
302 // Actual test.
303 final List<String> args = <String>[
304 path.join(flutterRoot, 'dev', 'forbidden_from_release_tests', 'bin', 'main.dart'),
305 '--snapshot',
306 path.join(tempDirectory.path, 'snapshot.arm64-v8a.json'),
307 '--package-config',
308 path.join(flutterRoot, '.dart_tool', 'package_config.json'),
309 '--forbidden-type',
310 'package:flutter/src/widgets/widget_inspector.dart::WidgetInspectorService',
311 '--forbidden-type',
312 'package:flutter/src/widgets/framework.dart::DebugCreator',
313 '--forbidden-type',
314 'package:flutter/src/foundation/print.dart::debugPrint',
315 ];
316 await runCommand(dart, args, workingDirectory: flutterRoot);
317}
318
319/// Verifies that APK, and IPA (if on macOS), and native desktop builds the
320/// examples apps without crashing. It does not actually launch the apps. That
321/// happens later in the devicelab. This is just a smoke-test. In particular,
322/// this will verify we can build when there are spaces in the path name for the
323/// Flutter SDK and target app.
324///
325/// Also does some checking about types included in hello_world.
326Future<void> _runBuildTests() async {
327 final List<Directory> exampleDirectories =
328 Directory(path.join(flutterRoot, 'examples'))
329 .listSync()
330 // API example builds will be tested in a separate shard.
331 .where(
332 (FileSystemEntity entity) => entity is Directory && path.basename(entity.path) != 'api',
333 )
334 .cast<Directory>()
335 .toList()
336 ..add(Directory(path.join(flutterRoot, 'packages', 'integration_test', 'example')))
337 ..add(
338 Directory(
339 path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'),
340 ),
341 )
342 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'android_views')))
343 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'channels')))
344 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'hybrid_android_views')))
345 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'flutter_gallery')))
346 ..add(
347 Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'ios_platform_view_tests')),
348 )
349 ..add(
350 Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'ios_app_with_extensions')),
351 )
352 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'platform_interaction')))
353 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'spell_check')))
354 ..add(Directory(path.join(flutterRoot, 'dev', 'integration_tests', 'ui')));
355
356 // The tests are randomly distributed into subshards so as to get a uniform
357 // distribution of costs, but the seed is fixed so that issues are reproducible.
358 final List<ShardRunner> tests = <ShardRunner>[
359 for (final Directory exampleDirectory in exampleDirectories)
360 () => _runExampleProjectBuildTests(exampleDirectory),
361 ...<ShardRunner>[
362 // Web compilation tests.
363 () => _flutterBuildDart2js(
364 path.join('dev', 'integration_tests', 'web'),
365 path.join('lib', 'main.dart'),
366 ),
367 // Should not fail to compile with dart:io.
368 () => _flutterBuildDart2js(
369 path.join('dev', 'integration_tests', 'web_compile_tests'),
370 path.join('lib', 'dart_io_import.dart'),
371 ),
372 // Should be able to compile with a call to:
373 // BackgroundIsolateBinaryMessenger.ensureInitialized.
374 () => _flutterBuildDart2js(
375 path.join('dev', 'integration_tests', 'web_compile_tests'),
376 path.join('lib', 'background_isolate_binary_messenger.dart'),
377 ),
378 ],
379 runForbiddenFromReleaseTests,
380 ]..shuffle(math.Random(0));
381
382 await runShardRunnerIndexOfTotalSubshard(tests);
383}
384
385Future<void> _runExampleProjectBuildTests(Directory exampleDirectory, [File? mainFile]) async {
386 // Only verify caching with flutter gallery.
387 final bool verifyCaching = exampleDirectory.path.contains('flutter_gallery');
388 final String examplePath = path.relative(exampleDirectory.path, from: Directory.current.path);
389 final List<String> additionalArgs = <String>[
390 if (mainFile != null) path.relative(mainFile.path, from: exampleDirectory.absolute.path),
391 ];
392 if (Directory(path.join(examplePath, 'android')).existsSync()) {
393 await _flutterBuildApk(
394 examplePath,
395 release: false,
396 additionalArgs: additionalArgs,
397 verifyCaching: verifyCaching,
398 );
399 await _flutterBuildApk(
400 examplePath,
401 release: true,
402 additionalArgs: additionalArgs,
403 verifyCaching: verifyCaching,
404 );
405 } else {
406 print('Example project ${path.basename(examplePath)} has no android directory, skipping apk');
407 }
408 if (Platform.isMacOS) {
409 if (Directory(path.join(examplePath, 'ios')).existsSync()) {
410 await _flutterBuildIpa(
411 examplePath,
412 release: false,
413 additionalArgs: additionalArgs,
414 verifyCaching: verifyCaching,
415 );
416 await _flutterBuildIpa(
417 examplePath,
418 release: true,
419 additionalArgs: additionalArgs,
420 verifyCaching: verifyCaching,
421 );
422 } else {
423 print('Example project ${path.basename(examplePath)} has no ios directory, skipping ipa');
424 }
425 }
426 if (Platform.isLinux) {
427 if (Directory(path.join(examplePath, 'linux')).existsSync()) {
428 await _flutterBuildLinux(
429 examplePath,
430 release: false,
431 additionalArgs: additionalArgs,
432 verifyCaching: verifyCaching,
433 );
434 await _flutterBuildLinux(
435 examplePath,
436 release: true,
437 additionalArgs: additionalArgs,
438 verifyCaching: verifyCaching,
439 );
440 } else {
441 print('Example project ${path.basename(examplePath)} has no linux directory, skipping Linux');
442 }
443 }
444 if (Platform.isMacOS) {
445 if (Directory(path.join(examplePath, 'macos')).existsSync()) {
446 await _flutterBuildMacOS(
447 examplePath,
448 release: false,
449 additionalArgs: additionalArgs,
450 verifyCaching: verifyCaching,
451 );
452 await _flutterBuildMacOS(
453 examplePath,
454 release: true,
455 additionalArgs: additionalArgs,
456 verifyCaching: verifyCaching,
457 );
458 } else {
459 print('Example project ${path.basename(examplePath)} has no macos directory, skipping macOS');
460 }
461 }
462 if (Platform.isWindows) {
463 if (Directory(path.join(examplePath, 'windows')).existsSync()) {
464 await _flutterBuildWin32(
465 examplePath,
466 release: false,
467 additionalArgs: additionalArgs,
468 verifyCaching: verifyCaching,
469 );
470 await _flutterBuildWin32(
471 examplePath,
472 release: true,
473 additionalArgs: additionalArgs,
474 verifyCaching: verifyCaching,
475 );
476 } else {
477 print(
478 'Example project ${path.basename(examplePath)} has no windows directory, skipping Win32',
479 );
480 }
481 }
482}
483
484Future<void> _flutterBuildApk(
485 String relativePathToApplication, {
486 required bool release,
487 bool verifyCaching = false,
488 List<String> additionalArgs = const <String>[],
489}) async {
490 printProgress(
491 '${green}Testing APK ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...',
492 );
493 await _flutterBuild(
494 relativePathToApplication,
495 'APK',
496 'apk',
497 release: release,
498 verifyCaching: verifyCaching,
499 additionalArgs: additionalArgs,
500 );
501}
502
503Future<void> _flutterBuildIpa(
504 String relativePathToApplication, {
505 required bool release,
506 List<String> additionalArgs = const <String>[],
507 bool verifyCaching = false,
508}) async {
509 assert(Platform.isMacOS);
510 printProgress(
511 '${green}Testing IPA ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...',
512 );
513 await _flutterBuild(
514 relativePathToApplication,
515 'IPA',
516 'ios',
517 release: release,
518 verifyCaching: verifyCaching,
519 additionalArgs: <String>[...additionalArgs, '--no-codesign'],
520 );
521}
522
523Future<void> _flutterBuildLinux(
524 String relativePathToApplication, {
525 required bool release,
526 bool verifyCaching = false,
527 List<String> additionalArgs = const <String>[],
528}) async {
529 assert(Platform.isLinux);
530 await runCommand(flutter, <String>['config', '--enable-linux-desktop']);
531 printProgress(
532 '${green}Testing Linux ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...',
533 );
534 await _flutterBuild(
535 relativePathToApplication,
536 'Linux',
537 'linux',
538 release: release,
539 verifyCaching: verifyCaching,
540 additionalArgs: additionalArgs,
541 );
542}
543
544Future<void> _flutterBuildMacOS(
545 String relativePathToApplication, {
546 required bool release,
547 bool verifyCaching = false,
548 List<String> additionalArgs = const <String>[],
549}) async {
550 assert(Platform.isMacOS);
551 await runCommand(flutter, <String>['config', '--enable-macos-desktop']);
552 printProgress(
553 '${green}Testing macOS ${release ? 'release' : 'debug'} build$reset for $cyan$relativePathToApplication$reset...',
554 );
555 await _flutterBuild(
556 relativePathToApplication,
557 'macOS',
558 'macos',
559 release: release,
560 verifyCaching: verifyCaching,
561 additionalArgs: additionalArgs,
562 );
563}
564
565Future<void> _flutterBuildWin32(
566 String relativePathToApplication, {
567 required bool release,
568 bool verifyCaching = false,
569 List<String> additionalArgs = const <String>[],
570}) async {
571 assert(Platform.isWindows);
572 printProgress(
573 '${green}Testing ${release ? 'release' : 'debug'} Windows build$reset for $cyan$relativePathToApplication$reset...',
574 );
575 await _flutterBuild(
576 relativePathToApplication,
577 'Windows',
578 'windows',
579 release: release,
580 verifyCaching: verifyCaching,
581 additionalArgs: additionalArgs,
582 );
583}
584
585Future<void> _flutterBuild(
586 String relativePathToApplication,
587 String platformLabel,
588 String platformBuildName, {
589 required bool release,
590 bool verifyCaching = false,
591 List<String> additionalArgs = const <String>[],
592}) async {
593 await runCommand(flutter, <String>[
594 'build',
595 platformBuildName,
596 if (verifyCaching) '--performance-measurement-file=perf.json',
597 ...additionalArgs,
598 if (release) '--release' else '--debug',
599 '-v',
600 ], workingDirectory: path.join(flutterRoot, relativePathToApplication));
601
602 if (verifyCaching) {
603 printProgress(
604 '${green}Testing $platformLabel cache$reset for $cyan$relativePathToApplication$reset...',
605 );
606 await runCommand(flutter, <String>[
607 'build',
608 platformBuildName,
609 '--performance-measurement-file=perf.json',
610 ...additionalArgs,
611 if (release) '--release' else '--debug',
612 '-v',
613 ], workingDirectory: path.join(flutterRoot, relativePathToApplication));
614 final File file = File(path.join(flutterRoot, relativePathToApplication, 'perf.json'));
615 if (!_allTargetsCached(file)) {
616 foundError(<String>[
617 '${red}Not all build targets cached after second run.$reset',
618 'The target performance data was: ${file.readAsStringSync().replaceAll('},', '},\n')}',
619 ]);
620 }
621 }
622}
623
624bool _allTargetsCached(File performanceFile) {
625 if (dryRun) {
626 return true;
627 }
628 final Map<String, Object?> data =
629 json.decode(performanceFile.readAsStringSync()) as Map<String, Object?>;
630 final List<Map<String, Object?>> targets = (data['targets']! as List<Object?>)
631 .cast<Map<String, Object?>>();
632 return targets.every((Map<String, Object?> element) => element['skipped'] == true);
633}
634
635Future<void> _flutterBuildDart2js(
636 String relativePathToApplication,
637 String target, {
638 bool expectNonZeroExit = false,
639}) async {
640 printProgress('${green}Testing Dart2JS build$reset for $cyan$relativePathToApplication$reset...');
641 await runCommand(
642 flutter,
643 <String>['build', 'web', '-v', '--target=$target'],
644 workingDirectory: path.join(flutterRoot, relativePathToApplication),
645 expectNonZeroExit: expectNonZeroExit,
646 environment: <String, String>{'FLUTTER_WEB': 'true'},
647 );
648}
649