flutter/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
Danny Tuppeny 53b87635b0
[flutter_tool] Handle RPCErrorKind.kConnectionDisposed (#164299)
There's currently a lot of code that handles RPC Errors that contain the
text "Service connection disposed" because the error originally did not
have a unique error code.

A new error code was added in
https://dart-review.googlesource.com/c/sdk/+/381501 but it's not
currently used because it won't be caught by existing code.

This change updates all places that check for this text, and now also
handle the new error code in preperation for the code changing in
future.

See https://github.com/flutter/flutter/issues/153471

cc @bkonyi 

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
Issue listed, but this change does not directly fix it, it just prepares
for a related future change that will simplify handling these errors
without string checks
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
2025-03-27 08:51:38 +00:00

1709 lines
56 KiB
Dart

// 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:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.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/base/terminal.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:flutter_tools/src/commands/run.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/web/compile.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart' as analytics;
import 'package:vm_service/vm_service.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
import '../../src/fakes.dart';
import '../../src/package_config.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
setUpAll(() {
Cache.disableLocking();
});
group('run', () {
late BufferLogger logger;
late TestDeviceManager testDeviceManager;
late FileSystem fileSystem;
setUp(() {
logger = BufferLogger.test();
testDeviceManager = TestDeviceManager(logger: logger);
fileSystem = MemoryFileSystem.test();
});
testUsingContext(
'fails when target not found',
() async {
final RunCommand command = RunCommand();
expect(
() => createTestCommandRunner(command).run(<String>['run', '-t', 'abc123', '--no-pub']),
throwsA(
isA<ToolExit>().having(
(ToolExit error) => error.exitCode,
'exitCode',
anyOf(isNull, 1),
),
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
},
);
testUsingContext(
'does not support "--use-application-binary" and "--fast-start"',
() async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true);
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--use-application-binary=app/bar/faz',
'--fast-start',
'--no-pub',
'--show-test-device',
]),
throwsA(
isException.having(
(Exception exception) => exception.toString(),
'toString',
isNot(contains('--fast-start is not supported with --use-application-binary')),
),
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
},
);
testUsingContext(
'Walks upward looking for a pubspec.yaml and succeeds if found',
() async {
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('''
{
"packages": [],
"configVersion": 2
}
''');
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.currentDirectory = fileSystem.directory('a/b/c')..createSync(recursive: true);
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub']),
throwsToolExit(),
);
final BufferLogger bufferLogger = globals.logger as BufferLogger;
expect(
bufferLogger.statusText,
containsIgnoringWhitespace('Changing current working directory to:'),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
},
);
testUsingContext(
'Walks upward looking for a pubspec.yaml and exits if missing',
() async {
fileSystem.currentDirectory = fileSystem.directory('a/b/c')..createSync(recursive: true);
fileSystem.file('lib/main.dart').createSync(recursive: true);
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub']),
throwsToolExit(message: 'No pubspec.yaml file found'),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
},
);
group('run app', () {
late MemoryFileSystem fs;
late Artifacts artifacts;
late FakeAnsiTerminal fakeTerminal;
late analytics.FakeAnalytics fakeAnalytics;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
fakeTerminal = FakeAnsiTerminal();
artifacts = Artifacts.test();
fs = MemoryFileSystem.test();
fs.currentDirectory.childFile('pubspec.yaml').writeAsStringSync('name: my_app');
writePackageConfigFile(directory: fs.currentDirectory, mainLibName: 'my_app');
final Directory libDir = fs.currentDirectory.childDirectory('lib');
libDir.createSync();
final File mainFile = libDir.childFile('main.dart');
mainFile.writeAsStringSync('void main() {}');
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
testUsingContext(
'exits with a user message when no supported devices attached',
() async {
final RunCommand command = RunCommand();
testDeviceManager.devices = <Device>[];
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub', '--no-hot']),
throwsA(isA<ToolExit>().having((ToolExit error) => error.message, 'message', isNull)),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace('No supported devices connected.'),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
},
);
testUsingContext(
'exits and lists available devices when specified device not found',
() async {
final RunCommand command = RunCommand();
final FakeDevice device = FakeDevice(isLocalEmulator: true);
testDeviceManager
..devices = <Device>[device]
..specifiedDeviceId = 'invalid-device-id';
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '-d', 'invalid-device-id', '--no-pub', '--no-hot']),
throwsToolExit(),
);
expect(
testLogger.statusText,
contains("No supported devices found with name or id matching 'invalid-device-id'"),
);
expect(testLogger.statusText, contains('The following devices were found:'));
expect(
testLogger.statusText,
contains('FakeDevice (mobile) • fake_device • ios • (simulator)'),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
},
);
testUsingContext(
'fails when targeted device is not Android with --device-user',
() async {
final FakeDevice device = FakeDevice(isLocalEmulator: true);
testDeviceManager.devices = <Device>[device];
final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates();
await expectLater(
createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--device-user', '10']),
throwsToolExit(
message:
'--device-user is only supported for Android. At least one Android device is required.',
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
Stdio: () => FakeStdio(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
},
);
testUsingContext(
'succeeds when targeted device is an Android device with --device-user',
() async {
final FakeDevice device = FakeDevice(
isLocalEmulator: true,
platformType: PlatformType.android,
);
testDeviceManager.devices = <Device>[device];
final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates();
await createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--device-user', '10']);
// Finishes normally without error.
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
Stdio: () => FakeStdio(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
},
);
testUsingContext(
'shows unsupported devices when no supported devices are found',
() async {
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(
targetPlatform: TargetPlatform.android_arm,
isLocalEmulator: true,
sdkNameAndVersion: 'api-14',
isSupported: false,
);
testDeviceManager.devices = <Device>[mockDevice];
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub', '--no-hot']),
throwsA(isA<ToolExit>().having((ToolExit error) => error.message, 'message', isNull)),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace('No supported devices connected.'),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace(
'The following devices were found, but are not supported by this project:',
),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace(
globals.userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(<Device>[mockDevice]),
),
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
},
);
testUsingContext(
'prints warning when --flavor is used with an unsupported target platform',
() async {
const List<String> runCommand = <String>[
'run',
'--no-pub',
'--no-hot',
'--flavor=vanilla',
'-d',
'all',
];
// Useful for test readability.
// ignore: avoid_redundant_argument_values
final FakeDevice deviceWithoutFlavorSupport = FakeDevice(supportsFlavors: false);
final FakeDevice deviceWithFlavorSupport = FakeDevice(supportsFlavors: true);
testDeviceManager.devices = <Device>[deviceWithoutFlavorSupport, deviceWithFlavorSupport];
await createTestCommandRunner(TestRunCommandThatOnlyValidates()).run(runCommand);
expect(
logger.warningText,
contains(
'--flavor is only supported for Android, macOS, and iOS devices. '
'Flavor-related features may not function properly and could '
'behave differently in a future release.',
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
Logger: () => logger,
},
);
testUsingContext(
'forwards --uninstall-only to DebuggingOptions',
() async {
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(sdkNameAndVersion: 'iOS 13')
..startAppSuccess = false;
testDeviceManager.devices = <Device>[mockDevice];
// Causes swift to be detected in the analytics.
fs.currentDirectory
.childDirectory('ios')
.childFile('AppDelegate.swift')
.createSync(recursive: true);
await expectToolExitLater(
createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--uninstall-first']),
isNull,
);
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.uninstallFirst, isTrue);
},
overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'passes device target platform to analytics',
() async {
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(sdkNameAndVersion: 'iOS 13')
..startAppSuccess = false;
testDeviceManager.devices = <Device>[mockDevice];
// Causes swift to be detected in the analytics.
fs.currentDirectory
.childDirectory('ios')
.childFile('AppDelegate.swift')
.createSync(recursive: true);
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub', '--no-hot']),
isNull,
);
expect(
fakeAnalytics.sentEvents,
contains(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: globals.stdio.hasTerminal,
runIsEmulator: false,
runTargetName: 'ios',
runTargetOsVersion: 'iOS 13',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: 'swift',
runIOSInterfaceType: 'usb',
runIsTest: false,
),
),
);
},
overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
analytics.Analytics: () => fakeAnalytics,
},
);
testUsingContext(
'correctly reports tests to analytics',
() async {
fs.currentDirectory
.childDirectory('test')
.childFile('widget_test.dart')
.createSync(recursive: true);
fs.currentDirectory
.childDirectory('ios')
.childFile('AppDelegate.swift')
.createSync(recursive: true);
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(sdkNameAndVersion: 'iOS 13')
..startAppSuccess = false;
testDeviceManager.devices = <Device>[mockDevice];
await expectToolExitLater(
createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', 'test/widget_test.dart']),
isNull,
);
expect(
fakeAnalytics.sentEvents,
contains(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: globals.stdio.hasTerminal,
runIsEmulator: false,
runTargetName: 'ios',
runTargetOsVersion: 'iOS 13',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: 'swift',
runIOSInterfaceType: 'usb',
runIsTest: true,
),
),
);
},
overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
analytics.Analytics: () => fakeAnalytics,
},
);
group('--machine', () {
testUsingContext(
'can pass --device-user',
() async {
final DaemonCapturingRunCommand command = DaemonCapturingRunCommand();
final FakeDevice device = FakeDevice(platformType: PlatformType.android);
testDeviceManager.devices = <Device>[device];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--machine',
'--device-user',
'10',
'-d',
device.id,
]),
throwsToolExit(),
);
expect(command.appDomain.userIdentifier, '10');
},
overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
Logger: () => AppRunLogger(parent: logger),
},
);
testUsingContext(
'can disable devtools with --no-devtools',
() async {
final DaemonCapturingRunCommand command = DaemonCapturingRunCommand();
final FakeDevice device = FakeDevice();
testDeviceManager.devices = <Device>[device];
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-devtools', '--machine', '-d', device.id]),
throwsToolExit(),
);
expect(command.appDomain.enableDevTools, isFalse);
},
overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
Logger: () => AppRunLogger(parent: logger),
},
);
});
});
group('Fatal Logs', () {
late TestRunCommandWithFakeResidentRunner command;
late MemoryFileSystem fs;
setUp(() {
command = TestRunCommandWithFakeResidentRunner()..fakeResidentRunner = FakeResidentRunner();
fs = MemoryFileSystem.test();
});
testUsingContext(
"doesn't fail if --fatal-warnings specified and no warnings occur",
() async {
try {
await createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--${FlutterOptions.kFatalWarnings}']);
} on Exception {
fail('Unexpected exception thrown');
}
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
"doesn't fail if --fatal-warnings not specified",
() async {
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
try {
await createTestCommandRunner(command).run(<String>['run', '--no-pub', '--no-hot']);
} on Exception {
fail('Unexpected exception thrown');
}
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'fails if --fatal-warnings specified and warnings emitted',
() async {
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
await expectLater(
createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--${FlutterOptions.kFatalWarnings}']),
throwsToolExit(
message:
'Logger received warning output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.',
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'fails if --fatal-warnings specified and errors emitted',
() async {
testLogger.printError('Error: Danger Will Robinson!');
await expectLater(
createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--${FlutterOptions.kFatalWarnings}']),
throwsToolExit(
message:
'Logger received error output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.',
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
},
);
});
testUsingContext(
'should only request artifacts corresponding to connected devices',
() async {
testDeviceManager.devices = <Device>[
FakeDevice(targetPlatform: TargetPlatform.android_arm),
];
expect(
await RunCommand().requiredArtifacts,
unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.androidGenSnapshot,
}),
);
testDeviceManager.devices = <Device>[FakeDevice()];
expect(
await RunCommand().requiredArtifacts,
unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.iOS,
}),
);
testDeviceManager.devices = <Device>[
FakeDevice(),
FakeDevice(targetPlatform: TargetPlatform.android_arm),
];
expect(
await RunCommand().requiredArtifacts,
unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.iOS,
DevelopmentArtifact.androidGenSnapshot,
}),
);
testDeviceManager.devices = <Device>[
FakeDevice(targetPlatform: TargetPlatform.web_javascript),
];
expect(
await RunCommand().requiredArtifacts,
unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.web,
}),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
group('usageValues', () {
testUsingContext(
'with only non-iOS usb device',
() async {
final List<Device> devices = <Device>[
FakeDevice(
targetPlatform: TargetPlatform.android_arm,
platformType: PlatformType.android,
),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(
devices: devices,
);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final analytics.Event usageValues = await command.unifiedAnalyticsUsageValues('run');
expect(
usageValues,
equals(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: false,
runIsEmulator: false,
runTargetName: 'android-arm',
runTargetOsVersion: '',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: '',
runIsTest: false,
),
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'with only iOS usb device',
() async {
final List<Device> devices = <Device>[FakeIOSDevice(sdkNameAndVersion: 'iOS 16.2')];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(
devices: devices,
);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final analytics.Event usageValues = await command.unifiedAnalyticsUsageValues('run');
expect(
usageValues,
equals(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: false,
runIsEmulator: false,
runTargetName: 'ios',
runTargetOsVersion: 'iOS 16.2',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: '',
runIOSInterfaceType: 'usb',
runIsTest: false,
),
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'with only iOS wireless device',
() async {
final List<Device> devices = <Device>[
FakeIOSDevice(
connectionInterface: DeviceConnectionInterface.wireless,
sdkNameAndVersion: 'iOS 16.2',
),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(
devices: devices,
);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final analytics.Event usageValues = await command.unifiedAnalyticsUsageValues('run');
expect(
usageValues,
equals(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: false,
runIsEmulator: false,
runTargetName: 'ios',
runTargetOsVersion: 'iOS 16.2',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: '',
runIOSInterfaceType: 'wireless',
runIsTest: false,
),
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'with both iOS usb and wireless devices',
() async {
final List<Device> devices = <Device>[
FakeIOSDevice(
connectionInterface: DeviceConnectionInterface.wireless,
sdkNameAndVersion: 'iOS 16.2',
),
FakeIOSDevice(sdkNameAndVersion: 'iOS 16.2'),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(
devices: devices,
);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final analytics.Event usageValues = await command.unifiedAnalyticsUsageValues('run');
expect(
usageValues,
equals(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: false,
runIsEmulator: false,
runTargetName: 'multiple',
runTargetOsVersion: 'multiple',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: '',
runIOSInterfaceType: 'wireless',
runIsTest: false,
),
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
});
group('--web-header', () {
setUp(() {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('''
{
"packages": [],
"configVersion": 2
}
''');
final FakeDevice device = FakeDevice(
isLocalEmulator: true,
platformType: PlatformType.android,
);
testDeviceManager.devices = <Device>[device];
});
testUsingContext(
'can accept simple, valid values',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--web-header', 'foo = bar']),
throwsToolExit(),
);
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webHeaders, <String, String>{'foo': 'bar'});
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
},
);
testUsingContext(
'throws a ToolExit when no value is provided',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--web-header', 'foo']),
throwsToolExit(message: 'Invalid web headers: foo'),
);
await expectLater(() => command.createDebuggingOptions(true), throwsToolExit());
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
},
);
testUsingContext(
'throws a ToolExit when value includes delimiter characters',
() async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true);
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--web-header',
'hurray/headers=flutter',
]),
throwsToolExit(),
);
await expectLater(
() => command.createDebuggingOptions(true),
throwsToolExit(message: 'Invalid web headers: hurray/headers=flutter'),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
},
);
testUsingContext(
'throws a ToolExit when using --wasm on a non-web platform',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub', '--wasm']),
throwsToolExit(message: '--wasm is only supported on the web platform'),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
},
);
testUsingContext(
'throws a ToolExit when using the skwasm renderer without --wasm',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', ...WebRendererMode.skwasm.toCliDartDefines]),
throwsToolExit(message: 'Skwasm renderer requires --wasm'),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
},
);
testUsingContext(
'accepts headers with commas in them',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--web-header',
'hurray=flutter,flutter=hurray',
]),
throwsToolExit(),
);
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webHeaders, <String, String>{'hurray': 'flutter,flutter=hurray'});
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
},
);
});
});
group('terminal', () {
late FakeAnsiTerminal fakeTerminal;
setUp(() {
fakeTerminal = FakeAnsiTerminal();
});
testUsingContext(
'Flutter run sets terminal singleCharMode to false on exit',
() async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await createTestCommandRunner(command).run(<String>['run', '--no-pub']);
// The sync completer where we initially set `terminal.singleCharMode` to
// `true` does not execute in unit tests, so explicitly check the
// `setSingleCharModeHistory` that the finally block ran, setting this
// back to `false`.
expect(fakeTerminal.setSingleCharModeHistory, contains(false));
},
overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'Flutter run catches StdinException while setting terminal singleCharMode to false',
() async {
fakeTerminal.hasStdin = false;
final FakeResidentRunner residentRunner = FakeResidentRunner();
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
try {
await createTestCommandRunner(command).run(<String>['run', '--no-pub']);
} catch (err) {
fail('Expected no error, got $err');
}
expect(fakeTerminal.setSingleCharModeHistory, isEmpty);
},
overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
});
testUsingContext(
'Flutter run catches catches errors due to vm service disconnection by text and throws a tool exit',
() async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kServiceDisappeared.code,
'',
);
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub']),
contains('Lost connection to device.'),
);
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kServerError.code,
'Service connection disposed.',
);
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub']),
contains('Lost connection to device.'),
);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'Flutter run catches catches errors due to vm service disconnection by code and throws a tool exit',
() async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kServiceDisappeared.code,
'',
);
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub']),
contains('Lost connection to device.'),
);
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kConnectionDisposed.code,
'dummy text not matched.',
);
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub']),
contains('Lost connection to device.'),
);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'Flutter run does not catch other RPC errors',
() async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kInvalidParams.code,
'',
);
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub']),
throwsA(isA<RPCError>()),
);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'Configures web connection options to use web sockets by default',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '--no-pub']),
throwsToolExit(),
);
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webUseSseForDebugBackend, false);
expect(options.webUseSseForDebugProxy, false);
expect(options.webUseSseForInjectedClient, false);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'flags propagate to debugging options',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--start-paused',
'--disable-service-auth-codes',
'--use-test-fonts',
'--trace-skia',
'--trace-systrace',
'--trace-to-file=path/to/trace.binpb',
'--verbose-system-logs',
'--native-null-assertions',
'--enable-impeller',
'--enable-vulkan-validation',
'--trace-systrace',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-embedder-api',
'--ci',
'--debug-logs-dir=path/to/logs',
]),
throwsToolExit(),
);
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.startPaused, true);
expect(options.disableServiceAuthCodes, true);
expect(options.useTestFonts, true);
expect(options.traceSkia, true);
expect(options.traceSystrace, true);
expect(options.traceToFile, 'path/to/trace.binpb');
expect(options.verboseSystemLogs, true);
expect(options.nativeNullAssertions, true);
expect(options.traceSystrace, true);
expect(options.enableImpeller, ImpellerStatus.enabled);
expect(options.enableVulkanValidation, true);
expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
expect(options.debugLogsDirectoryPath, 'path/to/logs');
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'usingCISystem can also be set by environment LUCI_CI',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>['run']),
throwsToolExit(),
);
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.usingCISystem, true);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => FakePlatform(environment: <String, String>{'LUCI_CI': 'True'}),
},
);
testUsingContext(
'wasm mode selects skwasm renderer by default',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>['run', '-d chrome', '--wasm']),
throwsToolExit(),
);
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.webUseWasm, true);
expect(options.webRenderer, WebRendererMode.skwasm);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'fails when "--web-launch-url" is not supported',
() async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--web-launch-url=http://flutter.dev']),
throwsA(
isException.having(
(Exception exception) => exception.toString(),
'toString',
isNot(contains('web-launch-url')),
),
),
);
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webLaunchUrl, 'http://flutter.dev');
final RegExp pattern = RegExp(r'^((http)?:\/\/)[^\s]+');
expect(pattern.hasMatch(options.webLaunchUrl!), true);
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
},
);
}
class TestDeviceManager extends DeviceManager {
TestDeviceManager({required super.logger});
List<Device> devices = <Device>[];
@override
List<DeviceDiscovery> get deviceDiscoverers {
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
devices.forEach(discoverer.addDevice);
return <DeviceDiscovery>[discoverer];
}
}
class FakeDevice extends Fake implements Device {
FakeDevice({
bool isLocalEmulator = false,
TargetPlatform targetPlatform = TargetPlatform.ios,
String sdkNameAndVersion = '',
PlatformType platformType = PlatformType.ios,
bool isSupported = true,
bool supportsFlavors = false,
}) : _isLocalEmulator = isLocalEmulator,
_targetPlatform = targetPlatform,
_sdkNameAndVersion = sdkNameAndVersion,
_platformType = platformType,
_isSupported = isSupported,
_supportsFlavors = supportsFlavors;
static const int kSuccess = 1;
static const int kFailure = -1;
final TargetPlatform _targetPlatform;
final bool _isLocalEmulator;
final String _sdkNameAndVersion;
final PlatformType _platformType;
final bool _isSupported;
final bool _supportsFlavors;
@override
Category get category => Category.mobile;
@override
String get id => 'fake_device';
Never _throwToolExit(int code) => throwToolExit('FakeDevice tool exit', exitCode: code);
@override
Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator);
@override
bool supportsRuntimeMode(BuildMode mode) => true;
@override
Future<bool> get supportsHardwareRendering async => true;
@override
bool supportsHotReload = false;
@override
bool get supportsHotRestart => true;
@override
bool get supportsFastStart => false;
@override
bool get supportsFlavors => _supportsFlavors;
@override
bool get ephemeral => true;
@override
bool get isConnected => true;
@override
DeviceConnectionInterface get connectionInterface => DeviceConnectionInterface.attached;
bool supported = true;
@override
bool isSupportedForProject(FlutterProject flutterProject) => _isSupported;
@override
bool isSupported() => supported;
@override
Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion);
@override
Future<String> get targetPlatformDisplayName async =>
getNameForTargetPlatform(await targetPlatform);
@override
DeviceLogReader getLogReader({ApplicationPackage? app, bool includePastLogs = false}) {
return FakeDeviceLogReader();
}
@override
String get name => 'FakeDevice';
@override
String get displayName => name;
@override
Future<TargetPlatform> get targetPlatform async => _targetPlatform;
@override
PlatformType get platformType => _platformType;
late bool startAppSuccess;
@override
DevFSWriter? createDevFSWriter(ApplicationPackage? app, String? userIdentifier) {
return null;
}
@override
Future<LaunchResult> startApp(
ApplicationPackage? package, {
String? mainPath,
String? route,
required DebuggingOptions debuggingOptions,
Map<String, Object?> platformArgs = const <String, Object?>{},
bool prebuiltApplication = false,
bool usesTerminalUi = true,
bool ipv6 = false,
String? userIdentifier,
}) async {
if (!startAppSuccess) {
return LaunchResult.failed();
}
if (startAppSuccess) {
return LaunchResult.succeeded();
}
final String dartFlags = debuggingOptions.dartFlags;
// In release mode, --dart-flags should be set to the empty string and
// provided flags should be dropped. In debug and profile modes,
// --dart-flags should not be empty.
if (debuggingOptions.buildInfo.isRelease) {
if (dartFlags.isNotEmpty) {
_throwToolExit(kFailure);
}
_throwToolExit(kSuccess);
} else {
if (dartFlags.isEmpty) {
_throwToolExit(kFailure);
}
_throwToolExit(kSuccess);
}
}
}
class FakeIOSDevice extends Fake implements IOSDevice {
FakeIOSDevice({
this.connectionInterface = DeviceConnectionInterface.attached,
bool isLocalEmulator = false,
String sdkNameAndVersion = '',
}) : _isLocalEmulator = isLocalEmulator,
_sdkNameAndVersion = sdkNameAndVersion;
final bool _isLocalEmulator;
final String _sdkNameAndVersion;
@override
Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator);
@override
Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion);
@override
final DeviceConnectionInterface connectionInterface;
@override
bool get isWirelesslyConnected => connectionInterface == DeviceConnectionInterface.wireless;
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
}
class TestRunCommandForUsageValues extends RunCommand {
TestRunCommandForUsageValues({List<Device>? devices}) {
this.devices = devices;
}
@override
Future<BuildInfo> getBuildInfo({
FlutterProject? project,
BuildMode? forcedBuildMode,
File? forcedTargetFile,
bool? forcedUseLocalCanvasKit,
}) async {
return const BuildInfo(
BuildMode.debug,
null,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
);
}
}
class TestRunCommandWithFakeResidentRunner extends RunCommand {
late FakeResidentRunner fakeResidentRunner;
@override
Future<ResidentRunner> createRunner({
required bool hotMode,
required List<FlutterDevice> flutterDevices,
required String? applicationBinaryPath,
required FlutterProject flutterProject,
}) async {
return fakeResidentRunner;
}
@override
// ignore: must_call_super
Future<void> validateCommand() async {
devices = <Device>[FakeDevice()..supportsHotReload = true];
}
}
class TestRunCommandThatOnlyValidates extends RunCommand {
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
@override
bool get shouldRunPub => false;
}
class FakeResidentRunner extends Fake implements ResidentRunner {
RPCError? rpcError;
@override
Future<int> run({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool enableDevTools = false,
String? route,
}) async {
await null;
if (rpcError != null) {
throw rpcError!;
}
return 0;
}
}
class DaemonCapturingRunCommand extends RunCommand {
late Daemon daemon;
late CapturingAppDomain appDomain;
@override
Daemon createMachineDaemon() {
daemon = super.createMachineDaemon();
appDomain = daemon.appDomain = CapturingAppDomain(daemon);
daemon.registerDomain(appDomain);
return daemon;
}
}
class CapturingAppDomain extends AppDomain {
CapturingAppDomain(super.daemon);
String? userIdentifier;
bool? enableDevTools;
@override
Future<AppInstance> startApp(
Device device,
String projectDirectory,
String target,
String? route,
DebuggingOptions options,
bool enableHotReload, {
File? applicationBinary,
required bool trackWidgetCreation,
String? projectRootPath,
String? packagesFilePath,
String? dillOutputPath,
String? isolateFilter,
bool machine = true,
String? userIdentifier,
}) async {
this.userIdentifier = userIdentifier;
enableDevTools = options.enableDevTools;
throwToolExit('');
}
}
class FakeAnsiTerminal extends Fake implements AnsiTerminal {
/// Setting to false will cause operations to Stdin to throw a [StdinException].
bool hasStdin = true;
@override
bool usesTerminalUi = false;
/// A list of all the calls to the [singleCharMode] setter.
List<bool> setSingleCharModeHistory = <bool>[];
@override
set singleCharMode(bool value) {
if (!hasStdin) {
throw const StdinException(
'Error setting terminal line mode',
OSError('The handle is invalid', 6),
);
}
setSingleCharModeHistory.add(value);
}
@override
bool get singleCharMode => setSingleCharModeHistory.last;
}