| // 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:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:path/path.dart' as pathlib; |
| import 'package:pool/pool.dart'; |
| |
| import '../environment.dart'; |
| import '../exceptions.dart'; |
| import '../felt_config.dart'; |
| import '../pipeline.dart'; |
| import '../utils.dart' show AnsiColors, FilePath, ProcessManager, cleanup, getBundleBuildDirectory, startProcess; |
| |
| /// Compiles a web test bundle into web_ui/build/test_bundles/<bundle-name>. |
| class CompileBundleStep implements PipelineStep { |
| CompileBundleStep({ |
| required this.bundle, |
| required this.isVerbose, |
| this.testFiles, |
| }); |
| |
| final TestBundle bundle; |
| final bool isVerbose; |
| final Set<FilePath>? testFiles; |
| |
| // Maximum number of concurrent compile processes to use. |
| static final int _compileConcurrency = int.parse(io.Platform.environment['FELT_COMPILE_CONCURRENCY'] ?? '8'); |
| final Pool compilePool = Pool(_compileConcurrency); |
| |
| @override |
| String get description => 'compile_bundle'; |
| |
| @override |
| bool get isSafeToInterrupt => true; |
| |
| @override |
| Future<void> interrupt() async { |
| await cleanup(); |
| } |
| |
| io.Directory get testSetDirectory => io.Directory( |
| pathlib.join(environment.webUiTestDir.path, bundle.testSet.directory) |
| ); |
| |
| io.Directory get outputBundleDirectory => getBundleBuildDirectory(bundle); |
| |
| List<FilePath> _findTestFiles() { |
| final io.Directory testDirectory = testSetDirectory; |
| if (!testDirectory.existsSync()) { |
| throw ToolExit('Test directory "${testDirectory.path}" for bundle ${bundle.name.ansiMagenta} does not exist.'); |
| } |
| return testDirectory |
| .listSync(recursive: true) |
| .whereType<io.File>() |
| .where((io.File f) => f.path.endsWith('_test.dart')) |
| .map<FilePath>((io.File f) => FilePath.fromWebUi( |
| pathlib.relative(f.path, from: environment.webUiRootDir.path))) |
| .toList(); |
| } |
| |
| TestCompiler _createCompiler() { |
| switch (bundle.compileConfig.compiler) { |
| case Compiler.dart2js: |
| return Dart2JSCompiler( |
| testSetDirectory, |
| outputBundleDirectory, |
| renderer: bundle.compileConfig.renderer, |
| isVerbose: isVerbose, |
| ); |
| case Compiler.dart2wasm: |
| return Dart2WasmCompiler( |
| testSetDirectory, |
| outputBundleDirectory, |
| renderer: bundle.compileConfig.renderer, |
| isVerbose: isVerbose, |
| ); |
| } |
| } |
| |
| @override |
| Future<void> run() async { |
| print('Compiling test bundle ${bundle.name.ansiMagenta}...'); |
| final List<FilePath> allTests = _findTestFiles(); |
| final TestCompiler compiler = _createCompiler(); |
| final Stopwatch stopwatch = Stopwatch()..start(); |
| final String testSetDirectoryPath = testSetDirectory.path; |
| |
| // Clear out old bundle compilations, if they exist |
| if (outputBundleDirectory.existsSync()) { |
| outputBundleDirectory.deleteSync(recursive: true ); |
| } |
| |
| final List<Future<MapEntry<String, CompileResult>>> pendingResults = <Future<MapEntry<String, CompileResult>>>[]; |
| for (final FilePath testFile in allTests) { |
| final String relativePath = pathlib.relative( |
| testFile.absolute, |
| from: testSetDirectoryPath); |
| final Future<MapEntry<String, CompileResult>> result = compilePool.withResource(() async { |
| if (testFiles != null && !testFiles!.contains(testFile)) { |
| return MapEntry<String, CompileResult>(relativePath, CompileResult.filtered); |
| } |
| final bool success = await compiler.compileTest(testFile); |
| const int maxTestNameLength = 80; |
| final String truncatedPath = relativePath.length > maxTestNameLength |
| ? relativePath.replaceRange(maxTestNameLength - 3, relativePath.length, '...') |
| : relativePath; |
| final String expandedPath = truncatedPath.padRight(maxTestNameLength); |
| io.stdout.write('\r ${success ? expandedPath.ansiGreen : expandedPath.ansiRed}'); |
| return success |
| ? MapEntry<String, CompileResult>(relativePath, CompileResult.success) |
| : MapEntry<String, CompileResult>(relativePath, CompileResult.compilationFailure); |
| }); |
| pendingResults.add(result); |
| } |
| final Map<String, CompileResult> results = Map<String, CompileResult>.fromEntries(await Future.wait(pendingResults)); |
| stopwatch.stop(); |
| |
| final String resultsJson = const JsonEncoder.withIndent(' ').convert(<String, dynamic>{ |
| 'name': bundle.name, |
| 'directory': bundle.testSet.directory, |
| 'compiler': bundle.compileConfig.compiler.name, |
| 'renderer': bundle.compileConfig.renderer.name, |
| 'compileTimeInMs': stopwatch.elapsedMilliseconds, |
| 'results': results.map((String k, CompileResult v) => MapEntry<String, String>(k, v.name)), |
| }); |
| final io.File outputResultsFile = io.File(pathlib.join( |
| outputBundleDirectory.path, |
| 'results.json', |
| )); |
| outputResultsFile.writeAsStringSync(resultsJson); |
| final List<String> failedFiles = <String>[]; |
| results.forEach((String fileName, CompileResult result) { |
| if (result == CompileResult.compilationFailure) { |
| failedFiles.add(fileName); |
| } |
| }); |
| if (failedFiles.isEmpty) { |
| print('\rCompleted compilation of ${bundle.name.ansiMagenta} in ${stopwatch.elapsedMilliseconds}ms.'.padRight(82)); |
| } else { |
| print('\rThe bundle ${bundle.name.ansiMagenta} compiled with some failures in ${stopwatch.elapsedMilliseconds}ms.'); |
| print('Compilation failures:'); |
| for (final String fileName in failedFiles) { |
| print(' $fileName'); |
| } |
| throw ToolExit('Failed to compile ${bundle.name.ansiMagenta}.'); |
| } |
| } |
| } |
| |
| enum CompileResult { |
| success, |
| compilationFailure, |
| filtered, |
| } |
| |
| abstract class TestCompiler { |
| TestCompiler( |
| this.inputTestSetDirectory, |
| this.outputTestBundleDirectory, |
| { |
| required this.renderer, |
| required this.isVerbose, |
| } |
| ); |
| |
| final io.Directory inputTestSetDirectory; |
| final io.Directory outputTestBundleDirectory; |
| final Renderer renderer; |
| final bool isVerbose; |
| |
| Future<bool> compileTest(FilePath input); |
| } |
| |
| class Dart2JSCompiler extends TestCompiler { |
| Dart2JSCompiler( |
| super.inputTestSetDirectory, |
| super.outputTestBundleDirectory, |
| { |
| required super.renderer, |
| required super.isVerbose, |
| } |
| ); |
| |
| @override |
| Future<bool> compileTest(FilePath input) async { |
| final String relativePath = pathlib.relative( |
| input.absolute, |
| from: inputTestSetDirectory.path |
| ); |
| |
| final String targetFileName = pathlib.join( |
| outputTestBundleDirectory.path, |
| '$relativePath.browser_test.dart.js', |
| ); |
| |
| final io.Directory outputDirectory = io.File(targetFileName).parent; |
| if (!outputDirectory.existsSync()) { |
| outputDirectory.createSync(recursive: true); |
| } |
| |
| final List<String> arguments = <String>[ |
| 'compile', |
| 'js', |
| '--no-minify', |
| '--disable-inlining', |
| '--enable-asserts', |
| |
| // 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=${renderer == Renderer.canvaskit}', |
| '-DFLUTTER_WEB_USE_SKWASM=${renderer == Renderer.skwasm}', |
| |
| '-O2', |
| '-o', |
| targetFileName, // target path. |
| relativePath, // current path. |
| ]; |
| |
| final ProcessManager process = await startProcess( |
| environment.dartExecutable, |
| arguments, |
| workingDirectory: inputTestSetDirectory.path, |
| failureIsSuccess: true, |
| evalOutput: !isVerbose, |
| ); |
| final int exitCode = await process.wait(); |
| if (exitCode != 0) { |
| io.stderr.writeln('ERROR: Failed to compile test $input. ' |
| 'Dart2js exited with exit code $exitCode'); |
| return false; |
| } else { |
| return true; |
| } |
| } |
| } |
| |
| class Dart2WasmCompiler extends TestCompiler { |
| Dart2WasmCompiler( |
| super.inputTestSetDirectory, |
| super.outputTestBundleDirectory, |
| { |
| required super.renderer, |
| required super.isVerbose, |
| } |
| ); |
| |
| @override |
| Future<bool> compileTest(FilePath input) async { |
| final String relativePath = pathlib.relative( |
| input.absolute, |
| from: inputTestSetDirectory.path |
| ); |
| |
| final String targetFileName = pathlib.join( |
| outputTestBundleDirectory.path, |
| '$relativePath.browser_test.dart.wasm', |
| ); |
| |
| final io.Directory outputDirectory = io.File(targetFileName).parent; |
| if (!outputDirectory.existsSync()) { |
| outputDirectory.createSync(recursive: true); |
| } |
| |
| final List<String> arguments = <String>[ |
| environment.dart2wasmSnapshotPath, |
| |
| '--dart-sdk=${environment.dartSdkDir.path}', |
| '--enable-asserts', |
| |
| // 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=${renderer == Renderer.canvaskit}', |
| '-DFLUTTER_WEB_USE_SKWASM=${renderer == Renderer.skwasm}', |
| |
| if (renderer == Renderer.skwasm) ...<String>[ |
| '--import-shared-memory', |
| '--shared-memory-max-pages=32768', |
| ], |
| |
| relativePath, // current path. |
| targetFileName, // target path. |
| ]; |
| |
| final ProcessManager process = await startProcess( |
| environment.dartAotRuntimePath, |
| arguments, |
| workingDirectory: inputTestSetDirectory.path, |
| failureIsSuccess: true, |
| evalOutput: !isVerbose, |
| ); |
| final int exitCode = await process.wait(); |
| |
| if (exitCode != 0) { |
| io.stderr.writeln('ERROR: Failed to compile test $input. ' |
| 'dart2wasm exited with exit code $exitCode'); |
| return false; |
| } else { |
| return true; |
| } |
| } |
| } |