// Copyright 2014 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:async'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/async_guard.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:mockito/mockito.dart'; import 'package:package_config/package_config.dart'; import 'package:process/process.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/mocks.dart'; void main() { ProcessManager mockProcessManager; ResidentCompiler generator; ResidentCompiler generatorWithScheme; MockProcess mockFrontendServer; MockStdIn mockFrontendServerStdIn; MockStream mockFrontendServerStdErr; StreamController stdErrStreamController; BufferLogger testLogger; setUp(() { testLogger = BufferLogger.test(); mockProcessManager = MockProcessManager(); mockFrontendServer = MockProcess(); mockFrontendServerStdIn = MockStdIn(); mockFrontendServerStdErr = MockStream(); generator = ResidentCompiler( 'sdkroot', buildMode: BuildMode.debug, logger: testLogger, processManager: mockProcessManager, artifacts: Artifacts.test(), platform: FakePlatform(operatingSystem: 'linux'), ); generatorWithScheme = ResidentCompiler( 'sdkroot', buildMode: BuildMode.debug, logger: testLogger, processManager: mockProcessManager, artifacts: Artifacts.test(), platform: FakePlatform(operatingSystem: 'linux'), fileSystemRoots: [ '/foo/bar/fizz', ], fileSystemScheme: 'scheme', ); when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn); when(mockFrontendServer.stderr) .thenAnswer((Invocation invocation) => mockFrontendServerStdErr); when(mockFrontendServer.exitCode).thenAnswer((Invocation invocation) { return Completer().future; }); stdErrStreamController = StreamController(); when(mockFrontendServerStdErr.transform(any)) .thenAnswer((Invocation invocation) => stdErrStreamController.stream); when(mockProcessManager.canRun(any)).thenReturn(true); when(mockProcessManager.start(any)).thenAnswer( (Invocation invocation) => Future.value(mockFrontendServer) ); }); testWithoutContext('incremental compile single dart compile', () async { when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => Stream>.fromFuture( Future>.value(utf8.encode( 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0' )) )); final CompilerOutput output = await generator.recompile( Uri.parse('/path/to/main.dart'), null /* invalidatedFiles */, outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n'); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(testLogger.errorText, equals('line1\nline2\n')); expect(output.outputFilename, equals('/path/to/main.dart.dill')); }); testWithoutContext('incremental compile single dart compile with filesystem scheme', () async { when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => Stream>.fromFuture( Future>.value(utf8.encode( 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0' )) )); final CompilerOutput output = await generatorWithScheme.recompile( Uri.parse('file:///foo/bar/fizz/main.dart'), null /* invalidatedFiles */, outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n'); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(testLogger.errorText, equals('line1\nline2\n')); expect(output.outputFilename, equals('/path/to/main.dart.dill')); }); testWithoutContext('incremental compile single dart compile abnormally terminates', () async { when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => const Stream>.empty() ); expect(asyncGuard(() => generator.recompile( Uri.parse('/path/to/main.dart'), null, /* invalidatedFiles */ outputPath: '/build/', packageConfig: PackageConfig.empty, )), throwsToolExit()); }); testWithoutContext('incremental compile single dart compile abnormally terminates via exitCode', () async { when(mockFrontendServer.exitCode) .thenAnswer((Invocation invocation) async => 1); when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => const Stream>.empty() ); expect(asyncGuard(() => generator.recompile( Uri.parse('/path/to/main.dart'), null, /* invalidatedFiles */ outputPath: '/build/', packageConfig: PackageConfig.empty, )), throwsToolExit()); }); testWithoutContext('incremental compile and recompile', () async { final StreamController> streamController = StreamController>(); when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => streamController.stream); streamController.add(utf8.encode('result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n')); await generator.recompile( Uri.parse('/path/to/main.dart'), null, /* invalidatedFiles */ outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n'); // No accept or reject commands should be issued until we // send recompile request. await _accept(streamController, generator, mockFrontendServerStdIn, ''); await _reject(streamController, generator, mockFrontendServerStdIn, '', ''); await _recompile(streamController, generator, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'); await _accept(streamController, generator, mockFrontendServerStdIn, r'^accept\n$'); await _recompile(streamController, generator, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'); // No sources returned from reject command. await _reject(streamController, generator, mockFrontendServerStdIn, 'result abc\nabc\n', r'^reject\n$'); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(mockFrontendServerStdIn.getAndClear(), isEmpty); expect(testLogger.errorText, equals( 'line0\nline1\n' 'line1\nline2\n' 'line1\nline2\n' )); }); testWithoutContext('incremental compile and recompile with filesystem scheme', () async { final StreamController> streamController = StreamController>(); when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => streamController.stream); streamController.add(utf8.encode('result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n')); await generatorWithScheme.recompile( Uri.parse('file:///foo/bar/fizz/main.dart'), null, /* invalidatedFiles */ outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n'); // No accept or reject commands should be issued until we // send recompile request. await _accept(streamController, generatorWithScheme, mockFrontendServerStdIn, ''); await _reject(streamController, generatorWithScheme, mockFrontendServerStdIn, '', ''); await _recompile(streamController, generatorWithScheme, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', mainUri: Uri.parse('file:///foo/bar/fizz/main.dart'), expectedMainUri: 'scheme:///main.dart'); await _accept(streamController, generatorWithScheme, mockFrontendServerStdIn, r'^accept\n$'); await _recompile(streamController, generatorWithScheme, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', mainUri: Uri.parse('file:///foo/bar/fizz/main.dart'), expectedMainUri: 'scheme:///main.dart'); // No sources returned from reject command. await _reject(streamController, generatorWithScheme, mockFrontendServerStdIn, 'result abc\nabc\n', r'^reject\n$'); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(mockFrontendServerStdIn.getAndClear(), isEmpty); expect(testLogger.errorText, equals( 'line0\nline1\n' 'line1\nline2\n' 'line1\nline2\n' )); }); testWithoutContext('incremental compile and recompile non-entrypoint file with filesystem scheme', () async { final Uri mainUri = Uri.parse('file:///foo/bar/fizz/main.dart'); const String expectedMainUri = 'scheme:///main.dart'; final List updatedUris = [ mainUri, Uri.parse('file:///foo/bar/fizz/other.dart'), ]; const List expectedUpdatedUris = [ expectedMainUri, 'scheme:///other.dart', ]; final StreamController> streamController = StreamController>(); when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => streamController.stream); streamController.add(utf8.encode('result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n')); await generatorWithScheme.recompile( Uri.parse('file:///foo/bar/fizz/main.dart'), null, /* invalidatedFiles */ outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n'); // No accept or reject commands should be issued until we // send recompile request. await _accept(streamController, generatorWithScheme, mockFrontendServerStdIn, ''); await _reject(streamController, generatorWithScheme, mockFrontendServerStdIn, '', ''); await _recompile(streamController, generatorWithScheme, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', mainUri: mainUri, expectedMainUri: expectedMainUri, updatedUris: updatedUris, expectedUpdatedUris: expectedUpdatedUris); await _accept(streamController, generatorWithScheme, mockFrontendServerStdIn, r'^accept\n$'); await _recompile(streamController, generatorWithScheme, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', mainUri: mainUri, expectedMainUri: expectedMainUri, updatedUris: updatedUris, expectedUpdatedUris: expectedUpdatedUris); // No sources returned from reject command. await _reject(streamController, generatorWithScheme, mockFrontendServerStdIn, 'result abc\nabc\n', r'^reject\n$'); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(mockFrontendServerStdIn.getAndClear(), isEmpty); expect(testLogger.errorText, equals( 'line0\nline1\n' 'line1\nline2\n' 'line1\nline2\n' )); }); testWithoutContext('incremental compile can suppress errors', () async { final StreamController> stdoutController = StreamController>(); when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => stdoutController.stream); stdoutController.add(utf8.encode('result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n')); await generator.recompile( Uri.parse('/path/to/main.dart'), [], outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n'); await _recompile(stdoutController, generator, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'); await _accept(stdoutController, generator, mockFrontendServerStdIn, r'^accept\n$'); await _recompile(stdoutController, generator, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', suppressErrors: true); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(mockFrontendServerStdIn.getAndClear(), isEmpty); // Compiler message is not printed with suppressErrors: true above. expect(testLogger.errorText, isNot(equals( 'line1\nline2\n' ))); expect(testLogger.traceText, contains( 'line1\nline2\n' )); }); testWithoutContext('incremental compile and recompile twice', () async { final StreamController> streamController = StreamController>(); when(mockFrontendServer.stdout) .thenAnswer((Invocation invocation) => streamController.stream); streamController.add(utf8.encode( 'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n' )); await generator.recompile( Uri.parse('/path/to/main.dart'), null /* invalidatedFiles */, outputPath: '/build/', packageConfig: PackageConfig.empty, ); expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n'); await _recompile(streamController, generator, mockFrontendServerStdIn, 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'); await _recompile(streamController, generator, mockFrontendServerStdIn, 'result abc\nline2\nline3\nabc\nabc /path/to/main.dart.dill 0\n'); verifyNoMoreInteractions(mockFrontendServerStdIn); expect(mockFrontendServerStdIn.getAndClear(), isEmpty); expect(testLogger.errorText, equals( 'line0\nline1\n' 'line1\nline2\n' 'line2\nline3\n' )); }); } Future _recompile( StreamController> streamController, ResidentCompiler generator, MockStdIn mockFrontendServerStdIn, String mockCompilerOutput, { bool suppressErrors = false, Uri mainUri, String expectedMainUri = '/path/to/main.dart', List updatedUris, List expectedUpdatedUris, }) async { mainUri ??= Uri.parse('/path/to/main.dart'); updatedUris ??= [mainUri]; expectedUpdatedUris ??= [expectedMainUri]; // Put content into the output stream after generator.recompile gets // going few lines below, resets completer. scheduleMicrotask(() { streamController.add(utf8.encode(mockCompilerOutput)); }); final CompilerOutput output = await generator.recompile( mainUri, updatedUris, outputPath: '/build/', packageConfig: PackageConfig.empty, suppressErrors: suppressErrors, ); expect(output.outputFilename, equals('/path/to/main.dart.dill')); final String commands = mockFrontendServerStdIn.getAndClear(); final RegExp whitespace = RegExp(r'\s+'); final List parts = commands.split(whitespace); // Test that uuid matches at beginning and end. expect(parts[2], equals(parts[3 + updatedUris.length])); expect(parts[1], equals(expectedMainUri)); for (int i = 0; i < expectedUpdatedUris.length; i++) { expect(parts[3 + i], equals(expectedUpdatedUris[i])); } mockFrontendServerStdIn.stdInWrites.clear(); } Future _accept( StreamController> streamController, ResidentCompiler generator, MockStdIn mockFrontendServerStdIn, String expected, ) async { // Put content into the output stream after generator.recompile gets // going few lines below, resets completer. generator.accept(); final String commands = mockFrontendServerStdIn.getAndClear(); final RegExp re = RegExp(expected); expect(commands, matches(re)); mockFrontendServerStdIn.stdInWrites.clear(); } Future _reject( StreamController> streamController, ResidentCompiler generator, MockStdIn mockFrontendServerStdIn, String mockCompilerOutput, String expected, ) async { // Put content into the output stream after generator.recompile gets // going few lines below, resets completer. scheduleMicrotask(() { streamController.add(utf8.encode(mockCompilerOutput)); }); final CompilerOutput output = await generator.reject(); expect(output, isNull); final String commands = mockFrontendServerStdIn.getAndClear(); final RegExp re = RegExp(expected); expect(commands, matches(re)); mockFrontendServerStdIn.stdInWrites.clear(); } class MockProcess extends Mock implements Process {} class MockProcessManager extends Mock implements ProcessManager {}