| // 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(() { | |
| FakeDevice.resetLog(); | |
| }); | |
| group('cpu check', () { | |
| test('arm64', () async { | |
| FakeDevice.pretendArm64(); | |
| final androidDevice = device as AndroidDevice; | |
| expect(await androidDevice.isArm64(), isTrue); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'getprop', arguments: <String>['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(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'input', arguments: <String>['keyevent', '26']), | |
| ]); | |
| }); | |
| }); | |
| group('wakeUp', () { | |
| test('when awake', () async { | |
| FakeDevice.pretendAwake(); | |
| await device.wakeUp(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'dumpsys', arguments: <String>['power']), | |
| ]); | |
| }); | |
| test('when asleep', () async { | |
| FakeDevice.pretendAsleep(); | |
| await device.wakeUp(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'dumpsys', arguments: <String>['power']), | |
| cmd(command: 'input', arguments: <String>['keyevent', '26']), | |
| ]); | |
| }); | |
| }); | |
| group('sendToSleep', () { | |
| test('when asleep', () async { | |
| FakeDevice.pretendAsleep(); | |
| await device.sendToSleep(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'dumpsys', arguments: <String>['power']), | |
| ]); | |
| }); | |
| test('when awake', () async { | |
| FakeDevice.pretendAwake(); | |
| await device.sendToSleep(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'dumpsys', arguments: <String>['power']), | |
| cmd(command: 'input', arguments: <String>['keyevent', '26']), | |
| ]); | |
| }); | |
| }); | |
| group('unlock', () { | |
| test('sends unlock event', () async { | |
| FakeDevice.pretendAwake(); | |
| await device.unlock(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'getprop', | |
| arguments: <String>[ | |
| 'ro.bootimage.build.fingerprint', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.release', | |
| ';', | |
| 'getprop', | |
| 'ro.build.version.sdk', | |
| ], | |
| ), | |
| cmd(command: 'dumpsys', arguments: <String>['power']), | |
| cmd(command: 'input', arguments: <String>['keyevent', '82']), | |
| ]); | |
| }); | |
| }); | |
| group('adb', () { | |
| test('tap', () async { | |
| FakeDevice.resetLog(); | |
| await device.tap(100, 200); | |
| expectLog(<CommandArgs>[ | |
| cmd(command: 'input', arguments: <String>['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(<CommandArgs>[ | |
| cmd( | |
| command: 'adb', | |
| environment: <String, String>{FakeDevice.canFailKey: 'false'}, | |
| arguments: <String>['-s', device.deviceId, 'wait-for-device'], | |
| ), | |
| cmd( | |
| command: 'adb', | |
| environment: <String, String>{FakeDevice.canFailKey: 'false'}, | |
| arguments: <String>['-s', device.deviceId, 'shell', 'getprop sys.boot_completed'], | |
| ), | |
| ]); | |
| }); | |
| test('reboot', () async { | |
| FakeDevice.resetLog(); | |
| await device.reboot(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'adb', | |
| environment: <String, String>{FakeDevice.canFailKey: 'false'}, | |
| arguments: <String>['-s', device.deviceId, 'reboot'], | |
| ), | |
| ]); | |
| }); | |
| test('clearLog', () async { | |
| FakeDevice.resetLog(); | |
| await device.clearLogs(); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'adb', | |
| environment: <String, String>{FakeDevice.canFailKey: 'true'}, | |
| arguments: <String>['-s', device.deviceId, 'logcat', '-c'], | |
| ), | |
| ]); | |
| }); | |
| test('startLoggingToSink calls adb', () async { | |
| FakeDevice.resetLog(); | |
| await device.startLoggingToSink(IOSink(_MemoryIOSink())); | |
| expectLog(<CommandArgs>[ | |
| cmd( | |
| command: 'adb', | |
| environment: <String, String>{FakeDevice.canFailKey: 'true'}, | |
| arguments: <String>['-s', device.deviceId, 'logcat', '--clear'], | |
| ), | |
| ]); | |
| }); | |
| test('getMemoryStats throws DeviceException if dumpsys meminfo fails to parse', () { | |
| FakeDevice.output = 'Some garbage output'; | |
| expect(device.getMemoryStats('com.example'), throwsA(isA<DeviceException>())); | |
| }); | |
| test('getMemoryStats returns memory stats if dumpsys meminfo parses successfully', () async { | |
| FakeDevice.resetLog(); | |
| FakeDevice.output = 'TOTAL 12345'; | |
| final Map<String, dynamic> stats = await device.getMemoryStats('com.example'); | |
| expect(stats, <String, dynamic>{'total_kb': 12345}); | |
| expectLog(const <CommandArgs>[ | |
| CommandArgs(command: 'dumpsys', arguments: <String>['meminfo', 'com.example']), | |
| ]); | |
| }); | |
| }); | |
| }); | |
| } | |
| void expectLog(List<CommandArgs> log) { | |
| expect(FakeDevice.commandLog, log); | |
| } | |
| CommandArgs cmd({ | |
| required String command, | |
| List<String>? arguments, | |
| Map<String, String>? 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<String>? arguments; | |
| final Map<String, String>? 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<String>().equals(other.arguments, arguments) && | |
| const MapEquality<String, String>().equals(other.environment, environment); | |
| } | |
| @override | |
| int get hashCode { | |
| return Object.hash( | |
| command, | |
| Object.hashAll(arguments ?? const <String>[]), | |
| Object.hashAllUnordered(environment?.keys ?? const <String>[]), | |
| Object.hashAllUnordered(environment?.values ?? const <String>[]), | |
| ); | |
| } | |
| } | |
| class FakeDevice extends AndroidDevice { | |
| FakeDevice({required super.deviceId}); | |
| static const String canFailKey = 'canFail'; | |
| static String output = ''; | |
| static List<CommandArgs> commandLog = <CommandArgs>[]; | |
| static void resetLog() { | |
| commandLog.clear(); | |
| } | |
| static void pretendAwake() { | |
| // Emit an integer value in addition to the state string to ensure only | |
| // the state string is matched. | |
| // | |
| // Regression testing for https://github.com/flutter/flutter/issues/174952. | |
| output = ''' | |
| mWakefulness=1 | |
| mWakefulness=Awake | |
| '''; | |
| } | |
| static void pretendAwakeSamsung() { | |
| // Emit an integer value in addition to the state string to ensure only | |
| // the state string is matched. | |
| // | |
| // Regression testing for https://github.com/flutter/flutter/issues/174952. | |
| output = ''' | |
| getWakefulnessLocked()=1 | |
| getWakefulnessLocked()=Awake | |
| '''; | |
| } | |
| static void pretendAsleep() { | |
| // Emit an integer value in addition to the state string to ensure only | |
| // the state string is matched. | |
| // | |
| // Regression testing for https://github.com/flutter/flutter/issues/174952. | |
| output = ''' | |
| mWakefulness=0 | |
| mWakefulness=Asleep | |
| '''; | |
| } | |
| static void pretendArm64() { | |
| output = ''' | |
| arm64 | |
| '''; | |
| } | |
| @override | |
| Future<String> adb( | |
| List<String> arguments, { | |
| Map<String, String>? environment, | |
| bool silent = false, | |
| bool canFail = false, | |
| }) async { | |
| environment ??= <String, String>{}; | |
| commandLog.add( | |
| CommandArgs( | |
| command: 'adb', | |
| // ignore: prefer_spread_collections | |
| arguments: <String>['-s', deviceId]..addAll(arguments), | |
| environment: environment..putIfAbsent('canFail', () => '$canFail'), | |
| ), | |
| ); | |
| return output; | |
| } | |
| @override | |
| Future<String> shellEval( | |
| String command, | |
| List<String> arguments, { | |
| Map<String, String>? environment, | |
| bool silent = false, | |
| }) async { | |
| commandLog.add(CommandArgs(command: command, arguments: arguments, environment: environment)); | |
| return output; | |
| } | |
| @override | |
| Future<void> shellExec( | |
| String command, | |
| List<String> arguments, { | |
| Map<String, String>? 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<int> data) { | |
| writes.add(data); | |
| } | |
| @override | |
| Future<void> addStream(Stream<List<int>> stream) { | |
| final completer = Completer<void>(); | |
| stream.listen(add).onDone(completer.complete); | |
| return completer.future; | |
| } | |
| @override | |
| void writeCharCode(int charCode) { | |
| add(<int>[charCode]); | |
| } | |
| @override | |
| void write(Object? obj) { | |
| add(encoding.encode('$obj')); | |
| } | |
| @override | |
| void writeln([Object? obj = '']) { | |
| add(encoding.encode('$obj\n')); | |
| } | |
| @override | |
| void writeAll(Iterable<dynamic> objects, [String separator = '']) { | |
| var 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<void> get done => close(); | |
| @override | |
| Future<void> close() async {} | |
| @override | |
| Future<void> flush() async {} | |
| } |