Add support for running tests through debug-adapter (#92587)
* Add support for running tests through debug-adapter * Improve comments about stdout + remove pedantic
This commit is contained in:
parent
7cb43e9580
commit
57dbf7f7e7
@ -28,6 +28,13 @@ class DebugAdapterCommand extends FlutterCommand {
|
|||||||
DebugAdapterCommand({ bool verboseHelp = false}) : hidden = !verboseHelp {
|
DebugAdapterCommand({ bool verboseHelp = false}) : hidden = !verboseHelp {
|
||||||
usesIpv6Flag(verboseHelp: verboseHelp);
|
usesIpv6Flag(verboseHelp: verboseHelp);
|
||||||
addDdsOptions(verboseHelp: verboseHelp);
|
addDdsOptions(verboseHelp: verboseHelp);
|
||||||
|
argParser
|
||||||
|
.addFlag(
|
||||||
|
'test',
|
||||||
|
defaultsTo: false,
|
||||||
|
help: 'Whether to use the "flutter test" debug adapter to run tests'
|
||||||
|
' and emit custom events for test progress/results.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -54,6 +61,7 @@ class DebugAdapterCommand extends FlutterCommand {
|
|||||||
platform: globals.platform,
|
platform: globals.platform,
|
||||||
ipv6: ipv6,
|
ipv6: ipv6,
|
||||||
enableDds: enableDds,
|
enableDds: enableDds,
|
||||||
|
test: boolArg('test') ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
await server.channel.closed;
|
await server.channel.closed;
|
||||||
|
@ -4,7 +4,14 @@ This document is Flutter-specific. For information on the standard Dart DAP impl
|
|||||||
|
|
||||||
Flutter includes support for debugging using [the Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) as an alternative to using the [VM Service](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md) directly, simplying the integration for new editors.
|
Flutter includes support for debugging using [the Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) as an alternative to using the [VM Service](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md) directly, simplying the integration for new editors.
|
||||||
|
|
||||||
The debug adapter is started with the `flutter debug-adapter` command and is intended to be consumed by DAP-compliant tools such as Flutter-specific extensions for editors, or configured by users whose editors include generic configurable DAP clients.
|
The debug adapters are started with the `flutter debug-adapter` command and are intended to be consumed by DAP-compliant tools such as Flutter-specific extensions for editors, or configured by users whose editors include generic configurable DAP clients.
|
||||||
|
|
||||||
|
Two adapters are available:
|
||||||
|
|
||||||
|
- `flutter debug_adapter`
|
||||||
|
- `flutter debug_adapter --test`
|
||||||
|
|
||||||
|
The standard adapter will run applications using `flutter run` while the `--test` adapter will cause scripts to be run using `flutter test` and will emit custom `dart.testNotification` events (described in the [Dart DAP documentation](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md#darttestnotification)).
|
||||||
|
|
||||||
Because in the DAP protocol the client speaks first, running this command from the terminal will result in no output (nor will the process terminate). This is expected behaviour.
|
Because in the DAP protocol the client speaks first, running this command from the terminal will result in no output (nor will the process terminate). This is expected behaviour.
|
||||||
|
|
||||||
|
@ -164,9 +164,7 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
|
|||||||
final List<String> toolArgs = <String>[
|
final List<String> toolArgs = <String>[
|
||||||
'run',
|
'run',
|
||||||
'--machine',
|
'--machine',
|
||||||
if (debug) ...<String>[
|
if (debug) '--start-paused',
|
||||||
'--start-paused',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
final List<String> processArgs = <String>[
|
final List<String> processArgs = <String>[
|
||||||
...toolArgs,
|
...toolArgs,
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
// 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:dds/dap.dart' hide PidTracker, PackageConfigUtils;
|
||||||
|
import 'package:vm_service/vm_service.dart' as vm;
|
||||||
|
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../base/io.dart';
|
||||||
|
import '../base/platform.dart';
|
||||||
|
import '../cache.dart';
|
||||||
|
import '../convert.dart';
|
||||||
|
import 'flutter_adapter_args.dart';
|
||||||
|
import 'mixins.dart';
|
||||||
|
|
||||||
|
/// A DAP Debug Adapter for running and debugging Flutter tests.
|
||||||
|
class FlutterTestDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments, FlutterAttachRequestArguments>
|
||||||
|
with PidTracker, PackageConfigUtils, TestAdapter {
|
||||||
|
FlutterTestDebugAdapter(
|
||||||
|
ByteStreamServerChannel channel, {
|
||||||
|
required this.fileSystem,
|
||||||
|
required this.platform,
|
||||||
|
bool ipv6 = false,
|
||||||
|
bool enableDds = true,
|
||||||
|
bool enableAuthCodes = true,
|
||||||
|
Logger? logger,
|
||||||
|
}) : super(
|
||||||
|
channel,
|
||||||
|
ipv6: ipv6,
|
||||||
|
enableDds: enableDds,
|
||||||
|
enableAuthCodes: enableAuthCodes,
|
||||||
|
logger: logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FileSystem fileSystem;
|
||||||
|
Platform platform;
|
||||||
|
Process? _process;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final FlutterLaunchRequestArguments Function(Map<String, Object?> obj)
|
||||||
|
parseLaunchArgs = FlutterLaunchRequestArguments.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final FlutterAttachRequestArguments Function(Map<String, Object?> obj)
|
||||||
|
parseAttachArgs = FlutterAttachRequestArguments.fromJson;
|
||||||
|
|
||||||
|
/// Whether the VM Service closing should be used as a signal to terminate the debug session.
|
||||||
|
///
|
||||||
|
/// Since we do not support attaching for tests, this is always false.
|
||||||
|
@override
|
||||||
|
bool get terminateOnVmServiceClose => false;
|
||||||
|
|
||||||
|
/// Called by [attachRequest] to request that we actually connect to the app to be debugged.
|
||||||
|
@override
|
||||||
|
Future<void> attachImpl() async {
|
||||||
|
sendOutput('console', '\nAttach is not currently supported');
|
||||||
|
handleSessionTerminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> debuggerConnected(vm.VM vmInfo) async {
|
||||||
|
// Capture the PID from the VM Service so that we can terminate it when
|
||||||
|
// cleaning up. Terminating the process might not be enough as it could be
|
||||||
|
// just a shell script (e.g. pub on Windows) and may not pass the
|
||||||
|
// signal on correctly.
|
||||||
|
// See: https://github.com/Dart-Code/Dart-Code/issues/907
|
||||||
|
final int? pid = vmInfo.pid;
|
||||||
|
if (pid != null) {
|
||||||
|
pidsToTerminate.add(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by [disconnectRequest] to request that we forcefully shut down the app being run (or in the case of an attach, disconnect).
|
||||||
|
///
|
||||||
|
/// Client IDEs/editors should send a terminateRequest before a
|
||||||
|
/// disconnectRequest to allow a graceful shutdown. This method must terminate
|
||||||
|
/// quickly and therefore may leave orphaned processes.
|
||||||
|
@override
|
||||||
|
Future<void> disconnectImpl() async {
|
||||||
|
terminatePids(ProcessSignal.sigkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by [launchRequest] to request that we actually start the tests to be run/debugged.
|
||||||
|
///
|
||||||
|
/// For debugging, this should start paused, connect to the VM Service, set
|
||||||
|
/// breakpoints, and resume.
|
||||||
|
@override
|
||||||
|
Future<void> launchImpl() async {
|
||||||
|
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
|
||||||
|
final String flutterToolPath = fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
|
||||||
|
|
||||||
|
final bool debug = !(args.noDebug ?? false);
|
||||||
|
final String? program = args.program;
|
||||||
|
|
||||||
|
final List<String> toolArgs = <String>[
|
||||||
|
'test',
|
||||||
|
'--machine',
|
||||||
|
if (debug) '--start-paused',
|
||||||
|
];
|
||||||
|
final List<String> processArgs = <String>[
|
||||||
|
...toolArgs,
|
||||||
|
...?args.toolArgs,
|
||||||
|
if (program != null) program,
|
||||||
|
...?args.args,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Find the package_config file for this script. This is used by the
|
||||||
|
// debugger to map package: URIs to file paths to check whether they're in
|
||||||
|
// the editors workspace (args.cwd/args.additionalProjectPaths) so they can
|
||||||
|
// be correctly classes as "my code", "sdk" or "external packages".
|
||||||
|
// TODO(dantup): Remove this once https://github.com/dart-lang/sdk/issues/45530
|
||||||
|
// is done as it will not be necessary.
|
||||||
|
final String? possibleRoot = program == null
|
||||||
|
? args.cwd
|
||||||
|
: fileSystem.path.isAbsolute(program)
|
||||||
|
? fileSystem.path.dirname(program)
|
||||||
|
: fileSystem.path.dirname(
|
||||||
|
fileSystem.path.normalize(fileSystem.path.join(args.cwd ?? '', args.program)));
|
||||||
|
if (possibleRoot != null) {
|
||||||
|
final File? packageConfig = findPackageConfigFile(possibleRoot);
|
||||||
|
if (packageConfig != null) {
|
||||||
|
usePackageConfigFile(packageConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.call('Spawning $flutterToolPath with $processArgs in ${args.cwd}');
|
||||||
|
final Process process = await Process.start(
|
||||||
|
flutterToolPath,
|
||||||
|
processArgs,
|
||||||
|
workingDirectory: args.cwd,
|
||||||
|
);
|
||||||
|
_process = process;
|
||||||
|
pidsToTerminate.add(process.pid);
|
||||||
|
|
||||||
|
process.stdout.transform(ByteToLineTransformer()).listen(_handleStdout);
|
||||||
|
process.stderr.listen(_handleStderr);
|
||||||
|
unawaited(process.exitCode.then(_handleExitCode));
|
||||||
|
|
||||||
|
// Delay responding until the debugger is connected.
|
||||||
|
if (debug) {
|
||||||
|
await debuggerInitialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
|
||||||
|
@override
|
||||||
|
Future<void> terminateImpl() async {
|
||||||
|
terminatePids(ProcessSignal.sigterm);
|
||||||
|
await _process?.exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the Flutter process exiting, terminating the debug session if it has not already begun terminating.
|
||||||
|
void _handleExitCode(int code) {
|
||||||
|
final String codeSuffix = code == 0 ? '' : ' ($code)';
|
||||||
|
logger?.call('Process exited ($code)');
|
||||||
|
handleSessionTerminate(codeSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles incoming JSON events from `flutter test --machine`.
|
||||||
|
bool _handleJsonEvent(String event, Map<String, Object?>? params) {
|
||||||
|
params ??= <String, Object?>{};
|
||||||
|
switch (event) {
|
||||||
|
case 'test.startedProcess':
|
||||||
|
_handleTestStartedProcess(params);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleStderr(List<int> data) {
|
||||||
|
logger?.call('stderr: $data');
|
||||||
|
sendOutput('stderr', utf8.decode(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles stdout from the `flutter test --machine` process, decoding the JSON and calling the appropriate handlers.
|
||||||
|
void _handleStdout(String data) {
|
||||||
|
// Output to stdout from `flutter test --machine` is either:
|
||||||
|
// 1. JSON output from flutter_tools (eg. "test.startedProcess") which is
|
||||||
|
// wrapped in [] brackets and has an event/params.
|
||||||
|
// 2. JSON output from package:test (not wrapped in brackets).
|
||||||
|
// 3. Non-JSON output (user messages, or flutter_tools printing things like
|
||||||
|
// call stacks/error information).
|
||||||
|
logger?.call('stdout: $data');
|
||||||
|
|
||||||
|
Object? jsonData;
|
||||||
|
try {
|
||||||
|
jsonData = jsonDecode(data);
|
||||||
|
} on FormatException {
|
||||||
|
// If the output wasn't valid JSON, it was standard stdout that should
|
||||||
|
// be passed through to the user.
|
||||||
|
sendOutput('stdout', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid flutter_tools JSON output (1) first.
|
||||||
|
final Map<String, Object?>? flutterPayload = jsonData is List &&
|
||||||
|
jsonData.length == 1 &&
|
||||||
|
jsonData.first is Map<String, Object?>
|
||||||
|
? jsonData.first as Map<String, Object?>
|
||||||
|
: null;
|
||||||
|
final Object? event = flutterPayload?['event'];
|
||||||
|
final Object? params = flutterPayload?['params'];
|
||||||
|
|
||||||
|
if (event is String && params is Map<String, Object?>?) {
|
||||||
|
_handleJsonEvent(event, params);
|
||||||
|
} else if (jsonData != null) {
|
||||||
|
// Handle package:test output (2).
|
||||||
|
sendTestEvents(jsonData);
|
||||||
|
} else {
|
||||||
|
// Other output should just be passed straight through.
|
||||||
|
sendOutput('stdout', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the test.processStarted event from Flutter that provides the VM Service URL.
|
||||||
|
void _handleTestStartedProcess(Map<String, Object?> params) {
|
||||||
|
final String? vmServiceUriString = params['observatoryUri'] as String?;
|
||||||
|
// For no-debug mode, this event is still sent, but has a null URI.
|
||||||
|
if (vmServiceUriString == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Uri vmServiceUri = Uri.parse(vmServiceUriString);
|
||||||
|
connectDebugger(vmServiceUri, resumeIfStarting: true);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import '../base/file_system.dart';
|
|||||||
import '../base/platform.dart';
|
import '../base/platform.dart';
|
||||||
import '../debug_adapters/flutter_adapter.dart';
|
import '../debug_adapters/flutter_adapter.dart';
|
||||||
import '../debug_adapters/flutter_adapter_args.dart';
|
import '../debug_adapters/flutter_adapter_args.dart';
|
||||||
|
import 'flutter_test_adapter.dart';
|
||||||
|
|
||||||
/// A DAP server that communicates over a [ByteStreamServerChannel], usually constructed from the processes stdin/stdout streams.
|
/// A DAP server that communicates over a [ByteStreamServerChannel], usually constructed from the processes stdin/stdout streams.
|
||||||
///
|
///
|
||||||
@ -27,15 +28,23 @@ class DapServer {
|
|||||||
this.ipv6 = false,
|
this.ipv6 = false,
|
||||||
this.enableDds = true,
|
this.enableDds = true,
|
||||||
this.enableAuthCodes = true,
|
this.enableAuthCodes = true,
|
||||||
|
bool test = false,
|
||||||
this.logger,
|
this.logger,
|
||||||
}) : channel = ByteStreamServerChannel(_input, _output, logger) {
|
}) : channel = ByteStreamServerChannel(_input, _output, logger) {
|
||||||
adapter = FlutterDebugAdapter(channel,
|
adapter = test
|
||||||
fileSystem: fileSystem,
|
? FlutterTestDebugAdapter(channel,
|
||||||
platform: platform,
|
fileSystem: fileSystem,
|
||||||
ipv6: ipv6,
|
platform: platform,
|
||||||
enableDds: enableDds,
|
ipv6: ipv6,
|
||||||
enableAuthCodes: enableAuthCodes,
|
enableDds: enableDds,
|
||||||
logger: logger);
|
enableAuthCodes: enableAuthCodes,
|
||||||
|
logger: logger)
|
||||||
|
: FlutterDebugAdapter(channel,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
platform: platform,
|
||||||
|
enableDds: enableDds,
|
||||||
|
enableAuthCodes: enableAuthCodes,
|
||||||
|
logger: logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ByteStreamServerChannel channel;
|
final ByteStreamServerChannel channel;
|
||||||
|
@ -27,7 +27,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
tempDir = createResolvedTempDirectorySync('debug_adapter_test.');
|
tempDir = createResolvedTempDirectorySync('flutter_adapter_test.');
|
||||||
dap = await DapTestSession.setUp();
|
dap = await DapTestSession.setUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
// 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 '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/tests_project.dart';
|
||||||
|
import '../test_utils.dart';
|
||||||
|
import 'test_client.dart';
|
||||||
|
import 'test_support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Directory tempDir;
|
||||||
|
/*late*/ DapTestSession dap;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
Cache.flutterRoot = getFlutterRoot();
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
tempDir = createResolvedTempDirectorySync('flutter_test_adapter_test.');
|
||||||
|
dap = await DapTestSession.setUp(additionalArgs: <String>['--test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await dap.tearDown();
|
||||||
|
tryToDelete(tempDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can run in debug mode', () async {
|
||||||
|
final DapTestClient client = dap.client;
|
||||||
|
final TestsProject project = TestsProject();
|
||||||
|
await project.setUpIn(tempDir);
|
||||||
|
|
||||||
|
// Collect output and test events while running the script.
|
||||||
|
final TestEvents outputEvents = await client.collectTestOutput(
|
||||||
|
launch: () => client.launch(
|
||||||
|
program: project.testFilePath,
|
||||||
|
cwd: project.dir.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check the printed output shows that the run finished, and it's exit
|
||||||
|
// code (which is 1 due to the failing test).
|
||||||
|
final String output = outputEvents.output.map((OutputEventBody e) => e.output).join();
|
||||||
|
expectLines(
|
||||||
|
output,
|
||||||
|
<Object>[
|
||||||
|
startsWith('Connecting to VM Service at'),
|
||||||
|
..._testsProjectExpectedOutput
|
||||||
|
],
|
||||||
|
allowExtras: true, // Allow for printed call stack etc.
|
||||||
|
);
|
||||||
|
|
||||||
|
_expectStandardTestsProjectResults(outputEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can run in noDebug mode', () async {
|
||||||
|
final DapTestClient client = dap.client;
|
||||||
|
final TestsProject project = TestsProject();
|
||||||
|
await project.setUpIn(tempDir);
|
||||||
|
|
||||||
|
// Collect output and test events while running the script.
|
||||||
|
final TestEvents outputEvents = await client.collectTestOutput(
|
||||||
|
launch: () => client.launch(
|
||||||
|
program: project.testFilePath,
|
||||||
|
noDebug: true,
|
||||||
|
cwd: project.dir.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check the printed output shows that the run finished, and it's exit
|
||||||
|
// code (which is 1 due to the failing test).
|
||||||
|
final String output = outputEvents.output.map((OutputEventBody e) => e.output).join();
|
||||||
|
expectLines(
|
||||||
|
output,
|
||||||
|
_testsProjectExpectedOutput,
|
||||||
|
allowExtras: true, // Allow for printed call stack etc.
|
||||||
|
);
|
||||||
|
|
||||||
|
_expectStandardTestsProjectResults(outputEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can run a single test', () async {
|
||||||
|
final DapTestClient client = dap.client;
|
||||||
|
final TestsProject project = TestsProject();
|
||||||
|
await project.setUpIn(tempDir);
|
||||||
|
|
||||||
|
// Collect output and test events while running the script.
|
||||||
|
final TestEvents outputEvents = await client.collectTestOutput(
|
||||||
|
launch: () => client.launch(
|
||||||
|
program: project.testFilePath,
|
||||||
|
noDebug: true,
|
||||||
|
cwd: project.dir.path,
|
||||||
|
// It's up to the calling IDE to pass the correct args for 'dart test'
|
||||||
|
// if it wants to run a subset of tests.
|
||||||
|
args: <String>[
|
||||||
|
'--plain-name',
|
||||||
|
'can pass',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Object> testsNames = outputEvents.testNotifications
|
||||||
|
.where((Map<String, Object>/*?*/ e) => e['type'] == 'testStart')
|
||||||
|
.map((Map<String, Object>/*?*/ e) => (e['test'] as Map<String, Object/*?*/>)['name'])
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
expect(testsNames, contains('Flutter tests can pass'));
|
||||||
|
expect(testsNames, isNot(contains('Flutter tests can fail')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matchers for the expected console output of [TestsProject].
|
||||||
|
final List<Object> _testsProjectExpectedOutput = <Object>[
|
||||||
|
// First test
|
||||||
|
'✓ Flutter tests can pass',
|
||||||
|
// Second test
|
||||||
|
'══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════',
|
||||||
|
'The following TestFailure object was thrown running a test:',
|
||||||
|
' Expected: false',
|
||||||
|
' Actual: <true>',
|
||||||
|
'',
|
||||||
|
'The test description was: can fail',
|
||||||
|
'',
|
||||||
|
'✖ Flutter tests can fail',
|
||||||
|
// Exit
|
||||||
|
'',
|
||||||
|
'Exited (1).',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A helper that verifies a full set of expected test results for the
|
||||||
|
/// [TestsProject] script.
|
||||||
|
void _expectStandardTestsProjectResults(TestEvents events) {
|
||||||
|
// Check we recieved all expected test events passed through from
|
||||||
|
// package:test.
|
||||||
|
final List<Object> eventNames =
|
||||||
|
events.testNotifications.map((Map<String, Object/*?*/> e) => e['type']).toList();
|
||||||
|
|
||||||
|
// start/done should always be first/last.
|
||||||
|
expect(eventNames.first, equals('start'));
|
||||||
|
expect(eventNames.last, equals('done'));
|
||||||
|
|
||||||
|
// allSuites should have occurred after start.
|
||||||
|
expect(
|
||||||
|
eventNames,
|
||||||
|
containsAllInOrder(<String>['start', 'allSuites']),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expect two tests, with the failing one emitting an error.
|
||||||
|
expect(
|
||||||
|
eventNames,
|
||||||
|
containsAllInOrder(<String>[
|
||||||
|
'testStart',
|
||||||
|
'testDone',
|
||||||
|
'testStart',
|
||||||
|
'error',
|
||||||
|
'testDone',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
@ -74,6 +74,12 @@ class DapTestClient {
|
|||||||
return _eventController.stream.where((Event e) => e.event == event);
|
return _eventController.stream.where((Event e) => e.event == event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a stream of 'dart.testNotification' custom events from the
|
||||||
|
/// package:test JSON reporter.
|
||||||
|
Stream<Map<String, Object?>> get testNotificationEvents =>
|
||||||
|
events('dart.testNotification')
|
||||||
|
.map((Event e) => e.body! as Map<String, Object?>);
|
||||||
|
|
||||||
/// Sends a custom request to the debug adapter to trigger a Hot Reload.
|
/// Sends a custom request to the debug adapter to trigger a Hot Reload.
|
||||||
Future<Response> hotReload() {
|
Future<Response> hotReload() {
|
||||||
return custom('hotReload');
|
return custom('hotReload');
|
||||||
@ -220,6 +226,17 @@ class DapTestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Useful events produced by the debug adapter during a debug session.
|
||||||
|
class TestEvents {
|
||||||
|
TestEvents({
|
||||||
|
required this.output,
|
||||||
|
required this.testNotifications,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<OutputEventBody> output;
|
||||||
|
final List<Map<String, Object?>> testNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
class _OutgoingRequest {
|
class _OutgoingRequest {
|
||||||
_OutgoingRequest(this.completer, this.name, this.allowFailure);
|
_OutgoingRequest(this.completer, this.name, this.allowFailure);
|
||||||
|
|
||||||
@ -273,4 +290,39 @@ extension DapTestClientExtension on DapTestClient {
|
|||||||
? output.skipWhile((OutputEventBody output) => output.output.startsWith('Running "flutter pub get"')).toList()
|
? output.skipWhile((OutputEventBody output) => output.output.startsWith('Running "flutter pub get"')).toList()
|
||||||
: output;
|
: output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all output and test events until the program terminates.
|
||||||
|
///
|
||||||
|
/// These results include all events in the order they are recieved, including
|
||||||
|
/// console, stdout, stderr and test notifications from the test JSON reporter.
|
||||||
|
///
|
||||||
|
/// Only one of [start] or [launch] may be provided. Use [start] to customise
|
||||||
|
/// the whole start of the session (including initialise) or [launch] to only
|
||||||
|
/// customise the [launchRequest].
|
||||||
|
Future<TestEvents> collectTestOutput({
|
||||||
|
String? program,
|
||||||
|
String? cwd,
|
||||||
|
Future<Response> Function()? start,
|
||||||
|
Future<Object?> Function()? launch,
|
||||||
|
}) async {
|
||||||
|
assert(
|
||||||
|
start == null || launch == null,
|
||||||
|
'Only one of "start" or "launch" may be provided',
|
||||||
|
);
|
||||||
|
|
||||||
|
final Future<List<OutputEventBody>> outputEventsFuture = outputEvents.toList();
|
||||||
|
final Future<List<Map<String, Object?>>> testNotificationEventsFuture = testNotificationEvents.toList();
|
||||||
|
|
||||||
|
if (start != null) {
|
||||||
|
await start();
|
||||||
|
} else {
|
||||||
|
await this.start(program: program, cwd: cwd, launch: launch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TestEvents(
|
||||||
|
output: await outputEventsFuture,
|
||||||
|
testNotifications: await testNotificationEventsFuture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,22 @@ final bool verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true';
|
|||||||
|
|
||||||
/// Expects the lines in [actual] to match the relevant matcher in [expected],
|
/// Expects the lines in [actual] to match the relevant matcher in [expected],
|
||||||
/// ignoring differences in line endings and trailing whitespace.
|
/// ignoring differences in line endings and trailing whitespace.
|
||||||
void expectLines(String actual, List<Object> expected) {
|
void expectLines(
|
||||||
expect(
|
String actual,
|
||||||
actual.replaceAll('\r\n', '\n').trim().split('\n'),
|
List<Object> expected, {
|
||||||
equals(expected),
|
bool allowExtras = false,
|
||||||
);
|
}) {
|
||||||
|
if (allowExtras) {
|
||||||
|
expect(
|
||||||
|
actual.replaceAll('\r\n', '\n').trim().split('\n'),
|
||||||
|
containsAllInOrder(expected),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(
|
||||||
|
actual.replaceAll('\r\n', '\n').trim().split('\n'),
|
||||||
|
equals(expected),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A helper class containing the DAP server/client for DAP integration tests.
|
/// A helper class containing the DAP server/client for DAP integration tests.
|
||||||
|
@ -34,8 +34,13 @@ class TestsProject extends Project {
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Hello world test', (WidgetTester tester) async {
|
group('Flutter tests', () {
|
||||||
expect(true, isTrue); // BREAKPOINT
|
testWidgets('can pass', (WidgetTester tester) async {
|
||||||
|
expect(true, isTrue); // BREAKPOINT
|
||||||
|
});
|
||||||
|
testWidgets('can fail', (WidgetTester tester) async {
|
||||||
|
expect(true, isFalse);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user