Unverified Commit 9e55af52 authored by Jia Hao's avatar Jia Hao Committed by GitHub

[flutter_tools] Decouple FlutterPlatform from Process (#74236)

parent 6a8ba743
......@@ -15,6 +15,7 @@ import '../base/utils.dart';
import '../globals.dart' as globals;
import '../vmservice.dart';
import 'test_device.dart';
import 'watcher.dart';
/// A class that's used to collect coverage data during tests.
......@@ -27,9 +28,9 @@ class CoverageCollector extends TestWatcher {
bool Function(String) libraryPredicate;
@override
Future<void> handleFinishedTest(ProcessEvent event) async {
_logMessage('test ${event.childIndex}: collecting coverage');
await collectCoverage(event.process, event.observatoryUri);
Future<void> handleFinishedTest(TestDevice testDevice) async {
_logMessage('Starting coverage collection');
await collectCoverage(testDevice);
}
void _logMessage(String line, { bool error = false }) {
......@@ -81,34 +82,41 @@ class CoverageCollector extends TestWatcher {
/// has been run to completion so that all coverage data has been recorded.
///
/// The returned [Future] completes when the coverage is collected.
Future<void> collectCoverage(Process process, Uri observatoryUri) async {
assert(process != null);
assert(observatoryUri != null);
final int pid = process.pid;
_logMessage('pid $pid: collecting coverage data from $observatoryUri...');
Future<void> collectCoverage(TestDevice testDevice) async {
assert(testDevice != null);
Map<String, dynamic> data;
final Future<void> processComplete = process.exitCode
.then<void>((int code) {
throw Exception('Failed to collect coverage, process terminated prematurely with exit code $code.');
});
final Future<void> collectionComplete = collect(observatoryUri, libraryPredicate)
.then<void>((Map<String, dynamic> result) {
if (result == null) {
throw Exception('Failed to collect coverage.');
}
data = result;
final Future<void> processComplete = testDevice.finished.catchError(
(Object error) => throw Exception(
'Failed to collect coverage, test device terminated prematurely with '
'error: ${(error as TestDeviceException).message}.'),
test: (Object error) => error is TestDeviceException,
);
final Future<void> collectionComplete = testDevice.observatoryUri
.then((Uri observatoryUri) {
_logMessage('collecting coverage data from $testDevice at $observatoryUri...');
return collect(observatoryUri, libraryPredicate)
.then<void>((Map<String, dynamic> result) {
if (result == null) {
throw Exception('Failed to collect coverage.');
}
_logMessage('Collected coverage data.');
data = result;
});
});
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null);
_logMessage('pid $pid ($observatoryUri): collected coverage data; merging...');
_logMessage('Merging coverage data...');
_addHitmap(await coverage.createHitmap(
data['coverage'] as List<Map<String, dynamic>>,
packagesPath: packagesPath,
checkIgnoredLines: true,
));
_logMessage('pid $pid ($observatoryUri): done merging coverage data into global coverage map.');
_logMessage('Done merging coverage data into global coverage map.');
}
/// Returns a future that will complete with the formatted coverage data
......@@ -188,10 +196,10 @@ class CoverageCollector extends TestWatcher {
}
@override
Future<void> handleTestCrashed(ProcessEvent event) async { }
Future<void> handleTestCrashed(TestDevice testDevice) async { }
@override
Future<void> handleTestTimedOut(ProcessEvent event) async { }
Future<void> handleTestTimedOut(TestDevice testDevice) async { }
}
Future<vm_service.VmService> _defaultConnect(Uri serviceUri) {
......
......@@ -6,6 +6,8 @@
import '../convert.dart';
import '../globals.dart' as globals;
import 'test_device.dart';
import 'watcher.dart';
/// Prints JSON events when running a test in --machine mode.
......@@ -18,25 +20,25 @@ class EventPrinter extends TestWatcher {
final TestWatcher _parent;
@override
void handleStartedProcess(ProcessEvent event) {
void handleStartedDevice(Uri observatoryUri) {
_sendEvent('test.startedProcess',
<String, dynamic>{'observatoryUri': event.observatoryUri.toString()});
_parent?.handleStartedProcess(event);
<String, dynamic>{'observatoryUri': observatoryUri.toString()});
_parent?.handleStartedDevice(observatoryUri);
}
@override
Future<void> handleTestCrashed(ProcessEvent event) async {
return _parent?.handleTestCrashed(event);
Future<void> handleTestCrashed(TestDevice testDevice) async {
return _parent?.handleTestCrashed(testDevice);
}
@override
Future<void> handleTestTimedOut(ProcessEvent event) async {
return _parent?.handleTestTimedOut(event);
Future<void> handleTestTimedOut(TestDevice testDevice) async {
return _parent?.handleTestTimedOut(testDevice);
}
@override
Future<void> handleFinishedTest(ProcessEvent event) async {
return _parent?.handleFinishedTest(event);
Future<void> handleFinishedTest(TestDevice testDevice) async {
return _parent?.handleFinishedTest(testDevice);
}
void _sendEvent(String name, [ dynamic params ]) {
......
This diff is collapsed.
// 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.
// @dart = 2.8
import 'dart:async';
import '../base/file_system.dart';
import '../globals.dart' as globals;
/// Manages a Font configuration that can be shared across multiple tests.
class FontConfigManager {
Directory _fontsDirectory;
File _cachedFontConfig;
/// Returns a Font configuration that limits font fallback to the artifact
/// cache directory.
File get fontConfigFile {
if (_cachedFontConfig != null) {
return _cachedFontConfig;
}
final StringBuffer sb = StringBuffer();
sb.writeln('<fontconfig>');
sb.writeln(' <dir>${globals.cache.getCacheArtifacts().path}</dir>');
sb.writeln(' <cachedir>/var/cache/fontconfig</cachedir>');
sb.writeln('</fontconfig>');
if (_fontsDirectory == null) {
_fontsDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_test_fonts.');
globals.printTrace('Using this directory for fonts configuration: ${_fontsDirectory.path}');
}
_cachedFontConfig = globals.fs.file('${_fontsDirectory.path}/fonts.conf');
_cachedFontConfig.createSync();
_cachedFontConfig.writeAsStringSync(sb.toString());
return _cachedFontConfig;
}
Future<void> dispose() async {
if (_fontsDirectory != null) {
globals.printTrace('Deleting ${_fontsDirectory.path}...');
await _fontsDirectory.delete(recursive: true);
_fontsDirectory = null;
}
}
}
// 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.
// @dart = 2.8
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';
/// A remote device where tests can be executed on.
///
/// Reusability of an instance across multiple runs is not guaranteed for all
/// implementations.
abstract class TestDevice {
/// Starts the test device with the provided entrypoint.
///
/// Returns a channel that can be used to communicate with the test process.
Future<StreamChannel<String>> start({@required String compiledEntrypointPath});
/// Should complete with null if the observatory is not enabled.
Future<Uri> get observatoryUri;
/// Terminates the test device.
///
/// A [TestDeviceException] can be thrown if it did not stop gracefully.
Future<void> kill();
/// Waits for the test device to stop.
///
/// A [TestDeviceException] can be thrown if it did not stop gracefully.
Future<void> get finished;
}
/// Thrown when the device encounters a problem.
class TestDeviceException implements Exception {
TestDeviceException(this.message, this.stackTrace);
final String message;
final StackTrace stackTrace;
@override
String toString() => 'TestDeviceException($message)';
}
......@@ -4,43 +4,27 @@
// @dart = 2.8
import '../base/io.dart' show Process;
import 'test_device.dart';
/// Callbacks for reporting progress while running tests.
abstract class TestWatcher {
/// Called after a child process starts.
/// Called after the test device starts.
///
/// If startPaused was true, the caller needs to resume in Observatory to
/// start running the tests.
void handleStartedProcess(ProcessEvent event) { }
void handleStartedDevice(Uri observatoryUri) { }
/// Called after the tests finish but before the process exits.
/// Called after the tests finish but before the test device exits.
///
/// The child process won't exit until this method completes.
/// Not called if the process died.
Future<void> handleFinishedTest(ProcessEvent event);
/// The test device won't exit until this method completes.
/// Not called if the test device died.
Future<void> handleFinishedTest(TestDevice testDevice);
/// Called when the test process crashed before connecting to test harness.
Future<void> handleTestCrashed(ProcessEvent event);
/// Called when the test device crashed before it could be connected to the
/// test harness.
Future<void> handleTestCrashed(TestDevice testDevice);
/// Called if we timed out waiting for the test process to connect to test
/// Called if we timed out waiting for the test device to connect to test
/// harness.
Future<void> handleTestTimedOut(ProcessEvent event);
}
/// Describes a child process started during testing.
class ProcessEvent {
ProcessEvent(this.childIndex, this.process, [this.observatoryUri]);
/// The index assigned when the child process was launched.
///
/// Indexes are assigned consecutively starting from zero.
/// When debugging, there should only be one child process so this will
/// always be zero.
final int childIndex;
final Process process;
/// The observatory URL or null if not debugging.
final Uri observatoryUri;
Future<void> handleTestTimedOut(TestDevice testDevice);
}
// 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.
// @dart = 2.8
import 'dart:async';
import 'package:dds/dds.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/test/font_config_manager.dart';
import 'package:flutter_tools/src/test/flutter_tester_device.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import '../src/common.dart';
import '../src/context.dart';
void main() {
FakePlatform platform;
FileSystem fileSystem;
ProcessManager processManager;
FlutterTesterTestDevice device;
setUp(() {
fileSystem = MemoryFileSystem.test();
// Not Windows.
platform = FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{},
);
processManager = FakeProcessManager.any();
});
FlutterTesterTestDevice createDevice({
List<String> dartEntrypointArgs = const <String>[],
bool enableObservatory = false,
}) =>
TestFlutterTesterDevice(
platform: platform,
fileSystem: fileSystem,
processManager: processManager,
enableObservatory: enableObservatory,
dartEntrypointArgs: dartEntrypointArgs,
);
group('The FLUTTER_TEST environment variable is passed to the test process', () {
setUp(() {
processManager = MockProcessManager();
device = createDevice();
fileSystem
.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}');
});
Future<Map<String, String>> captureEnvironment() async {
final Future<StreamChannel<String>> deviceStarted = device.start(
compiledEntrypointPath: 'example.dill',
);
when(processManager.start(
any,
environment: anyNamed('environment')),
).thenAnswer((_) {
return Future<Process>.value(MockProcess());
});
await untilCalled(processManager.start(any, environment: anyNamed('environment')));
final VerificationResult toVerify = verify(processManager.start(
any,
environment: captureAnyNamed('environment'),
));
expect(toVerify.captured, hasLength(1));
expect(toVerify.captured.first, isA<Map<String, String>>());
await deviceStarted;
return toVerify.captured.first as Map<String, String>;
}
testUsingContext('as true when not originally set', () async {
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'true');
});
testUsingContext('as true when set to true', () async {
platform.environment = <String, String>{'FLUTTER_TEST': 'true'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'true');
});
testUsingContext('as false when set to false', () async {
platform.environment = <String, String>{'FLUTTER_TEST': 'false'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'false');
});
testUsingContext('unchanged when set', () async {
platform.environment = <String, String>{'FLUTTER_TEST': 'neither true nor false'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'neither true nor false');
});
testUsingContext('as null when set to null', () async {
platform.environment = <String, String>{'FLUTTER_TEST': null};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], null);
});
});
group('Dart Entrypoint Args', () {
setUp(() {
processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'/',
'--disable-observatory',
'--ipv6',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=.dart_tool/package_config.json',
'--foo',
'--bar',
'example.dill'
],
stdout: 'success',
stderr: 'failure',
exitCode: 0,
)
]);
device = createDevice(dartEntrypointArgs: <String>['--foo', '--bar']);
});
testUsingContext('Can pass additional arguments to tester binary', () async {
await device.start(compiledEntrypointPath: 'example.dill');
expect((processManager as FakeProcessManager).hasRemainingExpectations, false);
});
});
group('DDS', () {
setUp(() {
processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'/',
'--observatory-port=0',
'--ipv6',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=.dart_tool/package_config.json',
'example.dill'
],
stdout: 'Observatory listening on http://localhost:1234',
stderr: 'failure',
exitCode: 0,
)
]);
device = createDevice(enableObservatory: true);
});
testUsingContext('skips setting observatory port and uses the input port for for DDS instead', () async {
await device.start(compiledEntrypointPath: 'example.dill');
await device.observatoryUri;
final Uri uri = await (device as TestFlutterTesterDevice).ddsServiceUriFuture();
expect(uri.port, 1234);
});
});
}
/// A Flutter Tester device.
///
/// Uses a mock HttpServer. We don't want to bind random ports in our CI hosts.
class TestFlutterTesterDevice extends FlutterTesterTestDevice {
TestFlutterTesterDevice({
@required Platform platform,
@required FileSystem fileSystem,
@required ProcessManager processManager,
@required bool enableObservatory,
@required List<String> dartEntrypointArgs,
}) : super(
id: 999,
shellPath: '/',
platform: platform,
fileSystem: fileSystem,
processManager: processManager,
logger: MockLogger(),
debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(
BuildMode.debug,
'',
treeShakeIcons: false,
packagesPath: '.dart_tool/package_config.json',
),
startPaused: false,
disableDds: false,
disableServiceAuthCodes: false,
hostVmServicePort: 1234,
nullAssertions: false,
dartEntrypointArgs: dartEntrypointArgs,
),
enableObservatory: enableObservatory,
machine: false,
host: InternetAddress.loopbackIPv6,
buildTestAssets: false,
flutterProject: null,
icudtlPath: null,
compileExpression: null,
fontConfigManager: FontConfigManager(),
);
final Completer<Uri> _ddsServiceUriCompleter = Completer<Uri>();
Future<Uri> ddsServiceUriFuture() => _ddsServiceUriCompleter.future;
@override
Future<DartDevelopmentService> startDds(Uri uri) async {
_ddsServiceUriCompleter.complete(uri);
final MockDartDevelopmentService mock = MockDartDevelopmentService();
when(mock.uri).thenReturn(Uri.parse('http://localhost:${debuggingOptions.hostVmServicePort}'));
return mock;
}
@override
Future<HttpServer> bind(InternetAddress host, int port) async => MockHttpServer();
@override
Future<StreamChannel<String>> get remoteChannel async => StreamChannelController<String>().foreign;
}
class MockDartDevelopmentService extends Mock implements DartDevelopmentService {}
class MockHttpServer extends Mock implements HttpServer {}
class MockLogger extends Mock implements Logger {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {
@override
Future<int> get exitCode async => 0;
@override
Stream<List<int>> get stdout => const Stream<List<int>>.empty();
@override
Stream<List<int>> get stderr => const Stream<List<int>>.empty();
}
......@@ -5,19 +5,22 @@
// @dart = 2.8
import 'package:flutter_tools/src/test/event_printer.dart';
import 'package:flutter_tools/src/test/watcher.dart';
import 'package:flutter_tools/src/test/test_device.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/fakes.dart';
void main() {
testWithoutContext('EventPrinter handles a null parent', () {
final EventPrinter eventPrinter = EventPrinter(out: StringBuffer());
final ProcessEvent processEvent = ProcessEvent(0, FakeProcess());
final _Device device = _Device();
final Uri observatoryUri = Uri.parse('http://localhost:1234');
expect(() => eventPrinter.handleFinishedTest(processEvent), returnsNormally);
expect(() => eventPrinter.handleStartedProcess(processEvent), returnsNormally);
expect(() => eventPrinter.handleTestCrashed(processEvent), returnsNormally);
expect(() => eventPrinter.handleTestTimedOut(processEvent), returnsNormally);
expect(() => eventPrinter.handleFinishedTest(device), returnsNormally);
expect(() => eventPrinter.handleStartedDevice(observatoryUri), returnsNormally);
expect(() => eventPrinter.handleTestCrashed(device), returnsNormally);
expect(() => eventPrinter.handleTestTimedOut(device), returnsNormally);
});
}
class _Device extends Mock implements TestDevice {}
......@@ -168,7 +168,7 @@ void main() {
extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String;
if ((!stdout.contains('+1: All tests passed')) ||
(!stdout.contains('test 0: starting shell process')) ||
(!stdout.contains('test 0: Starting flutter_tester process with command')) ||
(!stdout.contains('test 0: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) {
......@@ -185,7 +185,7 @@ void main() {
extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String;
if ((!stdout.contains('+2: All tests passed')) ||
(!stdout.contains('test 0: starting shell process')) ||
(!stdout.contains('test 0: Starting flutter_tester process with command')) ||
(!stdout.contains('test 0: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment