| // Copyright 2013 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:io' as io; |
| |
| import 'package:path/path.dart' as pathlib; |
| import 'package:pool/pool.dart'; |
| import 'package:web_test_utils/goldens.dart'; |
| |
| import '../environment.dart'; |
| import '../exceptions.dart'; |
| import '../pipeline.dart'; |
| import '../utils.dart'; |
| |
| /// Compiles web tests and their dependencies. |
| /// |
| /// Includes: |
| /// * compile the test code itself |
| /// * compile the page that hosts the tests |
| /// * fetch the golden repo for screenshot comparison |
| class CompileTestsStep implements PipelineStep { |
| CompileTestsStep({ |
| this.skipGoldensRepoFetch = false, |
| this.testFiles, |
| }); |
| |
| final bool skipGoldensRepoFetch; |
| final List<FilePath>? testFiles; |
| |
| @override |
| String get description => 'compile_tests'; |
| |
| @override |
| bool get isSafeToInterrupt => true; |
| |
| @override |
| Future<void> interrupt() async { |
| await cleanup(); |
| } |
| |
| @override |
| Future<void> run() async { |
| if (!skipGoldensRepoFetch) { |
| await fetchGoldensRepo(); |
| } |
| await buildHostPage(); |
| await compileTests(testFiles ?? findAllTests()); |
| } |
| } |
| |
| /// Compiles the specified unit tests. |
| Future<void> compileTests(List<FilePath> testFiles) async { |
| final Stopwatch stopwatch = Stopwatch()..start(); |
| |
| // Separate HTML targets from CanvasKit targets because the two use |
| // different dart2js options. |
| final List<FilePath> htmlTargets = <FilePath>[]; |
| final List<FilePath> canvasKitTargets = <FilePath>[]; |
| final String canvasKitTestDirectory = |
| pathlib.join(environment.webUiTestDir.path, 'canvaskit'); |
| for (final FilePath testFile in testFiles) { |
| if (pathlib.isWithin(canvasKitTestDirectory, testFile.absolute)) { |
| canvasKitTargets.add(testFile); |
| } else { |
| htmlTargets.add(testFile); |
| } |
| } |
| |
| await Future.wait(<Future<void>>[ |
| if (htmlTargets.isNotEmpty) |
| _compileTestsInParallel(targets: htmlTargets, forCanvasKit: false), |
| if (canvasKitTargets.isNotEmpty) |
| _compileTestsInParallel(targets: canvasKitTargets, forCanvasKit: true), |
| ]); |
| |
| stopwatch.stop(); |
| |
| final int targetCount = htmlTargets.length + canvasKitTargets.length; |
| print( |
| 'Built $targetCount tests in ${stopwatch.elapsedMilliseconds ~/ 1000} ' |
| 'seconds using $_dart2jsConcurrency concurrent dart2js processes.', |
| ); |
| } |
| |
| // Maximum number of concurrent dart2js processes to use. |
| const int _dart2jsConcurrency = int.fromEnvironment('FELT_DART2JS_CONCURRENCY', defaultValue: 8); |
| |
| final Pool _dart2jsPool = Pool(_dart2jsConcurrency); |
| |
| /// Spawns multiple dart2js processes to compile [targets] in parallel. |
| Future<void> _compileTestsInParallel({ |
| required List<FilePath> targets, |
| required bool forCanvasKit, |
| }) async { |
| final Stream<bool> results = _dart2jsPool.forEach( |
| targets, |
| (FilePath file) => compileUnitTest(file, forCanvasKit: forCanvasKit), |
| ); |
| await for (final bool isSuccess in results) { |
| if (!isSuccess) { |
| throw ToolExit('Failed to compile tests.'); |
| } |
| } |
| } |
| |
| /// Compiles one unit test using `dart2js`. |
| /// |
| /// When building for CanvasKit we have to use extra argument |
| /// `DFLUTTER_WEB_USE_SKIA=true`. |
| /// |
| /// Dart2js creates the following outputs: |
| /// - target.browser_test.dart.js |
| /// - target.browser_test.dart.js.deps |
| /// - target.browser_test.dart.js.maps |
| /// under the same directory with test file. If all these files are not in |
| /// the same directory, Chrome dev tools cannot load the source code during |
| /// debug. |
| /// |
| /// All the files under test already copied from /test directory to /build |
| /// directory before test are build. See [_copyFilesFromTestToBuild]. |
| /// |
| /// Later the extra files will be deleted in [_cleanupExtraFilesUnderTestDir]. |
| Future<bool> compileUnitTest(FilePath input, { required bool forCanvasKit }) async { |
| final String targetFileName = pathlib.join( |
| environment.webUiBuildDir.path, |
| '${input.relativeToWebUi}.browser_test.dart.js', |
| ); |
| |
| final io.Directory directoryToTarget = io.Directory(pathlib.join( |
| environment.webUiBuildDir.path, |
| pathlib.dirname(input.relativeToWebUi))); |
| |
| if (!directoryToTarget.existsSync()) { |
| directoryToTarget.createSync(recursive: true); |
| } |
| |
| final List<String> arguments = <String>[ |
| '--no-minify', |
| '--disable-inlining', |
| '--enable-asserts', |
| '--enable-experiment=non-nullable', |
| '--no-sound-null-safety', |
| |
| // We do not want to auto-select a renderer in tests. As of today, tests |
| // are designed to run in one specific mode. So instead, we specify the |
| // renderer explicitly. |
| '-DFLUTTER_WEB_AUTO_DETECT=false', |
| '-DFLUTTER_WEB_USE_SKIA=$forCanvasKit', |
| |
| '-O2', |
| '-o', |
| targetFileName, // target path. |
| input.relativeToWebUi, // current path. |
| ]; |
| |
| final int exitCode = await runProcess( |
| environment.dart2jsExecutable, |
| arguments, |
| workingDirectory: environment.webUiRootDir.path, |
| ); |
| |
| if (exitCode != 0) { |
| io.stderr.writeln('ERROR: Failed to compile test $input. ' |
| 'Dart2js exited with exit code $exitCode'); |
| return false; |
| } else { |
| return true; |
| } |
| } |
| |
| Future<void> buildHostPage() async { |
| final String hostDartPath = pathlib.join('lib', 'static', 'host.dart'); |
| final io.File hostDartFile = io.File(pathlib.join( |
| environment.webEngineTesterRootDir.path, |
| hostDartPath, |
| )); |
| final io.File timestampFile = io.File(pathlib.join( |
| environment.webEngineTesterRootDir.path, |
| '$hostDartPath.js.timestamp', |
| )); |
| |
| final String timestamp = |
| hostDartFile.statSync().modified.millisecondsSinceEpoch.toString(); |
| if (timestampFile.existsSync()) { |
| final String lastBuildTimestamp = timestampFile.readAsStringSync(); |
| if (lastBuildTimestamp == timestamp) { |
| // The file is still fresh. No need to rebuild. |
| return; |
| } else { |
| // Record new timestamp, but don't return. We need to rebuild. |
| print('${hostDartFile.path} timestamp changed. Rebuilding.'); |
| } |
| } else { |
| print('Building ${hostDartFile.path}.'); |
| } |
| |
| final int exitCode = await runProcess( |
| environment.dart2jsExecutable, |
| <String>[ |
| hostDartPath, |
| '-o', |
| '$hostDartPath.js', |
| ], |
| workingDirectory: environment.webEngineTesterRootDir.path, |
| ); |
| |
| if (exitCode != 0) { |
| throw ToolExit( |
| 'Failed to compile ${hostDartFile.path}. Compiler ' |
| 'exited with exit code $exitCode', |
| exitCode: exitCode, |
| ); |
| } |
| |
| // Record the timestamp to avoid rebuilding unless the file changes. |
| timestampFile.writeAsStringSync(timestamp); |
| } |
| |
| Future<void> fetchGoldensRepo() async { |
| print('INFO: Fetching goldens repo'); |
| final GoldensRepoFetcher goldensRepoFetcher = GoldensRepoFetcher( |
| environment.webUiGoldensRepositoryDirectory, |
| pathlib.join(environment.webUiDevDir.path, 'goldens_lock.yaml')); |
| await goldensRepoFetcher.fetch(); |
| } |