Unverified Commit 234952cf authored by Jia Hao's avatar Jia Hao Committed by GitHub

[flutter_tools] Simplify `flutter test` internals (#74176)

parent 7c22f0d2
...@@ -225,9 +225,7 @@ autoUpdateGoldenFiles = $updateGoldens; ...@@ -225,9 +225,7 @@ autoUpdateGoldenFiles = $updateGoldens;
return buffer.toString(); return buffer.toString();
} }
enum InitialResult { crashed, connected } enum _TestHarnessStatus { testerCrashed, finished }
enum TestResult { crashed, harnessBailed, testBailed }
typedef Finalizer = Future<void> Function(); typedef Finalizer = Future<void> Function();
...@@ -466,7 +464,7 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -466,7 +464,7 @@ class FlutterPlatform extends PlatformPlugin {
// We expect SIGKILL (9) because we tried to terminate it. // We expect SIGKILL (9) because we tried to terminate it.
// It's negative because signals are returned as negative exit codes. // It's negative because signals are returned as negative exit codes.
final String message = _getErrorMessage( final String message = _getErrorMessage(
_getExitCodeMessage(exitCode, 'after tests finished'), _getExitCodeMessage(exitCode),
testPath, testPath,
shellPath); shellPath);
controller.sink.addError(message); controller.sink.addError(message);
...@@ -474,21 +472,22 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -474,21 +472,22 @@ class FlutterPlatform extends PlatformPlugin {
} }
}); });
final Completer<void> gotProcessObservatoryUri = Completer<void>(); final Completer<Uri> gotProcessObservatoryUri = Completer<Uri>();
if (!enableObservatory) { if (!enableObservatory) {
gotProcessObservatoryUri.complete(); gotProcessObservatoryUri.complete();
} }
// Pipe stdout and stderr from the subprocess to our printStatus console. // Pipe stdout and stderr from the subprocess to our printStatus console.
// We also keep track of what observatory port the engine used, if any. // We also keep track of what observatory port the engine used, if any.
Uri processObservatoryUri;
final Uri ddsServiceUri = getDdsServiceUri(); final Uri ddsServiceUri = getDdsServiceUri();
_pipeStandardStreamsToConsole( _pipeStandardStreamsToConsole(
process, process,
reportObservatoryUri: (Uri detectedUri) async { reportObservatoryUri: (Uri detectedUri) async {
assert(processObservatoryUri == null); assert(!gotProcessObservatoryUri.isCompleted);
assert(explicitObservatoryPort == null || assert(explicitObservatoryPort == null ||
explicitObservatoryPort == detectedUri.port); explicitObservatoryPort == detectedUri.port);
Uri forwardingUri;
if (!disableDds) { if (!disableDds) {
final DartDevelopmentService dds = await DartDevelopmentService.startDartDevelopmentService( final DartDevelopmentService dds = await DartDevelopmentService.startDartDevelopmentService(
detectedUri, detectedUri,
...@@ -496,30 +495,26 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -496,30 +495,26 @@ class FlutterPlatform extends PlatformPlugin {
enableAuthCodes: !disableServiceAuthCodes, enableAuthCodes: !disableServiceAuthCodes,
ipv6: host.type == InternetAddressType.IPv6, ipv6: host.type == InternetAddressType.IPv6,
); );
processObservatoryUri = dds.uri; forwardingUri = dds.uri;
globals.printTrace('Dart Development Service started at ${dds.uri}, forwarding to VM service at ${dds.remoteVmServiceUri}.'); globals.printTrace('Dart Development Service started at ${dds.uri}, forwarding to VM service at ${dds.remoteVmServiceUri}.');
} else { } else {
processObservatoryUri = detectedUri; forwardingUri = detectedUri;
} }
{ {
globals.printTrace('Connecting to service protocol: $processObservatoryUri'); globals.printTrace('Connecting to service protocol: $forwardingUri');
final Future<vm_service.VmService> localVmService = connectToVmService(processObservatoryUri, final Future<vm_service.VmService> localVmService = connectToVmService(forwardingUri,
compileExpression: _compileExpressionService); compileExpression: _compileExpressionService);
unawaited(localVmService.then((vm_service.VmService vmservice) { unawaited(localVmService.then((vm_service.VmService vmservice) {
globals.printTrace('Successfully connected to service protocol: $processObservatoryUri'); globals.printTrace('Successfully connected to service protocol: $forwardingUri');
})); }));
} }
if (startPaused && !machine) { if (startPaused && !machine) {
globals.printStatus('The test process has been started.'); globals.printStatus('The test process has been started.');
globals.printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:'); globals.printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
globals.printStatus(' $processObservatoryUri'); globals.printStatus(' $forwardingUri');
globals.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.'); globals.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
} else {
globals.printTrace('test $ourTestCount: using observatory uri $processObservatoryUri from pid ${process.pid}');
} }
gotProcessObservatoryUri.complete(); gotProcessObservatoryUri.complete(forwardingUri);
watcher?.handleStartedProcess(
ProcessEvent(ourTestCount, process, processObservatoryUri));
}, },
); );
...@@ -527,124 +522,52 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -527,124 +522,52 @@ class FlutterPlatform extends PlatformPlugin {
// The engine could crash, in which case process.exitCode will complete. // The engine could crash, in which case process.exitCode will complete.
// The engine could connect to us, in which case webSocket.future will complete. // The engine could connect to us, in which case webSocket.future will complete.
// The local test harness could get bored of us. // The local test harness could get bored of us.
globals.printTrace('test $ourTestCount: awaiting initial result for pid ${process.pid}'); globals.printTrace('test $ourTestCount: awaiting connection to result for test process at pid ${process.pid}');
final InitialResult initialResult = await Future.any<InitialResult>(<Future<InitialResult>>[ final _TestHarnessStatus testHarnessStatus = await Future.any<_TestHarnessStatus>(<Future<_TestHarnessStatus>>[
process.exitCode.then<InitialResult>((int exitCode) => InitialResult.crashed), process.exitCode.then<_TestHarnessStatus>((int exitCode) => _TestHarnessStatus.testerCrashed),
gotProcessObservatoryUri.future.then<InitialResult>((void value) { gotProcessObservatoryUri.future.then<_TestHarnessStatus>((Uri processObservatoryUri) {
return webSocket.future.then<InitialResult>( if (processObservatoryUri != null) {
(WebSocket webSocket) => InitialResult.connected, globals.printTrace('test $ourTestCount: Observatory uri is available at $processObservatoryUri');
); }
}), watcher?.handleStartedProcess(ProcessEvent(ourTestCount, process, processObservatoryUri));
]);
return webSocket.future.then<_TestHarnessStatus>((WebSocket remoteSocket) async {
switch (initialResult) { globals.printTrace('test $ourTestCount: connected to test harness, now awaiting test result');
case InitialResult.crashed: await _controlTests(
globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed before connecting to test harness'); controller: controller,
final int exitCode = await process.exitCode; remoteSocket: remoteSocket,
subprocessActive = false; onError: (dynamic error, StackTrace stackTrace) {
final String message = _getErrorMessage(
_getExitCodeMessage(
exitCode, 'before connecting to test harness'),
testPath,
shellPath);
controller.sink.addError(message);
// Awaited for with 'sink.done' below.
unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
await watcher?.handleTestCrashed(ProcessEvent(ourTestCount, process));
break;
case InitialResult.connected:
globals.printTrace('test $ourTestCount: process with pid ${process.pid} connected to test harness');
final WebSocket testSocket = await webSocket.future;
final Completer<void> harnessDone = Completer<void>();
final StreamSubscription<dynamic> harnessToTest =
controller.stream.listen(
(dynamic event) {
testSocket.add(json.encode(event));
},
onDone: harnessDone.complete,
onError: (dynamic error, StackTrace stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well. // If you reach here, it's unlikely we're going to be able to really handle this well.
globals.printError('test harness controller stream experienced an unexpected error\ntest: $testPath\nerror: $error'); globals.printError('test: $testPath\nerror: $error');
if (!controllerSinkClosed) { if (!controllerSinkClosed) {
controller.sink.addError(error, stack); controller.sink.addError(error, stackTrace);
controller.sink.close(); controller.sink.close();
} else { } else {
globals.printError('unexpected error from test harness controller stream: $error'); globals.printError('unexpected error: $error');
} }
},
cancelOnError: true,
);
final Completer<void> testDone = Completer<void>();
final StreamSubscription<dynamic> testToHarness = testSocket.listen(
(dynamic encodedEvent) {
assert(encodedEvent is String); // we shouldn't ever get binary messages
controller.sink.add(json.decode(encodedEvent as String));
},
onDone: testDone.complete,
onError: (dynamic error, StackTrace stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
globals.printError('test socket stream experienced an unexpected error\ntest: $testPath\nerror: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
globals.printError('unexpected error from test socket stream: $error');
} }
},
cancelOnError: true,
); );
globals.printTrace('test $ourTestCount: awaiting test result for pid ${process.pid}'); await watcher?.handleFinishedTest(ProcessEvent(ourTestCount, process, processObservatoryUri));
final TestResult testResult = await Future.any<TestResult>(<Future<TestResult>>[ return _TestHarnessStatus.finished;
process.exitCode.then<TestResult>((int exitCode) { });
return TestResult.crashed; })
}),
harnessDone.future.then<TestResult>((void value) {
return TestResult.harnessBailed;
}),
testDone.future.then<TestResult>((void value) {
return TestResult.testBailed;
}),
]);
await Future.wait<void>(<Future<void>>[
harnessToTest.cancel(),
testToHarness.cancel(),
]); ]);
switch (testResult) { if (testHarnessStatus == _TestHarnessStatus.testerCrashed) {
case TestResult.crashed:
globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed'); globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
final int exitCode = await process.exitCode; final int exitCode = await process.exitCode;
subprocessActive = false; subprocessActive = false;
final String message = _getErrorMessage( final String message = _getErrorMessage(
_getExitCodeMessage( _getExitCodeMessage(exitCode),
exitCode, 'before test harness closed its WebSocket'),
testPath, testPath,
shellPath); shellPath);
controller.sink.addError(message); controller.sink.addError(message);
// Awaited for with 'sink.done' below. // Awaited for with 'sink.done' below in `finally`.
unawaited(controller.sink.close()); unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close'); globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done; await controller.sink.done;
break; await watcher?.handleTestCrashed(ProcessEvent(ourTestCount, process));
case TestResult.harnessBailed:
case TestResult.testBailed:
if (testResult == TestResult.harnessBailed) {
globals.printTrace('test $ourTestCount: process with pid ${process.pid} no longer needed by test harness');
} else {
assert(testResult == TestResult.testBailed);
globals.printTrace('test $ourTestCount: process with pid ${process.pid} no longer needs test harness');
}
await watcher?.handleFinishedTest(
ProcessEvent(ourTestCount, process, processObservatoryUri));
break;
}
break;
} }
} on Exception catch (error, stack) { } on Exception catch (error, stack) {
globals.printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}'); globals.printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
...@@ -878,22 +801,22 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -878,22 +801,22 @@ class FlutterPlatform extends PlatformPlugin {
return '$what\nTest: $testPath\nShell: $shellPath\n\n'; return '$what\nTest: $testPath\nShell: $shellPath\n\n';
} }
String _getExitCodeMessage(int exitCode, String when) { String _getExitCodeMessage(int exitCode) {
switch (exitCode) { switch (exitCode) {
case 1: case 1:
return 'Shell subprocess cleanly reported an error $when. Check the logs above for an error message.'; return 'Shell subprocess cleanly reported an error. Check the logs above for an error message.';
case 0: case 0:
return 'Shell subprocess ended cleanly $when. Did main() call exit()?'; return 'Shell subprocess ended cleanly. Did main() call exit()?';
case -0x0f: // ProcessSignal.SIGTERM case -0x0f: // ProcessSignal.SIGTERM
return 'Shell subprocess crashed with SIGTERM ($exitCode) $when.'; return 'Shell subprocess crashed with SIGTERM ($exitCode).';
case -0x0b: // ProcessSignal.SIGSEGV case -0x0b: // ProcessSignal.SIGSEGV
return 'Shell subprocess crashed with segmentation fault $when.'; return 'Shell subprocess crashed with segmentation fault.';
case -0x06: // ProcessSignal.SIGABRT case -0x06: // ProcessSignal.SIGABRT
return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.'; return 'Shell subprocess crashed with SIGABRT ($exitCode).';
case -0x02: // ProcessSignal.SIGINT case -0x02: // ProcessSignal.SIGINT
return 'Shell subprocess terminated by ^C (SIGINT, $exitCode) $when.'; return 'Shell subprocess terminated by ^C (SIGINT, $exitCode).';
default: default:
return 'Shell subprocess crashed with unexpected exit code $exitCode $when.'; return 'Shell subprocess crashed with unexpected exit code $exitCode.';
} }
} }
...@@ -970,3 +893,61 @@ class _AsyncError { ...@@ -970,3 +893,61 @@ class _AsyncError {
final dynamic error; final dynamic error;
final StackTrace stack; final StackTrace stack;
} }
/// Bridges the package:test controller and the remote tester.
///
/// Sets up a that allows the package:test test [controller] to communicate with
/// a [remoteSocket] that runs the test. The returned future completes when
/// either side is closed, which also indicates when the tests have finished.
Future<void> _controlTests({
@required
StreamChannel<dynamic> controller,
@required
WebSocket remoteSocket,
@required
void Function(dynamic, StackTrace) onError,
}) async {
final Completer<void> harnessDone = Completer<void>();
final StreamSubscription<dynamic> harnessToTest =
controller.stream.listen(
(dynamic event) {
remoteSocket.add(json.encode(event));
},
onDone: harnessDone.complete,
onError: (dynamic error, StackTrace stack) {
globals.printError('test harness controller stream experienced an unexpected error');
onError(error, stack);
},
cancelOnError: true,
);
final Completer<void> testDone = Completer<void>();
final StreamSubscription<dynamic> testToHarness = remoteSocket.listen(
(dynamic encodedEvent) {
assert(encodedEvent is String); // we shouldn't ever get binary messages
controller.sink.add(json.decode(encodedEvent as String));
},
onDone: testDone.complete,
onError: (dynamic error, StackTrace stack) {
globals.printError('test socket stream experienced an unexpected error');
onError(error, stack);
},
cancelOnError: true,
);
globals.printTrace('waiting for test harness or tests to finish');
await Future.any<void>(<Future<void>>[
harnessDone.future.then<void>((void value) {
globals.printTrace('test process is no longer needed by test harness');
}),
testDone.future.then<void>((void value) {
globals.printTrace('test harness is no longer needed by test process');
}),
]);
await Future.wait<void>(<Future<void>>[
harnessToTest.cancel(),
testToHarness.cancel(),
]);
}
...@@ -61,13 +61,18 @@ Future<Map<String, dynamic>> _runTest(String scriptPath) async { ...@@ -61,13 +61,18 @@ Future<Map<String, dynamic>> _runTest(String scriptPath) async {
final String testResults = (await process.stdout final String testResults = (await process.stdout
.transform(utf8.decoder) .transform(utf8.decoder)
.expand((String text) => text.split('\n')) .expand((String text) => text.split('\n'))
.map((String line) { .map<dynamic>((String line) {
try { try {
return jsonDecode(line) as Map<String, dynamic>; return jsonDecode(line);
} on FormatException { } on FormatException {
// Only interested in test events which are JSON. // Only interested in test events which are JSON.
} }
}) })
.expand<Map<String, dynamic>>((dynamic json) {
return json is List<dynamic>
? json.cast()
: <Map<String, dynamic>>[json as Map<String, dynamic>];
})
.where((Map<String, dynamic> testEvent) => .where((Map<String, dynamic> testEvent) =>
testEvent != null && testEvent['type'] == 'print') testEvent != null && testEvent['type'] == 'print')
.map((Map<String, dynamic> printEvent) => printEvent['message'] as String) .map((Map<String, dynamic> printEvent) => printEvent['message'] as String)
......
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