• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2019 The Chromium 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:meta/meta.dart';
8
9import '../artifacts.dart';
10import '../base/file_system.dart';
11import '../base/terminal.dart';
12import '../build_info.dart';
13import '../bundle.dart';
14import '../codegen.dart';
15import '../compile.dart';
16import '../dart/package_map.dart';
17import '../globals.dart';
18import '../project.dart';
19
20/// A request to the [TestCompiler] for recompilation.
21class _CompilationRequest {
22  _CompilationRequest(this.path, this.result);
23  String path;
24  Completer<String> result;
25}
26
27/// A frontend_server wrapper for the flutter test runner.
28///
29/// This class is a wrapper around compiler that allows multiple isolates to
30/// enqueue compilation requests, but ensures only one compilation at a time.
31class TestCompiler {
32  /// Creates a new [TestCompiler] which acts as a frontend_server proxy.
33  ///
34  /// [trackWidgetCreation] configures whether the kernel transform is applied
35  /// to the output. This also changes the output file to include a '.track`
36  /// extension.
37  ///
38  /// [flutterProject] is the project for which we are running tests.
39  TestCompiler(
40    this.trackWidgetCreation,
41    this.flutterProject,
42  ) : testFilePath = getKernelPathForTransformerOptions(
43        fs.path.join(flutterProject.directory.path, getBuildDirectory(), 'testfile.dill'),
44        trackWidgetCreation: trackWidgetCreation,
45      ) {
46    // Compiler maintains and updates single incremental dill file.
47    // Incremental compilation requests done for each test copy that file away
48    // for independent execution.
49    final Directory outputDillDirectory = fs.systemTempDirectory.createTempSync('flutter_test_compiler.');
50    outputDill = outputDillDirectory.childFile('output.dill');
51    printTrace('Compiler will use the following file as its incremental dill file: ${outputDill.path}');
52    printTrace('Listening to compiler controller...');
53    compilerController.stream.listen(_onCompilationRequest, onDone: () {
54      printTrace('Deleting ${outputDillDirectory.path}...');
55      outputDillDirectory.deleteSync(recursive: true);
56    });
57  }
58
59  final StreamController<_CompilationRequest> compilerController = StreamController<_CompilationRequest>();
60  final List<_CompilationRequest> compilationQueue = <_CompilationRequest>[];
61  final FlutterProject flutterProject;
62  final bool trackWidgetCreation;
63  final String testFilePath;
64
65
66  ResidentCompiler compiler;
67  File outputDill;
68  // Whether to report compiler messages.
69  bool _suppressOutput = false;
70
71  Future<String> compile(String mainDart) {
72    final Completer<String> completer = Completer<String>();
73    compilerController.add(_CompilationRequest(mainDart, completer));
74    return completer.future;
75  }
76
77  Future<void> _shutdown() async {
78    // Check for null in case this instance is shut down before the
79    // lazily-created compiler has been created.
80    if (compiler != null) {
81      await compiler.shutdown();
82      compiler = null;
83    }
84  }
85
86  Future<void> dispose() async {
87    await compilerController.close();
88    await _shutdown();
89  }
90
91  /// Create the resident compiler used to compile the test.
92  @visibleForTesting
93  Future<ResidentCompiler> createCompiler() async {
94    if (flutterProject.hasBuilders) {
95      return CodeGeneratingResidentCompiler.create(
96        flutterProject: flutterProject,
97        trackWidgetCreation: trackWidgetCreation,
98        compilerMessageConsumer: _reportCompilerMessage,
99        initializeFromDill: testFilePath,
100        // We already ran codegen once at the start, we only need to
101        // configure builders.
102        runCold: true,
103      );
104    }
105    return ResidentCompiler(
106      artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
107      packagesPath: PackageMap.globalPackagesPath,
108      trackWidgetCreation: trackWidgetCreation,
109      compilerMessageConsumer: _reportCompilerMessage,
110      initializeFromDill: testFilePath,
111      unsafePackageSerialization: false,
112    );
113  }
114
115  // Handle a compilation request.
116  Future<void> _onCompilationRequest(_CompilationRequest request) async {
117    final bool isEmpty = compilationQueue.isEmpty;
118    compilationQueue.add(request);
119    // Only trigger processing if queue was empty - i.e. no other requests
120    // are currently being processed. This effectively enforces "one
121    // compilation request at a time".
122    if (!isEmpty) {
123      return;
124    }
125    while (compilationQueue.isNotEmpty) {
126      final _CompilationRequest request = compilationQueue.first;
127      printTrace('Compiling ${request.path}');
128      final Stopwatch compilerTime = Stopwatch()..start();
129      bool firstCompile = false;
130      if (compiler == null) {
131        compiler = await createCompiler();
132        firstCompile = true;
133      }
134      _suppressOutput = false;
135      final CompilerOutput compilerOutput = await compiler.recompile(
136        request.path,
137        <Uri>[Uri.parse(request.path)],
138        outputPath: outputDill.path,
139      );
140      final String outputPath = compilerOutput?.outputFilename;
141
142      // In case compiler didn't produce output or reported compilation
143      // errors, pass [null] upwards to the consumer and shutdown the
144      // compiler to avoid reusing compiler that might have gotten into
145      // a weird state.
146      if (outputPath == null || compilerOutput.errorCount > 0) {
147        request.result.complete(null);
148        await _shutdown();
149      } else {
150        final File outputFile = fs.file(outputPath);
151        final File kernelReadyToRun = await outputFile.copy('${request.path}.dill');
152        final File testCache = fs.file(testFilePath);
153        if (firstCompile || !testCache.existsSync() || (testCache.lengthSync() < outputFile.lengthSync())) {
154          // The idea is to keep the cache file up-to-date and include as
155          // much as possible in an effort to re-use as many packages as
156          // possible.
157          ensureDirectoryExists(testFilePath);
158          await outputFile.copy(testFilePath);
159        }
160        request.result.complete(kernelReadyToRun.path);
161        compiler.accept();
162        compiler.reset();
163      }
164      printTrace('Compiling ${request.path} took ${compilerTime.elapsedMilliseconds}ms');
165      // Only remove now when we finished processing the element
166      compilationQueue.removeAt(0);
167    }
168  }
169
170  void _reportCompilerMessage(String message, {bool emphasis, TerminalColor color}) {
171    if (_suppressOutput) {
172      return;
173    }
174    if (message.startsWith('Error: Could not resolve the package \'flutter_test\'')) {
175      printTrace(message);
176      printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n',
177        emphasis: emphasis,
178        color: color,
179      );
180      _suppressOutput = true;
181      return;
182    }
183    printError('$message');
184  }
185}
186