// 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 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:collection/collection.dart' show ListEquality, MapEquality; import 'package:flutter_devicelab/framework/devices.dart'; import 'package:meta/meta.dart'; import 'common.dart'; void main() { group('device', () { late Device device; setUp(() { FakeDevice.resetLog(); device = FakeDevice(deviceId: 'fakeDeviceId'); }); tearDown(() {}); group('cpu check', () { test('arm64', () async { FakeDevice.pretendArm64(); final AndroidDevice androidDevice = device as AndroidDevice; expect(await androidDevice.isArm64(), isTrue); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'getprop', arguments: ['ro.product.cpu.abi']), ]); }); }); group('isAwake/isAsleep', () { test('reads Awake', () async { FakeDevice.pretendAwake(); expect(await device.isAwake(), isTrue); expect(await device.isAsleep(), isFalse); }); test('reads Awake - samsung devices', () async { FakeDevice.pretendAwakeSamsung(); expect(await device.isAwake(), isTrue); expect(await device.isAsleep(), isFalse); }); test('reads Asleep', () async { FakeDevice.pretendAsleep(); expect(await device.isAwake(), isFalse); expect(await device.isAsleep(), isTrue); }); }); group('togglePower', () { test('sends power event', () async { await device.togglePower(); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'input', arguments: ['keyevent', '26']), ]); }); }); group('wakeUp', () { test('when awake', () async { FakeDevice.pretendAwake(); await device.wakeUp(); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'dumpsys', arguments: ['power']), ]); }); test('when asleep', () async { FakeDevice.pretendAsleep(); await device.wakeUp(); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'dumpsys', arguments: ['power']), cmd(command: 'input', arguments: ['keyevent', '26']), ]); }); }); group('sendToSleep', () { test('when asleep', () async { FakeDevice.pretendAsleep(); await device.sendToSleep(); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'dumpsys', arguments: ['power']), ]); }); test('when awake', () async { FakeDevice.pretendAwake(); await device.sendToSleep(); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'dumpsys', arguments: ['power']), cmd(command: 'input', arguments: ['keyevent', '26']), ]); }); }); group('unlock', () { test('sends unlock event', () async { FakeDevice.pretendAwake(); await device.unlock(); expectLog([ cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk']), cmd(command: 'dumpsys', arguments: ['power']), cmd(command: 'input', arguments: ['keyevent', '82']), ]); }); }); group('adb', () { test('tap', () async { FakeDevice.resetLog(); await device.tap(100, 200); expectLog([ cmd(command: 'input', arguments: ['tap', '100', '200']), ]); }); test('awaitDevice', () async { FakeDevice.resetLog(); // The expected value from `adb shell getprop sys.boot_completed` FakeDevice.output = '1'; await device.awaitDevice(); expectLog([ cmd(command: 'adb', environment: { FakeDevice.canFailKey: 'false' }, arguments: [ '-s', device.deviceId, 'wait-for-device', ]), cmd(command: 'adb', environment: { FakeDevice.canFailKey: 'false', }, arguments: [ '-s', device.deviceId, 'shell', 'getprop sys.boot_completed', ]) ]); }); test('reboot', () async { FakeDevice.resetLog(); await device.reboot(); expectLog([ cmd(command: 'adb', environment: { FakeDevice.canFailKey: 'false' }, arguments: [ '-s', device.deviceId, 'reboot', ]), ]); }); test('clearLog', () async { FakeDevice.resetLog(); await device.clearLogs(); expectLog([ cmd(command: 'adb', environment: { FakeDevice.canFailKey: 'true' }, arguments: [ '-s', device.deviceId, 'logcat', '-c', ]), ]); }); test('startLoggingToSink calls adb', () async { FakeDevice.resetLog(); await device.startLoggingToSink(IOSink(_MemoryIOSink())); expectLog([ cmd(command: 'adb', environment: { FakeDevice.canFailKey: 'true' }, arguments: [ '-s', device.deviceId, 'logcat', '--clear', ]), ]); }); }); }); } void expectLog(List log) { expect(FakeDevice.commandLog, log); } CommandArgs cmd({ required String command, List? arguments, Map? environment, }) { return CommandArgs( command: command, arguments: arguments, environment: environment, ); } @immutable class CommandArgs { const CommandArgs({ required this.command, this.arguments, this.environment }); final String command; final List? arguments; final Map? environment; @override String toString() => 'CommandArgs(command: $command, arguments: $arguments, environment: $environment)'; @override bool operator==(Object other) { if (other.runtimeType != CommandArgs) { return false; } return other is CommandArgs && other.command == command && const ListEquality().equals(other.arguments, arguments) && const MapEquality().equals(other.environment, environment); } @override int get hashCode { return Object.hash( command, Object.hashAll(arguments ?? const []), Object.hashAllUnordered(environment?.keys ?? const []), Object.hashAllUnordered(environment?.values ?? const []), ); } } class FakeDevice extends AndroidDevice { FakeDevice({required super.deviceId}); static const String canFailKey = 'canFail'; static String output = ''; static List commandLog = []; static void resetLog() { commandLog.clear(); } static void pretendAwake() { output = ''' mWakefulness=Awake '''; } static void pretendAwakeSamsung() { output = ''' getWakefulnessLocked()=Awake '''; } static void pretendAsleep() { output = ''' mWakefulness=Asleep '''; } static void pretendArm64() { output = ''' arm64 '''; } @override Future adb(List arguments, {Map? environment, bool silent = false, bool canFail = false}) async { environment ??= {}; commandLog.add(CommandArgs( command: 'adb', // ignore: prefer_spread_collections arguments: ['-s', deviceId]..addAll(arguments), environment: environment..putIfAbsent('canFail', () => '$canFail'), )); return output; } @override Future shellEval(String command, List arguments, { Map? environment, bool silent = false }) async { commandLog.add(CommandArgs( command: command, arguments: arguments, environment: environment, )); return output; } @override Future shellExec(String command, List arguments, { Map? environment, bool silent = false }) async { commandLog.add(CommandArgs( command: command, arguments: arguments, environment: environment, )); } } /// An IOSink that collects whatever is written to it. /// Inspired by packages/flutter_tools/lib/src/base/net.dart class _MemoryIOSink implements IOSink { @override Encoding encoding = utf8; final BytesBuilder writes = BytesBuilder(copy: false); @override void add(List data) { writes.add(data); } @override Future addStream(Stream> stream) { final Completer completer = Completer(); stream.listen(add).onDone(completer.complete); return completer.future; } @override void writeCharCode(int charCode) { add([charCode]); } @override void write(Object? obj) { add(encoding.encode('$obj')); } @override void writeln([Object? obj = '']) { add(encoding.encode('$obj\n')); } @override void writeAll(Iterable objects, [String separator = '']) { bool addSeparator = false; for (final dynamic object in objects) { if (addSeparator) { write(separator); } write(object); addSeparator = true; } } @override void addError(dynamic error, [StackTrace? stackTrace]) { throw UnimplementedError(); } @override Future get done => close(); @override Future close() async {} @override Future flush() async {} }