blob: 1cfa8ff6c4dcdd344f0257ccda33659949f79d3c [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.
import 'dart:async';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:flutter_tools/src/daemon.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/proxied_devices/devices.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
void main() {
Daemon? daemon;
late NotifyingLogger notifyingLogger;
late BufferLogger bufferLogger;
late FakeAndroidDevice fakeDevice;
late FakeApplicationPackageFactory applicationPackageFactory;
late MemoryFileSystem memoryFileSystem;
late FakeProcessManager fakeProcessManager;
group('ProxiedDevices', () {
late DaemonConnection serverDaemonConnection;
late DaemonConnection clientDaemonConnection;
setUp(() {
bufferLogger = BufferLogger.test();
notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger);
final FakeDaemonStreams serverDaemonStreams = FakeDaemonStreams();
serverDaemonConnection = DaemonConnection(
daemonStreams: serverDaemonStreams,
logger: bufferLogger,
);
final FakeDaemonStreams clientDaemonStreams = FakeDaemonStreams();
clientDaemonConnection = DaemonConnection(
daemonStreams: clientDaemonStreams,
logger: bufferLogger,
);
serverDaemonStreams.inputs.addStream(clientDaemonStreams.outputs.stream);
clientDaemonStreams.inputs.addStream(serverDaemonStreams.outputs.stream);
applicationPackageFactory = FakeApplicationPackageFactory();
memoryFileSystem = MemoryFileSystem();
fakeProcessManager = FakeProcessManager.empty();
});
tearDown(() async {
if (daemon != null) {
return daemon!.shutdown();
}
notifyingLogger.dispose();
await serverDaemonConnection.dispose();
await clientDaemonConnection.dispose();
});
testUsingContext('can list devices', () async {
daemon = Daemon(
serverDaemonConnection,
notifyingLogger: notifyingLogger,
);
fakeDevice = FakeAndroidDevice();
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
daemon!.deviceDomain.addDeviceDiscoverer(discoverer);
discoverer.addDevice(fakeDevice);
final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger);
final List<Device> devices = await proxiedDevices.discoverDevices();
expect(devices, hasLength(1));
final Device device = devices[0];
expect(device.id, fakeDevice.id);
expect(device.name, 'Proxied ${fakeDevice.name}');
expect(await device.targetPlatform, await fakeDevice.targetPlatform);
expect(await device.isLocalEmulator, await fakeDevice.isLocalEmulator);
});
testUsingContext('calls supportsRuntimeMode', () async {
daemon = Daemon(
serverDaemonConnection,
notifyingLogger: notifyingLogger,
);
fakeDevice = FakeAndroidDevice();
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
daemon!.deviceDomain.addDeviceDiscoverer(discoverer);
discoverer.addDevice(fakeDevice);
final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger);
final List<Device> devices = await proxiedDevices.devices;
expect(devices, hasLength(1));
final Device device = devices[0];
final bool supportsRuntimeMode = await device.supportsRuntimeMode(BuildMode.release);
expect(fakeDevice.supportsRuntimeModeCalledBuildMode, BuildMode.release);
expect(supportsRuntimeMode, true);
});
testUsingContext('redirects logs', () async {
daemon = Daemon(
serverDaemonConnection,
notifyingLogger: notifyingLogger,
);
fakeDevice = FakeAndroidDevice();
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
daemon!.deviceDomain.addDeviceDiscoverer(discoverer);
discoverer.addDevice(fakeDevice);
final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger);
final FakeDeviceLogReader fakeLogReader = FakeDeviceLogReader();
fakeDevice.logReader = fakeLogReader;
final List<Device> devices = await proxiedDevices.devices;
expect(devices, hasLength(1));
final Device device = devices[0];
final DeviceLogReader logReader = await device.getLogReader();
fakeLogReader.logLinesController.add('Some log line');
final String receivedLogLine = await logReader.logLines.first;
expect(receivedLogLine, 'Some log line');
// Now try to stop the log reader
expect(fakeLogReader.disposeCalled, false);
logReader.dispose();
await pumpEventQueue();
expect(fakeLogReader.disposeCalled, true);
});
testUsingContext('starts and stops app', () async {
daemon = Daemon(
serverDaemonConnection,
notifyingLogger: notifyingLogger,
);
fakeDevice = FakeAndroidDevice();
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
daemon!.deviceDomain.addDeviceDiscoverer(discoverer);
discoverer.addDevice(fakeDevice);
final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger);
final FakePrebuiltApplicationPackage prebuiltApplicationPackage = FakePrebuiltApplicationPackage();
final File dummyApplicationBinary = memoryFileSystem.file('/directory/dummy_file');
dummyApplicationBinary.parent.createSync();
dummyApplicationBinary.writeAsStringSync('dummy content');
prebuiltApplicationPackage.applicationPackage = dummyApplicationBinary;
final List<Device> devices = await proxiedDevices.devices;
expect(devices, hasLength(1));
final Device device = devices[0];
// Now try to start the app
final FakeApplicationPackage applicationPackage = FakeApplicationPackage();
applicationPackageFactory.applicationPackage = applicationPackage;
final Uri observatoryUri = Uri.parse('http://127.0.0.1:12345/observatory');
fakeDevice.launchResult = LaunchResult.succeeded(observatoryUri: observatoryUri);
final LaunchResult launchResult = await device.startApp(
prebuiltApplicationPackage,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
);
expect(launchResult.started, true);
// The returned observatoryUri was a forwarded port, so we cannot compare them directly.
expect(launchResult.observatoryUri!.path, observatoryUri.path);
expect(applicationPackageFactory.applicationBinaryRequested!.readAsStringSync(), 'dummy content');
expect(applicationPackageFactory.platformRequested, TargetPlatform.android_arm);
expect(fakeDevice.startAppPackage, applicationPackage);
// Now try to stop the app
final bool stopAppResult = await device.stopApp(prebuiltApplicationPackage);
expect(fakeDevice.stopAppPackage, applicationPackage);
expect(stopAppResult, true);
}, overrides: <Type, Generator>{
ApplicationPackageFactory: () => applicationPackageFactory,
FileSystem: () => memoryFileSystem,
ProcessManager: () => fakeProcessManager,
});
testUsingContext('takes screenshot', () async {
daemon = Daemon(
serverDaemonConnection,
notifyingLogger: notifyingLogger,
);
fakeDevice = FakeAndroidDevice();
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
daemon!.deviceDomain.addDeviceDiscoverer(discoverer);
discoverer.addDevice(fakeDevice);
final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger);
final List<Device> devices = await proxiedDevices.devices;
expect(devices, hasLength(1));
final Device device = devices[0];
final List<int> screenshot = <int>[1,2,3,4,5];
fakeDevice.screenshot = screenshot;
final File screenshotOutputFile = memoryFileSystem.file('screenshot_file');
await device.takeScreenshot(screenshotOutputFile);
expect(await screenshotOutputFile.readAsBytes(), screenshot);
}, overrides: <Type, Generator>{
FileSystem: () => memoryFileSystem,
ProcessManager: () => fakeProcessManager,
});
});
}
class FakeDaemonStreams implements DaemonStreams {
final StreamController<DaemonMessage> inputs = StreamController<DaemonMessage>();
final StreamController<DaemonMessage> outputs = StreamController<DaemonMessage>();
@override
Stream<DaemonMessage> get inputStream {
return inputs.stream;
}
@override
void send(Map<String, dynamic> message, [ List<int>? binary ]) {
outputs.add(DaemonMessage(message, binary != null ? Stream<List<int>>.value(binary) : null));
}
@override
Future<void> dispose() async {
await inputs.close();
// In some tests, outputs have no listeners. We don't wait for outputs to close.
unawaited(outputs.close());
}
}
// 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 {
@override
final String id = 'device';
@override
final String name = 'device';
@override
Future<String> get emulatorId async => 'device';
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm;
@override
Future<bool> get isLocalEmulator async => false;
@override
final Category category = Category.mobile;
@override
final PlatformType platformType = PlatformType.android;
@override
final bool ephemeral = false;
@override
Future<String> get sdkNameAndVersion async => 'Android 12';
@override
bool get supportsHotReload => true;
@override
bool get supportsHotRestart => true;
@override
bool get supportsScreenshot => true;
@override
bool get supportsFastStart => true;
@override
bool get supportsFlutterExit => true;
@override
Future<bool> get supportsHardwareRendering async => true;
@override
bool get supportsStartPaused => true;
BuildMode? supportsRuntimeModeCalledBuildMode;
@override
Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
supportsRuntimeModeCalledBuildMode = buildMode;
return true;
}
late DeviceLogReader logReader;
@override
FutureOr<DeviceLogReader> getLogReader({
covariant ApplicationPackage? app,
bool includePastLogs = false,
}) => logReader;
ApplicationPackage? startAppPackage;
late LaunchResult launchResult;
@override
Future<LaunchResult> startApp(
ApplicationPackage package, {
String? mainPath,
String? route,
DebuggingOptions? debuggingOptions,
Map<String, Object?> platformArgs = const <String, Object>{},
bool prebuiltApplication = false,
bool ipv6 = false,
String? userIdentifier,
}) async {
startAppPackage = package;
return launchResult;
}
ApplicationPackage? stopAppPackage;
@override
Future<bool> stopApp(
ApplicationPackage app, {
String? userIdentifier,
}) async {
stopAppPackage = app;
return true;
}
late List<int> screenshot;
@override
Future<void> takeScreenshot(File outputFile) {
return outputFile.writeAsBytes(screenshot);
}
}
class FakeDeviceLogReader implements DeviceLogReader {
final StreamController<String> logLinesController = StreamController<String>();
bool disposeCalled = false;
@override
int? appPid;
@override
FlutterVmService? connectedVMService;
@override
void dispose() {
disposeCalled = true;
}
@override
Stream<String> get logLines => logLinesController.stream;
@override
String get name => 'device';
}
class FakeApplicationPackageFactory implements ApplicationPackageFactory {
TargetPlatform? platformRequested;
File? applicationBinaryRequested;
ApplicationPackage? applicationPackage;
@override
Future<ApplicationPackage?> getPackageForPlatform(TargetPlatform platform, {BuildInfo? buildInfo, File? applicationBinary}) async {
platformRequested = platform;
applicationBinaryRequested = applicationBinary;
return applicationPackage;
}
}
class FakeApplicationPackage extends Fake implements ApplicationPackage {}
class FakePrebuiltApplicationPackage extends Fake implements PrebuiltApplicationPackage {
@override
late File applicationPackage;
}