blob: 5de689234c2bce44abe20a8408326d635d92f7ff [file] [log] [blame]
// 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/src/dap/protocol_generated.dart';
import 'package:file/file.dart';
import 'package:flutter_tools/src/cache.dart';
import '../../src/common.dart';
import '../test_data/basic_project.dart';
import '../test_data/compile_error_project.dart';
import '../test_utils.dart';
import 'test_client.dart';
import 'test_support.dart';
void main() {
Directory tempDir;
/*late*/ DapTestSession dap;
final String relativeMainPath = 'lib${fileSystem.path.separator}main.dart';
setUpAll(() {
Cache.flutterRoot = getFlutterRoot();
});
setUp(() async {
tempDir = createResolvedTempDirectorySync('flutter_adapter_test.');
dap = await DapTestSession.setUp();
});
tearDown(() async {
await dap.tearDown();
tryToDelete(tempDir);
});
testWithoutContext('can run and terminate a Flutter app in debug mode', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Once the "topLevelFunction" output arrives, we can terminate the app.
unawaited(
dap.client.outputEvents
.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
.whenComplete(() => dap.client.terminate()),
);
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
launch: () => dap.client
.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
);
final String output = _uniqueOutputLines(outputEvents);
expectLines(output, <Object>[
'Launching $relativeMainPath on Flutter test device in debug mode...',
startsWith('Connecting to VM Service at'),
'topLevelFunction',
'',
startsWith('Exited'),
]);
});
testWithoutContext('can run and terminate a Flutter app in noDebug mode', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Once the "topLevelFunction" output arrives, we can terminate the app.
unawaited(
dap.client.outputEvents
.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
.whenComplete(() => dap.client.terminate()),
);
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
launch: () => dap.client
.launch(
cwd: _project.dir.path,
noDebug: true,
toolArgs: <String>['-d', 'flutter-tester'],
),
);
final String output = _uniqueOutputLines(outputEvents);
expectLines(output, <Object>[
'Launching $relativeMainPath on Flutter test device in debug mode...',
'topLevelFunction',
'',
startsWith('Exited'),
]);
});
testWithoutContext('correctly outputs launch errors and terminates', () async {
final CompileErrorProject _project = CompileErrorProject();
await _project.setUpIn(tempDir);
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
launch: () => dap.client
.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
);
final String output = _uniqueOutputLines(outputEvents);
expect(output, contains('this code does not compile'));
expect(output, contains('Exception: Failed to build'));
expect(output, contains('Exited (1)'));
});
testWithoutContext('can hot reload', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Launch the app and wait for it to print "topLevelFunction".
await Future.wait(<Future<Object>>[
dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
dap.client.start(
launch: () => dap.client.launch(
cwd: _project.dir.path,
noDebug: true,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Capture the next two output events that we expect to be the Reload
// notification and then topLevelFunction being printed again.
final Future<List<String>> outputEventsFuture = dap.client.output
// But skip any topLevelFunctions that come before the reload.
.skipWhile((String output) => output.startsWith('topLevelFunction'))
.take(2)
.toList();
await dap.client.hotReload();
expectLines(
(await outputEventsFuture).join(),
<Object>[
startsWith('Reloaded'),
'topLevelFunction',
],
);
await dap.client.terminate();
});
testWithoutContext('can hot restart', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Launch the app and wait for it to print "topLevelFunction".
await Future.wait(<Future<Object>>[
dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
dap.client.start(
launch: () => dap.client.launch(
cwd: _project.dir.path,
noDebug: true,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Capture the next two output events that we expect to be the Restart
// notification and then topLevelFunction being printed again.
final Future<List<String>> outputEventsFuture = dap.client.output
// But skip any topLevelFunctions that come before the restart.
.skipWhile((String output) => output.startsWith('topLevelFunction'))
.take(2)
.toList();
await dap.client.hotRestart();
expectLines(
(await outputEventsFuture).join(),
<Object>[
startsWith('Restarted application'),
'topLevelFunction',
],
);
await dap.client.terminate();
});
testWithoutContext('can hot restart when exceptions occur on outgoing isolates', () async {
final BasicProjectThatThrows _project = BasicProjectThatThrows();
await _project.setUpIn(tempDir);
// Launch the app and wait for it to stop at an exception.
int originalThreadId, newThreadId;
await Future.wait(<Future<Object>>[
// Capture the thread ID of the stopped thread.
dap.client.stoppedEvents.first.then((StoppedEventBody event) => originalThreadId = event.threadId),
dap.client.start(
exceptionPauseMode: 'All', // Ensure we stop on all exceptions
launch: () => dap.client.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Hot restart, ensuring it completes and capturing the ID of the new thread
// to pause.
await Future.wait(<Future<Object>>[
// Capture the thread ID of the newly stopped thread.
dap.client.stoppedEvents.first.then((StoppedEventBody event) => newThreadId = event.threadId),
dap.client.hotRestart(),
], eagerError: true);
// We should not have stopped on the original thread, but the new thread
// from after the restart.
expect(newThreadId, isNot(equals(originalThreadId)));
await dap.client.terminate();
});
testWithoutContext('sends events for extension state updates', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
const String debugPaintRpc = 'ext.flutter.debugPaint';
// Create a future to capture the isolate ID when the debug paint service
// extension loads, as we'll need that to call it later.
final Future<String> isolateIdForDebugPaint = dap.client
.serviceExtensionAdded(debugPaintRpc)
.then((Map<String, Object/*?*/> body) => body['isolateId'] as String);
// Launch the app and wait for it to print "topLevelFunction" so we know
// it's up and running.
await Future.wait(<Future<Object>>[
dap.client.outputEvents.firstWhere((OutputEventBody output) =>
output.output.startsWith('topLevelFunction')),
dap.client.start(
launch: () => dap.client.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Capture the next relevant state-change event (which should occur as a
// result of the call below).
final Future<Map<String, Object/*?*/>> stateChangeEventFuture =
dap.client.serviceExtensionStateChanged(debugPaintRpc);
// Enable debug paint to trigger the state change.
await dap.client.custom(
'callService',
<String, Object/*?*/>{
'method': debugPaintRpc,
'params': <String, Object/*?*/>{
'enabled': true,
'isolateId': await isolateIdForDebugPaint,
},
},
);
// Ensure the event occurred, and its value was as expected.
final Map<String, Object/*?*/> stateChangeEvent = await stateChangeEventFuture;
expect(stateChangeEvent['value'], 'true'); // extension state change values are always strings
await dap.client.terminate();
});
}
/// Extracts the output from a set of [OutputEventBody], removing any
/// adjacent duplicates and combining into a single string.
String _uniqueOutputLines(List<OutputEventBody> outputEvents) {
String/*?*/ lastItem;
return outputEvents
.map((OutputEventBody e) => e.output)
.where((String output) {
// Skip the item if it's the same as the previous one.
final bool isDupe = output == lastItem;
lastItem = output;
return !isDupe;
})
.join();
}