blob: 48f87422992697a66baf7048410022a1c9fb29f6 [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 'dart:io';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.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/attach.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/application_package.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/macos/macos_ipad_device.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:meta/meta.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
import '../../src/fake_vm_services.dart';
import '../../src/test_flutter_command_runner.dart';
final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
id: '1',
pauseEvent: vm_service.Event(
kind: vm_service.EventKind.kResume,
timestamp: 0
),
breakpoints: <vm_service.Breakpoint>[],
exceptionPauseMode: null,
isolateFlags: <vm_service.IsolateFlag>[],
libraries: <vm_service.LibraryRef>[],
livePorts: 0,
name: 'test',
number: '1',
pauseOnExit: false,
runnable: true,
startTime: 0,
isSystemIsolate: false,
);
void main() {
tearDown(() {
MacOSDesignedForIPadDevices.allowDiscovery = false;
});
group('attach', () {
StreamLogger logger;
FileSystem testFileSystem;
setUp(() {
Cache.disableLocking();
logger = StreamLogger();
testFileSystem = MemoryFileSystem(
style: globals.platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
);
testFileSystem.directory('lib').createSync();
testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync();
});
group('with one device and no specified target file', () {
const int devicePort = 499;
const int hostPort = 42;
FakeDeviceLogReader fakeLogReader;
RecordingPortForwarder portForwarder;
FakeDartDevelopmentService fakeDds;
FakeAndroidDevice device;
setUp(() {
fakeLogReader = FakeDeviceLogReader();
portForwarder = RecordingPortForwarder(hostPort);
fakeDds = FakeDartDevelopmentService();
device = FakeAndroidDevice(id: '1')
..portForwarder = portForwarder
..dds = fakeDds;
});
tearDown(() {
fakeLogReader.dispose();
});
testUsingContext('finds observatory port and forwards', () async {
device.onGetLogReader = () {
fakeLogReader.addLine('Foo');
fakeLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
return fakeLogReader;
};
testDeviceManager.addDevice(device);
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
if (message == '[verbose] Observatory URL on device: http://127.0.0.1:$devicePort') {
// The "Observatory URL on device" message is output by the ProtocolDiscovery when it found the observatory.
completer.complete();
}
});
final Future<void> task = createTestCommandRunner(AttachCommand()).run(<String>['attach']);
await completer.future;
expect(portForwarder.devicePort, devicePort);
expect(portForwarder.hostPort, hostPort);
await fakeLogReader.dispose();
await expectLoggerInterruptEndsTask(task, logger);
await loggerSubscription.cancel();
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
});
testUsingContext('Fails with tool exit on bad Observatory uri', () async {
device.onGetLogReader = () {
fakeLogReader.addLine('Foo');
fakeLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
fakeLogReader.dispose();
return fakeLogReader;
};
testDeviceManager.addDevice(device);
expect(() => createTestCommandRunner(AttachCommand()).run(<String>['attach']), throwsToolExit());
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
});
testUsingContext('accepts filesystem parameters', () async {
device.onGetLogReader = () {
fakeLogReader.addLine('Foo');
fakeLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
return fakeLogReader;
};
testDeviceManager.addDevice(device);
const String filesystemScheme = 'foo';
const String filesystemRoot = '/build-output/';
const String projectRoot = '/build-output/project-root';
const String outputDill = '/tmp/output.dill';
final FakeHotRunner hotRunner = FakeHotRunner();
hotRunner.onAttach = (
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async => 0;
hotRunner.exited = false;
hotRunner.isWaitingForObservatory = false;
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
final AttachCommand command = AttachCommand(
hotRunnerFactory: hotRunnerFactory,
);
await createTestCommandRunner(command).run(<String>[
'attach',
'--filesystem-scheme',
filesystemScheme,
'--filesystem-root',
filesystemRoot,
'--project-root',
projectRoot,
'--output-dill',
outputDill,
'-v', // enables verbose logging
]);
// Validate the attach call built a fake runner with the right
// project root and output dill.
expect(hotRunnerFactory.projectRootPath, projectRoot);
expect(hotRunnerFactory.dillOutputPath, outputDill);
expect(hotRunnerFactory.devices, hasLength(1));
// Validate that the attach call built a flutter device with the right
// output dill, filesystem scheme, and filesystem root.
final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
expect(flutterDevice.buildInfo.fileSystemScheme, filesystemScheme);
expect(flutterDevice.buildInfo.fileSystemRoots, const <String>[filesystemRoot]);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('exits when ipv6 is specified and debug-port is not', () async {
testDeviceManager.addDevice(device);
final AttachCommand command = AttachCommand();
await expectLater(
createTestCommandRunner(command).run(<String>['attach', '--ipv6']),
throwsToolExit(
message: 'When the --debug-port or --debug-url is unknown, this command determines '
'the value of --ipv6 on its own.',
),
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
},);
testUsingContext('exits when observatory-port is specified and debug-port is not', () async {
device.onGetLogReader = () {
fakeLogReader.addLine('Foo');
fakeLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
return fakeLogReader;
};
testDeviceManager.addDevice(device);
final AttachCommand command = AttachCommand();
await expectLater(
createTestCommandRunner(command).run(<String>['attach', '--observatory-port', '100']),
throwsToolExit(
message: 'When the --debug-port or --debug-url is unknown, this command does not use '
'the value of --observatory-port.',
),
);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
},);
});
group('forwarding to given port', () {
const int devicePort = 499;
const int hostPort = 42;
RecordingPortForwarder portForwarder;
FakeAndroidDevice device;
setUp(() {
final FakeDartDevelopmentService fakeDds = FakeDartDevelopmentService();
portForwarder = RecordingPortForwarder(hostPort);
device = FakeAndroidDevice(id: '1')
..portForwarder = portForwarder
..dds = fakeDds;
});
testUsingContext('succeeds in ipv4 mode', () async {
testDeviceManager.addDevice(device);
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
// Wait until resident_runner.dart tries to connect.
// There's nothing to connect _to_, so that's as far as we care to go.
completer.complete();
}
});
final Future<void> task = createTestCommandRunner(AttachCommand())
.run(<String>['attach', '--debug-port', '$devicePort']);
await completer.future;
expect(portForwarder.devicePort, devicePort);
expect(portForwarder.hostPort, hostPort);
await expectLoggerInterruptEndsTask(task, logger);
await loggerSubscription.cancel();
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
});
testUsingContext('succeeds in ipv6 mode', () async {
testDeviceManager.addDevice(device);
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
// Wait until resident_runner.dart tries to connect.
// There's nothing to connect _to_, so that's as far as we care to go.
completer.complete();
}
});
final Future<void> task = createTestCommandRunner(AttachCommand())
.run(<String>['attach', '--debug-port', '$devicePort', '--ipv6']);
await completer.future;
expect(portForwarder.devicePort, devicePort);
expect(portForwarder.hostPort, hostPort);
await expectLoggerInterruptEndsTask(task, logger);
await loggerSubscription.cancel();
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
});
testUsingContext('skips in ipv4 mode with a provided observatory port', () async {
testDeviceManager.addDevice(device);
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
// Wait until resident_runner.dart tries to connect.
// There's nothing to connect _to_, so that's as far as we care to go.
completer.complete();
}
});
final Future<void> task = createTestCommandRunner(AttachCommand()).run(
<String>[
'attach',
'--debug-port',
'$devicePort',
'--observatory-port',
'$hostPort',
// Ensure DDS doesn't use hostPort by binding to a random port.
'--dds-port',
'0',
],
);
await completer.future;
expect(portForwarder.devicePort, null);
expect(portForwarder.hostPort, 42);
await expectLoggerInterruptEndsTask(task, logger);
await loggerSubscription.cancel();
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
});
testUsingContext('skips in ipv6 mode with a provided observatory port', () async {
testDeviceManager.addDevice(device);
final Completer<void> completer = Completer<void>();
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
// Wait until resident_runner.dart tries to connect.
// There's nothing to connect _to_, so that's as far as we care to go.
completer.complete();
}
});
final Future<void> task = createTestCommandRunner(AttachCommand()).run(
<String>[
'attach',
'--debug-port',
'$devicePort',
'--observatory-port',
'$hostPort',
'--ipv6',
// Ensure DDS doesn't use hostPort by binding to a random port.
'--dds-port',
'0',
],
);
await completer.future;
expect(portForwarder.devicePort, null);
expect(portForwarder.hostPort, 42);
await expectLoggerInterruptEndsTask(task, logger);
await loggerSubscription.cancel();
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
});
});
testUsingContext('exits when no device connected', () async {
final AttachCommand command = AttachCommand();
await expectLater(
createTestCommandRunner(command).run(<String>['attach']),
throwsToolExit(),
);
expect(testLogger.statusText, containsIgnoringWhitespace('No supported devices connected'));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails when targeted device is not Android with --device-user', () async {
final FakeIOSDevice device = FakeIOSDevice();
testDeviceManager.addDevice(device);
expect(createTestCommandRunner(AttachCommand()).run(<String>[
'attach',
'--device-user',
'10',
]), throwsToolExit(message: '--device-user is only supported for Android'));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('exits when multiple devices connected', () async {
final AttachCommand command = AttachCommand();
testDeviceManager.addDevice(FakeAndroidDevice(id: 'xx1'));
testDeviceManager.addDevice(FakeAndroidDevice(id: 'yy2'));
await expectLater(
createTestCommandRunner(command).run(<String>['attach']),
throwsToolExit(),
);
expect(testLogger.statusText, containsIgnoringWhitespace('More than one device'));
expect(testLogger.statusText, contains('xx1'));
expect(testLogger.statusText, contains('yy2'));
expect(MacOSDesignedForIPadDevices.allowDiscovery, isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Catches service disappeared error', () async {
final FakeAndroidDevice device = FakeAndroidDevice(id: '1')
..portForwarder = const NoOpDevicePortForwarder()
..onGetLogReader = () => NoOpDeviceLogReader('test');
final FakeHotRunner hotRunner = FakeHotRunner();
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
hotRunner.onAttach = (
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async {
await null;
throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kServiceDisappeared, '');
};
testDeviceManager.addDevice(device);
testFileSystem.file('lib/main.dart').createSync();
final AttachCommand command = AttachCommand(hotRunnerFactory: hotRunnerFactory);
await expectLater(createTestCommandRunner(command).run(<String>[
'attach',
]), throwsToolExit(message: 'Lost connection to device.'));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Does not catch generic RPC error', () async {
final FakeAndroidDevice device = FakeAndroidDevice(id: '1')
..portForwarder = const NoOpDevicePortForwarder()
..onGetLogReader = () => NoOpDeviceLogReader('test');
final FakeHotRunner hotRunner = FakeHotRunner();
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
hotRunner.onAttach = (
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async {
await null;
throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kInvalidParams, '');
};
testDeviceManager.addDevice(device);
testFileSystem.file('lib/main.dart').createSync();
final AttachCommand command = AttachCommand(hotRunnerFactory: hotRunnerFactory);
await expectLater(createTestCommandRunner(command).run(<String>[
'attach',
]), throwsA(isA<vm_service.RPCError>()));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
}
class FakeHotRunner extends Fake implements HotRunner {
Future<int> Function(Completer<DebugConnectionInfo>, Completer<void>, bool, bool) onAttach;
@override
bool exited = false;
@override
bool isWaitingForObservatory = true;
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance = false,
bool enableDevTools = false,
}) {
return onAttach(connectionInfoCompleter, appStartedCompleter, allowExistingDdsInstance, enableDevTools);
}
}
class FakeHotRunnerFactory extends Fake implements HotRunnerFactory {
HotRunner hotRunner;
String dillOutputPath;
String projectRootPath;
List<FlutterDevice> devices;
@override
HotRunner build(
List<FlutterDevice> devices, {
String target,
DebuggingOptions debuggingOptions,
bool benchmarkMode = false,
File applicationBinary,
bool hostIsIde = false,
String projectRootPath,
String packagesFilePath,
String dillOutputPath,
bool stayResident = true,
bool ipv6 = false,
FlutterProject flutterProject,
}) {
this.devices = devices;
this.dillOutputPath = dillOutputPath;
this.projectRootPath = projectRootPath;
return hotRunner;
}
}
class RecordingPortForwarder implements DevicePortForwarder {
RecordingPortForwarder([this.hostPort]);
int devicePort;
int hostPort;
@override
Future<void> dispose() async { }
@override
Future<int> forward(int devicePort, {int hostPort}) async {
this.devicePort = devicePort;
this.hostPort ??= hostPort;
return this.hostPort;
}
@override
List<ForwardedPort> get forwardedPorts => <ForwardedPort>[];
@override
Future<void> unforward(ForwardedPort forwardedPort) async { }
}
class StreamLogger extends Logger {
@override
bool get isVerbose => true;
@override
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
bool wrap,
}) {
hadErrorOutput = true;
_log('[stderr] $message');
}
@override
void printWarning(
String message, {
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
bool wrap,
}) {
hadWarningOutput = true;
_log('[stderr] $message');
}
@override
void printStatus(
String message, {
bool emphasis,
TerminalColor color,
bool newline,
int indent,
int hangingIndent,
bool wrap,
}) {
_log('[stdout] $message');
}
@override
void printBox(
String message, {
String title,
}) {
if (title == null) {
_log('[stdout] $message');
} else {
_log('[stdout] $title: $message');
}
}
@override
void printTrace(String message) {
_log('[verbose] $message');
}
@override
Status startProgress(
String message, {
@required Duration timeout,
String progressId,
bool multilineOutput = false,
bool includeTiming = true,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
_log('[progress] $message');
return SilentStatus(
stopwatch: Stopwatch(),
)..start();
}
@override
Status startSpinner({ VoidCallback onFinish }) {
return SilentStatus(
stopwatch: Stopwatch(),
onFinish: onFinish,
)..start();
}
bool _interrupt = false;
void interrupt() {
_interrupt = true;
}
final StreamController<String> _controller = StreamController<String>.broadcast();
void _log(String message) {
_controller.add(message);
if (_interrupt) {
_interrupt = false;
throw const LoggerInterrupted();
}
}
Stream<String> get stream => _controller.stream;
@override
void sendEvent(String name, [Map<String, dynamic> args]) { }
@override
bool get supportsColor => throw UnimplementedError();
@override
bool get hasTerminal => false;
@override
void clear() => _log('[stdout] ${globals.terminal.clearScreen()}\n');
@override
Terminal get terminal => Terminal.test();
}
class LoggerInterrupted implements Exception {
const LoggerInterrupted();
}
Future<void> expectLoggerInterruptEndsTask(Future<void> task, StreamLogger logger) async {
logger.interrupt(); // an exception during the task should cause it to fail...
await expectLater(
() => task,
throwsA(isA<ToolExit>().having((ToolExit error) => error.exitCode, 'exitCode', 2)),
);
}
VMServiceConnector getFakeVmServiceFactory({
@required Completer<void> vmServiceDoneCompleter,
}) {
assert(vmServiceDoneCompleter != null);
return (
Uri httpUri, {
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
GetSkSLMethod getSkSLMethod,
PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
CompressionOptions compression,
Device device,
Logger logger,
}) async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: kListViewsMethod,
args: null,
jsonResponse: <String, Object>{
'views': <Object>[
<String, Object>{
'id': '1',
'isolate': fakeUnpausedIsolate.toJson()
},
],
},
),
FakeVmServiceRequest(
method: 'getVM',
args: null,
jsonResponse: vm_service.VM.parse(<String, Object>{})
.toJson(),
),
FakeVmServiceRequest(
method: '_createDevFS',
args: <String, Object>{
'fsName': globals.fs.currentDirectory.absolute.path,
},
jsonResponse: <String, Object>{
'uri': globals.fs.currentDirectory.absolute.path,
},
),
FakeVmServiceRequest(
method: kListViewsMethod,
args: null,
jsonResponse: <String, Object>{
'views': <Object>[
<String, Object>{
'id': '1',
'isolate': fakeUnpausedIsolate.toJson()
},
],
},
),
],
);
return fakeVmServiceHost.vmService;
};
}
class TestHotRunnerFactory extends HotRunnerFactory {
HotRunner _runner;
@override
HotRunner build(
List<FlutterDevice> devices, {
String target,
DebuggingOptions debuggingOptions,
bool benchmarkMode = false,
File applicationBinary,
bool hostIsIde = false,
String projectRootPath,
String packagesFilePath,
String dillOutputPath,
bool stayResident = true,
bool ipv6 = false,
FlutterProject flutterProject,
}) {
_runner ??= HotRunner(
devices,
target: target,
debuggingOptions: debuggingOptions,
benchmarkMode: benchmarkMode,
applicationBinary: applicationBinary,
hostIsIde: hostIsIde,
projectRootPath: projectRootPath,
dillOutputPath: dillOutputPath,
stayResident: stayResident,
ipv6: ipv6,
);
return _runner;
}
Future<void> exitApp() async {
assert(_runner != null);
await _runner.exit();
}
}
class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
@override
Future<void> get done => noopCompleter.future;
final Completer<void> noopCompleter = Completer<void>();
@override
Future<void> startDartDevelopmentService(
Uri observatoryUri, {
@required Logger logger,
int hostPort,
bool ipv6,
bool disableServiceAuthCodes,
}) async {}
@override
Uri get uri => Uri.parse('http://localhost:8181');
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeAndroidDevice extends Fake implements AndroidDevice {
FakeAndroidDevice({@required this.id});
@override
DartDevelopmentService dds;
@override
final String id;
@override
String get name => 'd$id';
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get sdkNameAndVersion async => 'Android 46';
@override
Future<String> get targetPlatformDisplayName async => 'android';
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm;
@override
bool isSupported() => true;
@override
bool get supportsHotRestart => true;
@override
bool get supportsFlutterExit => false;
@override
bool isSupportedForProject(FlutterProject flutterProject) => true;
@override
DevicePortForwarder portForwarder;
DeviceLogReader Function() onGetLogReader;
@override
FutureOr<DeviceLogReader> getLogReader({
AndroidApk app,
bool includePastLogs = false,
}) {
return onGetLogReader();
}
@override
OverrideArtifacts get artifactOverrides => null;
@override
final PlatformType platformType = PlatformType.android;
@override
Category get category => Category.mobile;
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeIOSDevice extends Fake implements IOSDevice {
FakeIOSDevice({this.dds, this.portForwarder, this.logReader});
@override
final DevicePortForwarder portForwarder;
@override
final DartDevelopmentService dds;
final DeviceLogReader logReader;
@override
DeviceLogReader getLogReader({
IOSApp app,
bool includePastLogs = false,
}) => logReader;
@override
OverrideArtifacts get artifactOverrides => null;
@override
final String name = 'name';
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
@override
final PlatformType platformType = PlatformType.ios;
}