Unverified Commit 359daf4f authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] reland: remove globals from android device/testing (#57614)

parent 478d4c9c
...@@ -5,19 +5,19 @@ ...@@ -5,19 +5,19 @@
import 'dart:async'; import 'dart:async';
import 'package:async/async.dart'; import 'package:async/async.dart';
import '../base/context.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../convert.dart'; import '../convert.dart';
/// Default factory that creates a real Android console connection. /// Default factory that creates a real Android console connection.
final AndroidConsoleSocketFactory _kAndroidConsoleSocketFactory = (String host, int port) => Socket.connect( host, port); ///
/// The default implementation will create real connections to a device.
/// Override this in tests with an implementation that returns mock responses.
Future<Socket> kAndroidConsoleSocketFactory(String host, int port) => Socket.connect(host, port);
/// Currently active implementation of the AndroidConsoleFactory. /// Currently active implementation of the AndroidConsoleFactory.
/// ///
/// The default implementation will create real connections to a device. /// The default implementation will create real connections to a device.
/// Override this in tests with an implementation that returns mock responses. /// Override this in tests with an implementation that returns mock responses.
AndroidConsoleSocketFactory get androidConsoleSocketFactory => context.get<AndroidConsoleSocketFactory>() ?? _kAndroidConsoleSocketFactory;
typedef AndroidConsoleSocketFactory = Future<Socket> Function(String host, int port); typedef AndroidConsoleSocketFactory = Future<Socket> Function(String host, int port);
/// Creates a console connection to an Android emulator that can be used to run /// Creates a console connection to an Android emulator that can be used to run
......
...@@ -14,11 +14,11 @@ import '../base/common.dart' show throwToolExit, unawaited; ...@@ -14,11 +14,11 @@ import '../base/common.dart' show throwToolExit, unawaited;
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../convert.dart'; import '../convert.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart'; import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
...@@ -46,11 +46,11 @@ const Map<String, _HardwareType> _kKnownHardware = <String, _HardwareType>{ ...@@ -46,11 +46,11 @@ const Map<String, _HardwareType> _kKnownHardware = <String, _HardwareType>{
'samsungexynos7570': _HardwareType.physical, 'samsungexynos7570': _HardwareType.physical,
}; };
bool allowHeapCorruptionOnWindows(int exitCode) { bool allowHeapCorruptionOnWindows(int exitCode, Platform platform) {
// In platform tools 29.0.0 adb.exe seems to be ending with this heap // In platform tools 29.0.0 adb.exe seems to be ending with this heap
// corruption error code on seemingly successful termination. // corruption error code on seemingly successful termination.
// So we ignore this error on Windows. // So we ignore this error on Windows.
return exitCode == -1073740940 && globals.platform.isWindows; return exitCode == -1073740940 && platform.isWindows;
} }
class AndroidDevice extends Device { class AndroidDevice extends Device {
...@@ -59,44 +59,68 @@ class AndroidDevice extends Device { ...@@ -59,44 +59,68 @@ class AndroidDevice extends Device {
this.productID, this.productID,
this.modelID, this.modelID,
this.deviceCodeName, this.deviceCodeName,
}) : super( @required Logger logger,
@required ProcessManager processManager,
@required Platform platform,
@required AndroidSdk androidSdk,
@required FileSystem fileSystem,
TimeoutConfiguration timeoutConfiguration = const TimeoutConfiguration(),
AndroidConsoleSocketFactory androidConsoleSocketFactory = kAndroidConsoleSocketFactory,
}) : _logger = logger,
_processManager = processManager,
_androidSdk = androidSdk,
_platform = platform,
_fileSystem = fileSystem,
_androidConsoleSocketFactory = androidConsoleSocketFactory,
_timeoutConfiguration = timeoutConfiguration,
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
super(
id, id,
category: Category.mobile, category: Category.mobile,
platformType: PlatformType.android, platformType: PlatformType.android,
ephemeral: true, ephemeral: true,
); );
final Logger _logger;
final ProcessManager _processManager;
final AndroidSdk _androidSdk;
final Platform _platform;
final FileSystem _fileSystem;
final ProcessUtils _processUtils;
final AndroidConsoleSocketFactory _androidConsoleSocketFactory;
final TimeoutConfiguration _timeoutConfiguration;
final String productID; final String productID;
final String modelID; final String modelID;
final String deviceCodeName; final String deviceCodeName;
Map<String, String> _properties; Map<String, String> _properties;
bool _isLocalEmulator; bool _isLocalEmulator;
TargetPlatform _platform; TargetPlatform _applicationPlatform;
Future<String> _getProperty(String name) async { Future<String> _getProperty(String name) async {
if (_properties == null) { if (_properties == null) {
_properties = <String, String>{}; _properties = <String, String>{};
final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']); final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
globals.printTrace(propCommand.join(' ')); _logger.printTrace(propCommand.join(' '));
try { try {
// We pass an encoding of latin1 so that we don't try and interpret the // We pass an encoding of latin1 so that we don't try and interpret the
// `adb shell getprop` result as UTF8. // `adb shell getprop` result as UTF8.
final ProcessResult result = await globals.processManager.run( final ProcessResult result = await _processManager.run(
propCommand, propCommand,
stdoutEncoding: latin1, stdoutEncoding: latin1,
stderrEncoding: latin1, stderrEncoding: latin1,
); );
if (result.exitCode == 0 || allowHeapCorruptionOnWindows(result.exitCode)) { if (result.exitCode == 0 || allowHeapCorruptionOnWindows(result.exitCode, _platform)) {
_properties = parseAdbDeviceProperties(result.stdout as String); _properties = parseAdbDeviceProperties(result.stdout as String);
} else { } else {
globals.printError('Error ${result.exitCode} retrieving device properties for $name:'); _logger.printError('Error ${result.exitCode} retrieving device properties for $name:');
globals.printError(result.stderr as String); _logger.printError(result.stderr as String);
} }
} on ProcessException catch (error) { } on ProcessException catch (error) {
globals.printError('Error retrieving device properties for $name: $error'); _logger.printError('Error retrieving device properties for $name: $error');
} }
} }
...@@ -107,14 +131,14 @@ class AndroidDevice extends Device { ...@@ -107,14 +131,14 @@ class AndroidDevice extends Device {
Future<bool> get isLocalEmulator async { Future<bool> get isLocalEmulator async {
if (_isLocalEmulator == null) { if (_isLocalEmulator == null) {
final String hardware = await _getProperty('ro.hardware'); final String hardware = await _getProperty('ro.hardware');
globals.printTrace('ro.hardware = $hardware'); _logger.printTrace('ro.hardware = $hardware');
if (_kKnownHardware.containsKey(hardware)) { if (_kKnownHardware.containsKey(hardware)) {
// Look for known hardware models. // Look for known hardware models.
_isLocalEmulator = _kKnownHardware[hardware] == _HardwareType.emulator; _isLocalEmulator = _kKnownHardware[hardware] == _HardwareType.emulator;
} else { } else {
// Fall back to a best-effort heuristic-based approach. // Fall back to a best-effort heuristic-based approach.
final String characteristics = await _getProperty('ro.build.characteristics'); final String characteristics = await _getProperty('ro.build.characteristics');
globals.printTrace('ro.build.characteristics = $characteristics'); _logger.printTrace('ro.build.characteristics = $characteristics');
_isLocalEmulator = characteristics != null && characteristics.contains('emulator'); _isLocalEmulator = characteristics != null && characteristics.contains('emulator');
} }
} }
...@@ -144,27 +168,27 @@ class AndroidDevice extends Device { ...@@ -144,27 +168,27 @@ class AndroidDevice extends Device {
const String host = 'localhost'; const String host = 'localhost';
final int port = int.parse(portMatch.group(1)); final int port = int.parse(portMatch.group(1));
globals.printTrace('Fetching avd name for $name via Android console on $host:$port'); _logger.printTrace('Fetching avd name for $name via Android console on $host:$port');
try { try {
final Socket socket = await androidConsoleSocketFactory(host, port); final Socket socket = await _androidConsoleSocketFactory(host, port);
final AndroidConsole console = AndroidConsole(socket); final AndroidConsole console = AndroidConsole(socket);
try { try {
await console await console
.connect() .connect()
.timeout(timeoutConfiguration.fastOperation, .timeout(_timeoutConfiguration.fastOperation,
onTimeout: () => throw TimeoutException('Connection timed out')); onTimeout: () => throw TimeoutException('Connection timed out'));
return await console return await console
.getAvdName() .getAvdName()
.timeout(timeoutConfiguration.fastOperation, .timeout(_timeoutConfiguration.fastOperation,
onTimeout: () => throw TimeoutException('"avd name" timed out')); onTimeout: () => throw TimeoutException('"avd name" timed out'));
} finally { } finally {
console.destroy(); console.destroy();
} }
} on Exception catch (e) { } on Exception catch (e) {
globals.printTrace('Failed to fetch avd name for emulator at $host:$port: $e'); _logger.printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
// If we fail to connect to the device, we should not fail so just return // If we fail to connect to the device, we should not fail so just return
// an empty name. This data is best-effort. // an empty name. This data is best-effort.
return null; return null;
...@@ -173,7 +197,7 @@ class AndroidDevice extends Device { ...@@ -173,7 +197,7 @@ class AndroidDevice extends Device {
@override @override
Future<TargetPlatform> get targetPlatform async { Future<TargetPlatform> get targetPlatform async {
if (_platform == null) { if (_applicationPlatform == null) {
// http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...) // http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
switch (await _getProperty('ro.product.cpu.abi')) { switch (await _getProperty('ro.product.cpu.abi')) {
case 'arm64-v8a': case 'arm64-v8a':
...@@ -183,24 +207,24 @@ class AndroidDevice extends Device { ...@@ -183,24 +207,24 @@ class AndroidDevice extends Device {
// to assuming 64 bit. // to assuming 64 bit.
final String abilist = await _getProperty('ro.product.cpu.abilist'); final String abilist = await _getProperty('ro.product.cpu.abilist');
if (abilist == null || abilist.contains('arm64-v8a')) { if (abilist == null || abilist.contains('arm64-v8a')) {
_platform = TargetPlatform.android_arm64; _applicationPlatform = TargetPlatform.android_arm64;
} else { } else {
_platform = TargetPlatform.android_arm; _applicationPlatform = TargetPlatform.android_arm;
} }
break; break;
case 'x86_64': case 'x86_64':
_platform = TargetPlatform.android_x64; _applicationPlatform = TargetPlatform.android_x64;
break; break;
case 'x86': case 'x86':
_platform = TargetPlatform.android_x86; _applicationPlatform = TargetPlatform.android_x86;
break; break;
default: default:
_platform = TargetPlatform.android_arm; _applicationPlatform = TargetPlatform.android_arm;
break; break;
} }
} }
return _platform; return _applicationPlatform;
} }
@override @override
...@@ -217,7 +241,7 @@ class AndroidDevice extends Device { ...@@ -217,7 +241,7 @@ class AndroidDevice extends Device {
AndroidDevicePortForwarder _portForwarder; AndroidDevicePortForwarder _portForwarder;
List<String> adbCommandForDevice(List<String> args) { List<String> adbCommandForDevice(List<String> args) {
return <String>[getAdbPath(globals.androidSdk), '-s', id, ...args]; return <String>[getAdbPath(_androidSdk), '-s', id, ...args];
} }
String runAdbCheckedSync( String runAdbCheckedSync(
...@@ -226,13 +250,13 @@ class AndroidDevice extends Device { ...@@ -226,13 +250,13 @@ class AndroidDevice extends Device {
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
Map<String, String> environment, Map<String, String> environment,
}) { }) {
return processUtils.runSync( return _processUtils.runSync(
adbCommandForDevice(params), adbCommandForDevice(params),
throwOnError: true, throwOnError: true,
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter, allowReentrantFlutter: allowReentrantFlutter,
environment: environment, environment: environment,
whiteListFailures: allowHeapCorruptionOnWindows, whiteListFailures: (int value) => allowHeapCorruptionOnWindows(value, _platform),
).stdout.trim(); ).stdout.trim();
} }
...@@ -241,12 +265,12 @@ class AndroidDevice extends Device { ...@@ -241,12 +265,12 @@ class AndroidDevice extends Device {
String workingDirectory, String workingDirectory,
bool allowReentrantFlutter = false, bool allowReentrantFlutter = false,
}) async { }) async {
return processUtils.run( return _processUtils.run(
adbCommandForDevice(params), adbCommandForDevice(params),
throwOnError: true, throwOnError: true,
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter, allowReentrantFlutter: allowReentrantFlutter,
whiteListFailures: allowHeapCorruptionOnWindows, whiteListFailures: (int value) => allowHeapCorruptionOnWindows(value, _platform),
); );
} }
...@@ -268,27 +292,27 @@ class AndroidDevice extends Device { ...@@ -268,27 +292,27 @@ class AndroidDevice extends Device {
} }
return false; return false;
} }
globals.printError( _logger.printError(
'Unrecognized adb version string $adbVersion. Skipping version check.'); 'Unrecognized adb version string $adbVersion. Skipping version check.');
return true; return true;
} }
Future<bool> _checkForSupportedAdbVersion() async { Future<bool> _checkForSupportedAdbVersion() async {
if (globals.androidSdk == null) { if (_androidSdk == null) {
return false; return false;
} }
try { try {
final RunResult adbVersion = await processUtils.run( final RunResult adbVersion = await _processUtils.run(
<String>[getAdbPath(globals.androidSdk), 'version'], <String>[getAdbPath(_androidSdk), 'version'],
throwOnError: true, throwOnError: true,
); );
if (_isValidAdbVersion(adbVersion.stdout)) { if (_isValidAdbVersion(adbVersion.stdout)) {
return true; return true;
} }
globals.printError('The ADB at "${getAdbPath(globals.androidSdk)}" is too old; please install version 1.0.39 or later.'); _logger.printError('The ADB at "${getAdbPath(_androidSdk)}" is too old; please install version 1.0.39 or later.');
} on Exception catch (error, trace) { } on Exception catch (error, trace) {
globals.printError('Error running ADB: $error', stackTrace: trace); _logger.printError('Error running ADB: $error', stackTrace: trace);
} }
return false; return false;
...@@ -300,8 +324,8 @@ class AndroidDevice extends Device { ...@@ -300,8 +324,8 @@ class AndroidDevice extends Device {
// output lines like this, which we want to ignore: // output lines like this, which we want to ignore:
// adb server is out of date. killing.. // adb server is out of date. killing..
// * daemon started successfully * // * daemon started successfully *
await processUtils.run( await _processUtils.run(
<String>[getAdbPath(globals.androidSdk), 'start-server'], <String>[getAdbPath(_androidSdk), 'start-server'],
throwOnError: true, throwOnError: true,
); );
...@@ -313,12 +337,12 @@ class AndroidDevice extends Device { ...@@ -313,12 +337,12 @@ class AndroidDevice extends Device {
final int sdkVersionParsed = int.tryParse(sdkVersion); final int sdkVersionParsed = int.tryParse(sdkVersion);
if (sdkVersionParsed == null) { if (sdkVersionParsed == null) {
globals.printError('Unexpected response from getprop: "$sdkVersion"'); _logger.printError('Unexpected response from getprop: "$sdkVersion"');
return false; return false;
} }
if (sdkVersionParsed < minApiLevel) { if (sdkVersionParsed < minApiLevel) {
globals.printError( _logger.printError(
'The Android version ($sdkVersion) on the target device is too old. Please ' 'The Android version ($sdkVersion) on the target device is too old. Please '
'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.'); 'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
return false; return false;
...@@ -326,8 +350,8 @@ class AndroidDevice extends Device { ...@@ -326,8 +350,8 @@ class AndroidDevice extends Device {
return true; return true;
} on Exception catch (e, stacktrace) { } on Exception catch (e, stacktrace) {
globals.printError('Unexpected failure from adb: $e'); _logger.printError('Unexpected failure from adb: $e');
globals.printError('Stacktrace: $stacktrace'); _logger.printError('Stacktrace: $stacktrace');
return false; return false;
} }
} }
...@@ -337,13 +361,13 @@ class AndroidDevice extends Device { ...@@ -337,13 +361,13 @@ class AndroidDevice extends Device {
} }
Future<String> _getDeviceApkSha1(AndroidApk apk) async { Future<String> _getDeviceApkSha1(AndroidApk apk) async {
final RunResult result = await processUtils.run( final RunResult result = await _processUtils.run(
adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(apk)])); adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(apk)]));
return result.stdout; return result.stdout;
} }
String _getSourceSha1(AndroidApk apk) { String _getSourceSha1(AndroidApk apk) {
final File shaFile = globals.fs.file('${apk.file.path}.sha1'); final File shaFile = _fileSystem.file('${apk.file.path}.sha1');
return shaFile.existsSync() ? shaFile.readAsStringSync() : ''; return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
} }
...@@ -357,7 +381,7 @@ class AndroidDevice extends Device { ...@@ -357,7 +381,7 @@ class AndroidDevice extends Device {
final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]); final RunResult listOut = await runAdbCheckedAsync(<String>['shell', 'pm', 'list', 'packages', app.id]);
return LineSplitter.split(listOut.stdout).contains('package:${app.id}'); return LineSplitter.split(listOut.stdout).contains('package:${app.id}');
} on Exception catch (error) { } on Exception catch (error) {
globals.printTrace('$error'); _logger.printTrace('$error');
return false; return false;
} }
} }
...@@ -371,7 +395,7 @@ class AndroidDevice extends Device { ...@@ -371,7 +395,7 @@ class AndroidDevice extends Device {
@override @override
Future<bool> installApp(AndroidApk app) async { Future<bool> installApp(AndroidApk app) async {
if (!app.file.existsSync()) { if (!app.file.existsSync()) {
globals.printError('"${globals.fs.path.relative(app.file.path)}" does not exist.'); _logger.printError('"${_fileSystem.path.relative(app.file.path)}" does not exist.');
return false; return false;
} }
...@@ -380,8 +404,11 @@ class AndroidDevice extends Device { ...@@ -380,8 +404,11 @@ class AndroidDevice extends Device {
return false; return false;
} }
final Status status = globals.logger.startProgress('Installing ${globals.fs.path.relative(app.file.path)}...', timeout: timeoutConfiguration.slowOperation); final Status status = _logger.startProgress(
final RunResult installResult = await processUtils.run( 'Installing ${_fileSystem.path.relative(app.file.path)}...',
timeout: _timeoutConfiguration.slowOperation,
);
final RunResult installResult = await _processUtils.run(
adbCommandForDevice(<String>['install', '-t', '-r', app.file.path])); adbCommandForDevice(<String>['install', '-t', '-r', app.file.path]));
status.stop(); status.stop();
// Some versions of adb exit with exit code 0 even on failure :( // Some versions of adb exit with exit code 0 even on failure :(
...@@ -389,12 +416,12 @@ class AndroidDevice extends Device { ...@@ -389,12 +416,12 @@ class AndroidDevice extends Device {
final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true); final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
final String failure = failureExp.stringMatch(installResult.stdout); final String failure = failureExp.stringMatch(installResult.stdout);
if (failure != null) { if (failure != null) {
globals.printError('Package install error: $failure'); _logger.printError('Package install error: $failure');
return false; return false;
} }
if (installResult.exitCode != 0) { if (installResult.exitCode != 0) {
globals.printError('Error: ADB exited with exit code ${installResult.exitCode}'); _logger.printError('Error: ADB exited with exit code ${installResult.exitCode}');
globals.printError('$installResult'); _logger.printError('$installResult');
return false; return false;
} }
try { try {
...@@ -402,7 +429,7 @@ class AndroidDevice extends Device { ...@@ -402,7 +429,7 @@ class AndroidDevice extends Device {
'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app), 'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app),
]); ]);
} on ProcessException catch (error) { } on ProcessException catch (error) {
globals.printError('adb shell failed to write the SHA hash: $error.'); _logger.printError('adb shell failed to write the SHA hash: $error.');
return false; return false;
} }
return true; return true;
...@@ -417,19 +444,19 @@ class AndroidDevice extends Device { ...@@ -417,19 +444,19 @@ class AndroidDevice extends Device {
String uninstallOut; String uninstallOut;
try { try {
final RunResult uninstallResult = await processUtils.run( final RunResult uninstallResult = await _processUtils.run(
adbCommandForDevice(<String>['uninstall', app.id]), adbCommandForDevice(<String>['uninstall', app.id]),
throwOnError: true, throwOnError: true,
); );
uninstallOut = uninstallResult.stdout; uninstallOut = uninstallResult.stdout;
} on Exception catch (error) { } on Exception catch (error) {
globals.printError('adb uninstall failed: $error'); _logger.printError('adb uninstall failed: $error');
return false; return false;
} }
final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true); final RegExp failureExp = RegExp(r'^Failure.*$', multiLine: true);
final String failure = failureExp.stringMatch(uninstallOut); final String failure = failureExp.stringMatch(uninstallOut);
if (failure != null) { if (failure != null) {
globals.printError('Package uninstall error: $failure'); _logger.printError('Package uninstall error: $failure');
return false; return false;
} }
...@@ -440,21 +467,21 @@ class AndroidDevice extends Device { ...@@ -440,21 +467,21 @@ class AndroidDevice extends Device {
final bool wasInstalled = await isAppInstalled(package); final bool wasInstalled = await isAppInstalled(package);
if (wasInstalled) { if (wasInstalled) {
if (await isLatestBuildInstalled(package)) { if (await isLatestBuildInstalled(package)) {
globals.printTrace('Latest build already installed.'); _logger.printTrace('Latest build already installed.');
return true; return true;
} }
} }
globals.printTrace('Installing APK.'); _logger.printTrace('Installing APK.');
if (!await installApp(package)) { if (!await installApp(package)) {
globals.printTrace('Warning: Failed to install APK.'); _logger.printTrace('Warning: Failed to install APK.');
if (wasInstalled) { if (wasInstalled) {
globals.printStatus('Uninstalling old version...'); _logger.printStatus('Uninstalling old version...');
if (!await uninstallApp(package)) { if (!await uninstallApp(package)) {
globals.printError('Error: Uninstalling old version failed.'); _logger.printError('Error: Uninstalling old version failed.');
return false; return false;
} }
if (!await installApp(package)) { if (!await installApp(package)) {
globals.printError('Error: Failed to install APK again.'); _logger.printError('Error: Failed to install APK again.');
return false; return false;
} }
return true; return true;
...@@ -484,7 +511,7 @@ class AndroidDevice extends Device { ...@@ -484,7 +511,7 @@ class AndroidDevice extends Device {
final TargetPlatform devicePlatform = await targetPlatform; final TargetPlatform devicePlatform = await targetPlatform;
if (devicePlatform == TargetPlatform.android_x86 && if (devicePlatform == TargetPlatform.android_x86 &&
!debuggingOptions.buildInfo.isDebug) { !debuggingOptions.buildInfo.isDebug) {
globals.printError('Profile and release builds are only supported on ARM/x64 targets.'); _logger.printError('Profile and release builds are only supported on ARM/x64 targets.');
return LaunchResult.failed(); return LaunchResult.failed();
} }
...@@ -503,12 +530,12 @@ class AndroidDevice extends Device { ...@@ -503,12 +530,12 @@ class AndroidDevice extends Device {
androidArch = AndroidArch.x86; androidArch = AndroidArch.x86;
break; break;
default: default:
globals.printError('Android platforms are only supported.'); _logger.printError('Android platforms are only supported.');
return LaunchResult.failed(); return LaunchResult.failed();
} }
if (!prebuiltApplication || globals.androidSdk.licensesAvailable && globals.androidSdk.latestVersion == null) { if (!prebuiltApplication || _androidSdk.licensesAvailable && _androidSdk.latestVersion == null) {
globals.printTrace('Building APK'); _logger.printTrace('Building APK');
final FlutterProject project = FlutterProject.current(); final FlutterProject project = FlutterProject.current();
await androidBuilder.buildApk( await androidBuilder.buildApk(
project: project, project: project,
...@@ -528,7 +555,7 @@ class AndroidDevice extends Device { ...@@ -528,7 +555,7 @@ class AndroidDevice extends Device {
throwToolExit('Problem building Android application: see above error(s).'); throwToolExit('Problem building Android application: see above error(s).');
} }
globals.printTrace("Stopping app '${package.name}' on $name."); _logger.printTrace("Stopping app '${package.name}' on $name.");
await stopApp(package); await stopApp(package);
if (!await _installLatestApp(package)) { if (!await _installLatestApp(package)) {
...@@ -536,7 +563,7 @@ class AndroidDevice extends Device { ...@@ -536,7 +563,7 @@ class AndroidDevice extends Device {
} }
final bool traceStartup = platformArgs['trace-startup'] as bool ?? false; final bool traceStartup = platformArgs['trace-startup'] as bool ?? false;
globals.printTrace('$this startApp'); _logger.printTrace('$this startApp');
ProtocolDiscovery observatoryDiscovery; ProtocolDiscovery observatoryDiscovery;
...@@ -601,7 +628,7 @@ class AndroidDevice extends Device { ...@@ -601,7 +628,7 @@ class AndroidDevice extends Device {
final String result = (await runAdbCheckedAsync(cmd)).stdout; final String result = (await runAdbCheckedAsync(cmd)).stdout;
// This invocation returns 0 even when it fails. // This invocation returns 0 even when it fails.
if (result.contains('Error: ')) { if (result.contains('Error: ')) {
globals.printError(result.trim(), wrap: false); _logger.printError(result.trim(), wrap: false);
return LaunchResult.failed(); return LaunchResult.failed();
} }
...@@ -612,7 +639,7 @@ class AndroidDevice extends Device { ...@@ -612,7 +639,7 @@ class AndroidDevice extends Device {
// Wait for the service protocol port here. This will complete once the // Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on...". // device has printed "Observatory is listening on...".
globals.printTrace('Waiting for observatory port to be available...'); _logger.printTrace('Waiting for observatory port to be available...');
// TODO(danrubel): Waiting for observatory services can be made common across all devices. // TODO(danrubel): Waiting for observatory services can be made common across all devices.
try { try {
...@@ -620,7 +647,7 @@ class AndroidDevice extends Device { ...@@ -620,7 +647,7 @@ class AndroidDevice extends Device {
if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) { if (debuggingOptions.buildInfo.isDebug || debuggingOptions.buildInfo.isProfile) {
observatoryUri = await observatoryDiscovery.uri; observatoryUri = await observatoryDiscovery.uri;
if (observatoryUri == null) { if (observatoryUri == null) {
globals.printError( _logger.printError(
'Error waiting for a debug connection: ' 'Error waiting for a debug connection: '
'The log reader stopped unexpectedly', 'The log reader stopped unexpectedly',
); );
...@@ -629,7 +656,7 @@ class AndroidDevice extends Device { ...@@ -629,7 +656,7 @@ class AndroidDevice extends Device {
} }
return LaunchResult.succeeded(observatoryUri: observatoryUri); return LaunchResult.succeeded(observatoryUri: observatoryUri);
} on Exception catch (error) { } on Exception catch (error) {
globals.printError('Error waiting for a debug connection: $error'); _logger.printError('Error waiting for a debug connection: $error');
return LaunchResult.failed(); return LaunchResult.failed();
} finally { } finally {
await observatoryDiscovery.cancel(); await observatoryDiscovery.cancel();
...@@ -651,13 +678,13 @@ class AndroidDevice extends Device { ...@@ -651,13 +678,13 @@ class AndroidDevice extends Device {
return Future<bool>.value(false); return Future<bool>.value(false);
} }
final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]); final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
return processUtils.stream(command).then<bool>( return _processUtils.stream(command).then<bool>(
(int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode)); (int exitCode) => exitCode == 0 || allowHeapCorruptionOnWindows(exitCode, _platform));
} }
@override @override
Future<MemoryInfo> queryMemoryInfo() async { Future<MemoryInfo> queryMemoryInfo() async {
final RunResult runResult = await processUtils.run(adbCommandForDevice(<String>[ final RunResult runResult = await _processUtils.run(adbCommandForDevice(<String>[
'shell', 'shell',
'dumpsys', 'dumpsys',
'meminfo', 'meminfo',
...@@ -673,7 +700,7 @@ class AndroidDevice extends Device { ...@@ -673,7 +700,7 @@ class AndroidDevice extends Device {
@override @override
void clearLogs() { void clearLogs() {
processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c'])); _processUtils.runSync(adbCommandForDevice(<String>['logcat', '-c']));
} }
@override @override
...@@ -685,23 +712,23 @@ class AndroidDevice extends Device { ...@@ -685,23 +712,23 @@ class AndroidDevice extends Device {
if (includePastLogs) { if (includePastLogs) {
return _pastLogReader ??= await AdbLogReader.createLogReader( return _pastLogReader ??= await AdbLogReader.createLogReader(
this, this,
globals.processManager, _processManager,
includePastLogs: true, includePastLogs: true,
); );
} else { } else {
return _logReader ??= await AdbLogReader.createLogReader( return _logReader ??= await AdbLogReader.createLogReader(
this, this,
globals.processManager, _processManager,
); );
} }
} }
@override @override
DevicePortForwarder get portForwarder => _portForwarder ??= AndroidDevicePortForwarder( DevicePortForwarder get portForwarder => _portForwarder ??= AndroidDevicePortForwarder(
processManager: globals.processManager, processManager: _processManager,
logger: globals.logger, logger: _logger,
deviceId: id, deviceId: id,
adbPath: globals.androidSdk.adbPath, adbPath: _androidSdk.adbPath,
); );
static final RegExp _timeRegExp = RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true); static final RegExp _timeRegExp = RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);
...@@ -715,7 +742,7 @@ class AndroidDevice extends Device { ...@@ -715,7 +742,7 @@ class AndroidDevice extends Device {
'shell', '-x', 'logcat', '-v', 'time', '-t', '1' 'shell', '-x', 'logcat', '-v', 'time', '-t', '1'
]); ]);
} on Exception catch (error) { } on Exception catch (error) {
globals.printError('Failed to extract the most recent timestamp from the Android log: $error.'); _logger.printError('Failed to extract the most recent timestamp from the Android log: $error.');
return null; return null;
} }
final Match timeMatch = _timeRegExp.firstMatch(output); final Match timeMatch = _timeRegExp.firstMatch(output);
...@@ -732,7 +759,7 @@ class AndroidDevice extends Device { ...@@ -732,7 +759,7 @@ class AndroidDevice extends Device {
Future<void> takeScreenshot(File outputFile) async { Future<void> takeScreenshot(File outputFile) async {
const String remotePath = '/data/local/tmp/flutter_screenshot.png'; const String remotePath = '/data/local/tmp/flutter_screenshot.png';
await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]); await runAdbCheckedAsync(<String>['shell', 'screencap', '-p', remotePath]);
await processUtils.run( await _processUtils.run(
adbCommandForDevice(<String>['pull', remotePath, outputFile.path]), adbCommandForDevice(<String>['pull', remotePath, outputFile.path]),
throwOnError: true, throwOnError: true,
); );
......
...@@ -6,8 +6,10 @@ import 'package:meta/meta.dart'; ...@@ -6,8 +6,10 @@ import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
...@@ -63,7 +65,11 @@ class AndroidDevices extends PollingDeviceDiscovery { ...@@ -63,7 +65,11 @@ class AndroidDevices extends PollingDeviceDiscovery {
'ANDROID_HOME environment variable: ${exception.executable}'); 'ANDROID_HOME environment variable: ${exception.executable}');
} }
final List<AndroidDevice> devices = <AndroidDevice>[]; final List<AndroidDevice> devices = <AndroidDevice>[];
parseADBDeviceOutput(text, devices: devices); parseADBDeviceOutput(
text,
devices: devices,
timeoutConfiguration: timeoutConfiguration,
);
return devices; return devices;
} }
...@@ -80,7 +86,11 @@ class AndroidDevices extends PollingDeviceDiscovery { ...@@ -80,7 +86,11 @@ class AndroidDevices extends PollingDeviceDiscovery {
} else { } else {
final String text = result.stdout; final String text = result.stdout;
final List<String> diagnostics = <String>[]; final List<String> diagnostics = <String>[];
parseADBDeviceOutput(text, diagnostics: diagnostics); parseADBDeviceOutput(
text,
diagnostics: diagnostics,
timeoutConfiguration: timeoutConfiguration,
);
return diagnostics; return diagnostics;
} }
} }
...@@ -96,6 +106,12 @@ class AndroidDevices extends PollingDeviceDiscovery { ...@@ -96,6 +106,12 @@ class AndroidDevices extends PollingDeviceDiscovery {
String text, { String text, {
List<AndroidDevice> devices, List<AndroidDevice> devices,
List<String> diagnostics, List<String> diagnostics,
AndroidSdk androidSdk,
FileSystem fileSystem,
Logger logger,
Platform platform,
ProcessManager processManager,
@required TimeoutConfiguration timeoutConfiguration,
}) { }) {
// Check for error messages from adb // Check for error messages from adb
if (!text.contains('List of devices')) { if (!text.contains('List of devices')) {
...@@ -154,6 +170,12 @@ class AndroidDevices extends PollingDeviceDiscovery { ...@@ -154,6 +170,12 @@ class AndroidDevices extends PollingDeviceDiscovery {
productID: info['product'], productID: info['product'],
modelID: info['model'] ?? deviceID, modelID: info['model'] ?? deviceID,
deviceCodeName: info['device'], deviceCodeName: info['device'],
androidSdk: androidSdk ?? globals.androidSdk,
fileSystem: fileSystem ?? globals.fs,
logger: logger ?? globals.logger,
platform: platform ?? globals.platform,
processManager: processManager ?? globals.processManager,
timeoutConfiguration: timeoutConfiguration,
)); ));
} }
} else { } else {
......
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_device_discovery.dart'; import 'package:flutter_tools/src/android/android_device_discovery.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_workflow.dart'; import 'package:flutter_tools/src/android/android_workflow.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -69,7 +71,15 @@ void main() { ...@@ -69,7 +71,15 @@ void main() {
List of devices attached List of devices attached
05a02bac device usb:336592896X product:razor model:Nexus_7 device:flo 05a02bac device usb:336592896X product:razor model:Nexus_7 device:flo
''', devices: devices); ''',
devices: devices,
androidSdk: MockAndroidSdk(),
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
timeoutConfiguration: const TimeoutConfiguration(),
platform: FakePlatform(),
fileSystem: MemoryFileSystem.test(),
);
expect(devices, hasLength(1)); expect(devices, hasLength(1));
expect(devices.first.name, 'Nexus 7'); expect(devices.first.name, 'Nexus 7');
...@@ -84,7 +94,15 @@ localhost:36790 device ...@@ -84,7 +94,15 @@ localhost:36790 device
0149947A0D01500C device usb:340787200X 0149947A0D01500C device usb:340787200X
emulator-5612 host features:shell_2 emulator-5612 host features:shell_2
''', devices: devices); ''',
devices: devices,
androidSdk: MockAndroidSdk(),
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
timeoutConfiguration: const TimeoutConfiguration(),
platform: FakePlatform(),
fileSystem: MemoryFileSystem.test(),
);
expect(devices, hasLength(3)); expect(devices, hasLength(3));
expect(devices.first.name, 'localhost:36790'); expect(devices.first.name, 'localhost:36790');
...@@ -95,7 +113,15 @@ emulator-5612 host features:shell_2 ...@@ -95,7 +113,15 @@ emulator-5612 host features:shell_2
AndroidDevices.parseADBDeviceOutput(''' AndroidDevices.parseADBDeviceOutput('''
List of devices attached List of devices attached
ZX1G22JJWR device usb:3-3 product:shamu model:Nexus_6 device:shamu features:cmd,shell_v2 ZX1G22JJWR device usb:3-3 product:shamu model:Nexus_6 device:shamu features:cmd,shell_v2
''', devices: devices); ''',
devices: devices,
androidSdk: MockAndroidSdk(),
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
timeoutConfiguration: const TimeoutConfiguration(),
platform: FakePlatform(),
fileSystem: MemoryFileSystem.test(),
);
expect(devices, hasLength(1)); expect(devices, hasLength(1));
expect(devices.first.name, 'Nexus 6'); expect(devices.first.name, 'Nexus 6');
...@@ -108,7 +134,10 @@ ZX1G22JJWR device usb:3-3 product:shamu model:Nexus_6 device:shamu f ...@@ -108,7 +134,10 @@ ZX1G22JJWR device usb:3-3 product:shamu model:Nexus_6 device:shamu f
It appears you do not have 'Android SDK Platform-tools' installed. It appears you do not have 'Android SDK Platform-tools' installed.
Use the 'android' tool to install them: Use the 'android' tool to install them:
android update sdk --no-ui --filter 'platform-tools' android update sdk --no-ui --filter 'platform-tools'
''', devices: devices, diagnostics: diagnostics); ''', devices: devices,
diagnostics: diagnostics,
timeoutConfiguration: const TimeoutConfiguration(),
);
expect(devices, isEmpty); expect(devices, isEmpty);
expect(diagnostics, hasLength(1)); expect(diagnostics, hasLength(1));
......
...@@ -7,6 +7,8 @@ import 'package:flutter_tools/src/android/android_device.dart'; ...@@ -7,6 +7,8 @@ import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -55,11 +57,16 @@ void main() { ...@@ -55,11 +57,16 @@ void main() {
TargetPlatform.android_arm64, TargetPlatform.android_arm64,
TargetPlatform.android_x64, TargetPlatform.android_x64,
]) { ]) {
testUsingContext('AndroidDevice.startApp allows release builds on $targetPlatform', () async { testWithoutContext('AndroidDevice.startApp allows release builds on $targetPlatform', () async {
const String deviceId = '1234';
final String arch = getNameForAndroidArch( final String arch = getNameForAndroidArch(
getAndroidArchForName(getNameForTargetPlatform(targetPlatform))); getAndroidArchForName(getNameForTargetPlatform(targetPlatform)));
final AndroidDevice device = AndroidDevice(deviceId, modelID: 'TestModel'); final AndroidDevice device = AndroidDevice('1234', modelID: 'TestModel',
fileSystem: fileSystem,
processManager: processManager,
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
);
final File apkFile = fileSystem.file('app.apk')..createSync(); final File apkFile = fileSystem.file('app.apk')..createSync();
final AndroidApk apk = AndroidApk( final AndroidApk apk = AndroidApk(
id: 'FlutterApp', id: 'FlutterApp',
...@@ -117,16 +124,17 @@ void main() { ...@@ -117,16 +124,17 @@ void main() {
expect(launchResult.started, true); expect(launchResult.started, true);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
AndroidSdk: () => androidSdk,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
}); });
} }
testUsingContext('AndroidDevice.startApp does not allow release builds on x86', () async { testWithoutContext('AndroidDevice.startApp does not allow release builds on x86', () async {
const String deviceId = '1234'; final AndroidDevice device = AndroidDevice('1234', modelID: 'TestModel',
final AndroidDevice device = AndroidDevice(deviceId, modelID: 'TestModel'); fileSystem: fileSystem,
processManager: processManager,
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
);
final File apkFile = fileSystem.file('app.apk')..createSync(); final File apkFile = fileSystem.file('app.apk')..createSync();
final AndroidApk apk = AndroidApk( final AndroidApk apk = AndroidApk(
id: 'FlutterApp', id: 'FlutterApp',
...@@ -155,15 +163,16 @@ void main() { ...@@ -155,15 +163,16 @@ void main() {
expect(launchResult.started, false); expect(launchResult.started, false);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
AndroidSdk: () => androidSdk,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
}); });
testUsingContext('AndroidDevice.startApp forwards all supported debugging options', () async { testWithoutContext('AndroidDevice.startApp forwards all supported debugging options', () async {
const String deviceId = '1234'; final AndroidDevice device = AndroidDevice('1234', modelID: 'TestModel',
final AndroidDevice device = AndroidDevice(deviceId, modelID: 'TestModel'); fileSystem: fileSystem,
processManager: processManager,
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
);
final File apkFile = fileSystem.file('app.apk')..createSync(); final File apkFile = fileSystem.file('app.apk')..createSync();
final AndroidApk apk = AndroidApk( final AndroidApk apk = AndroidApk(
id: 'FlutterApp', id: 'FlutterApp',
...@@ -264,10 +273,6 @@ void main() { ...@@ -264,10 +273,6 @@ void main() {
// This fails to start due to observatory discovery issues. // This fails to start due to observatory discovery issues.
expect(launchResult.started, false); expect(launchResult.started, false);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
AndroidSdk: () => androidSdk,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
}); });
} }
......
...@@ -2,14 +2,28 @@ ...@@ -2,14 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart';
void main() { void main() {
testWithoutContext('AndroidDevice.stopApp handles a null ApplicationPackage', () async { testWithoutContext('AndroidDevice.stopApp handles a null ApplicationPackage', () async {
final AndroidDevice androidDevice = AndroidDevice('2'); final AndroidDevice androidDevice = AndroidDevice('1234',
androidSdk: MockAndroidSdk(),
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
processManager: FakeProcessManager.any(),
);
expect(await androidDevice.stopApp(null), false); expect(await androidDevice.stopApp(null), false);
}); });
} }
class MockAndroidSdk extends Mock implements AndroidSdk {}
...@@ -9,13 +9,13 @@ import 'dart:typed_data'; ...@@ -9,13 +9,13 @@ import 'dart:typed_data';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_console.dart'; import 'package:flutter_tools/src/android/android_console.dart';
import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.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/platform.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
...@@ -24,239 +24,150 @@ import '../../src/common.dart'; ...@@ -24,239 +24,150 @@ import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
void main() { void main() {
testUsingContext('AndroidDevice stores the requested id', () { testWithoutContext('AndroidDevice stores the requested id', () {
const String deviceId = '1234'; final AndroidDevice device = setUpAndroidDevice();
final AndroidDevice device = AndroidDevice(deviceId);
expect(device.id, deviceId); expect(device.id, '1234');
}); });
group('parseAdbDeviceProperties', () { testWithoutContext('parseAdbDeviceProperties parses adb shell output', () {
test('parse adb shell output', () {
final Map<String, String> properties = parseAdbDeviceProperties(kAdbShellGetprop); final Map<String, String> properties = parseAdbDeviceProperties(kAdbShellGetprop);
expect(properties, isNotNull); expect(properties, isNotNull);
expect(properties['ro.build.characteristics'], 'emulator'); expect(properties['ro.build.characteristics'], 'emulator');
expect(properties['ro.product.cpu.abi'], 'x86_64'); expect(properties['ro.product.cpu.abi'], 'x86_64');
expect(properties['ro.build.version.sdk'], '23'); expect(properties['ro.build.version.sdk'], '23');
}); });
});
group('adb.exe exiting with heap corruption on windows', () {
final ProcessManager mockProcessManager = MockProcessManager();
String hardware;
String buildCharacteristics;
setUp(() {
hardware = 'goldfish';
buildCharacteristics = 'unused';
exitCode = -1;
when(mockProcessManager.run(
argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'),
)).thenAnswer((_) {
final StringBuffer buf = StringBuffer()
..writeln('[ro.hardware]: [$hardware]')..writeln(
'[ro.build.characteristics]: [$buildCharacteristics]');
final ProcessResult result = ProcessResult(1, exitCode, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
});
testUsingContext('nonHeapCorruptionErrorOnWindows', () async { testWithoutContext('adb exiting with heap corruption is only allowed on windows', () async {
exitCode = -1073740941; final List<FakeCommand> commands = <FakeCommand>[
final AndroidDevice device = AndroidDevice('test'); const FakeCommand(
expect(await device.isLocalEmulator, false); command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
}, overrides: <Type, Generator>{ stdout: '[ro.hardware]: [goldfish]\n[ro.build.characteristics]: [unused]',
ProcessManager: () => mockProcessManager, // Heap corruption exit code.
Platform: () => FakePlatform( exitCode: -1073740940,
operatingSystem: 'windows', )
environment: <String, String>{ ];
'ANDROID_HOME': '/',
}, final AndroidDevice windowsDevice = setUpAndroidDevice(
), processManager: FakeProcessManager.list(commands.toList()),
platform: FakePlatform(operatingSystem: 'windows'),
);
final AndroidDevice linuxDevice = setUpAndroidDevice(
processManager: FakeProcessManager.list(commands.toList()),
platform: FakePlatform(operatingSystem: 'linux'),
);
final AndroidDevice macOsDevice = setUpAndroidDevice(
processManager: FakeProcessManager.list(commands.toList()),
platform: FakePlatform(operatingSystem: 'macos')
);
// Parsing succeedes despite the error.
expect(await windowsDevice.isLocalEmulator, true);
// Parsing fails and these default to false.
expect(await linuxDevice.isLocalEmulator, false);
expect(await macOsDevice.isLocalEmulator, false);
});
testWithoutContext('AndroidDevice can detect TargetPlatform from property '
'abi and abiList', () async {
// The format is [ABI, ABI list]: expected target platform.
final Map<List<String>, TargetPlatform> values = <List<String>, TargetPlatform>{
<String>['x86_64', 'unknown']: TargetPlatform.android_x64,
<String>['x86', 'unknown']: TargetPlatform.android_x86,
// The default ABI is arm32
<String>['???', 'unknown']: TargetPlatform.android_arm,
<String>['arm64-v8a', 'arm64-v8a,']: TargetPlatform.android_arm64,
// The Kindle Fire runs 32 bit apps on 64 bit hardware.
<String>['arm64-v8a', 'arm']: TargetPlatform.android_arm,
};
for (final MapEntry<List<String>, TargetPlatform> entry in values.entries) {
final AndroidDevice device = setUpAndroidDevice(
processManager: FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>['adb', '-s', '1234', 'shell', 'getprop'],
stdout: '[ro.product.cpu.abi]: [${entry.key.first}]\n'
'[ro.product.cpu.abilist]: [${entry.key.last}]'
)
]),
);
expect(await device.targetPlatform, entry.value);
}
}); });
testUsingContext('heapCorruptionOnWindows', () async { testWithoutContext('AndroidDevice can detect local emulator for known types', () async {
exitCode = -1073740940; final Set<String> knownPhyiscal = <String>{
final AndroidDevice device = AndroidDevice('test'); 'qcom',
expect(await device.isLocalEmulator, true); 'samsungexynos7420',
}, overrides: <Type, Generator>{ 'samsungexynos7580',
ProcessManager: () => mockProcessManager, 'samsungexynos7870',
Platform: () => FakePlatform( 'samsungexynos7880',
operatingSystem: 'windows', 'samsungexynos8890',
environment: <String, String>{ 'samsungexynos8895',
'ANDROID_HOME': '/', 'samsungexynos9810',
}, 'samsungexynos7570',
};
final Set<String> knownEmulator = <String>{
'goldfish',
'ranchu',
};
for (final String hardware in knownPhyiscal.followedBy(knownEmulator)) {
final AndroidDevice device = setUpAndroidDevice(
processManager: FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>[
'adb', '-s', '1234', 'shell', 'getprop',
],
stdout: '[ro.hardware]: [$hardware]\n'
'[ro.build.characteristics]: [unused]'
), ),
}); ])
);
testUsingContext('heapCorruptionExitCodeOnLinux', () async { expect(await device.isLocalEmulator, knownEmulator.contains(hardware));
exitCode = -1073740940; }
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{
'ANDROID_HOME': '/',
},
),
}); });
testUsingContext('noErrorOnLinux', () async { testWithoutContext('AndroidDevice can detect unknown hardware', () async {
exitCode = 0; final AndroidDevice device = setUpAndroidDevice(
final AndroidDevice device = AndroidDevice('test'); processManager: FakeProcessManager.list(<FakeCommand>[
expect(await device.isLocalEmulator, true); const FakeCommand(
}, overrides: <Type, Generator>{ command: <String>[
ProcessManager: () => mockProcessManager, 'adb', '-s', '1234', 'shell', 'getprop',
Platform: () => FakePlatform( ],
operatingSystem: 'linux', stdout: '[ro.hardware]: [unknown]\n'
environment: <String, String>{ '[ro.build.characteristics]: [att]'
'ANDROID_HOME': '/',
},
), ),
}); ])
}); );
group('ABI detection', () {
ProcessManager mockProcessManager;
String cpu;
String abilist;
setUp(() {
mockProcessManager = MockProcessManager();
cpu = 'unknown';
abilist = 'unknown';
when(mockProcessManager.run(
argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'),
)).thenAnswer((_) {
final StringBuffer buf = StringBuffer()
..writeln('[ro.product.cpu.abi]: [$cpu]')
..writeln('[ro.product.cpu.abilist]: [$abilist]');
final ProcessResult result = ProcessResult(1, 0, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
});
testUsingContext('detects x64', () async {
cpu = 'x86_64';
final AndroidDevice device = AndroidDevice('test');
expect(await device.targetPlatform, TargetPlatform.android_x64);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager
});
testUsingContext('detects x86', () async {
cpu = 'x86';
final AndroidDevice device = AndroidDevice('test');
expect(await device.targetPlatform, TargetPlatform.android_x86);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager
});
testUsingContext('unknown device defaults to 32bit arm', () async {
cpu = '???';
final AndroidDevice device = AndroidDevice('test');
expect(await device.targetPlatform, TargetPlatform.android_arm);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager
});
testUsingContext('detects 64 bit arm', () async {
cpu = 'arm64-v8a';
abilist = 'arm64-v8a,';
final AndroidDevice device = AndroidDevice('test');
// If both abi properties agree, we are 64 bit.
expect(await device.targetPlatform, TargetPlatform.android_arm64);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager
});
testUsingContext('detects kindle fire ABI', () async {
cpu = 'arm64-v8a';
abilist = 'arm';
final AndroidDevice device = AndroidDevice('test');
// If one does not contain arm64, assume 32 bit.
expect(await device.targetPlatform, TargetPlatform.android_arm);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager
});
});
group('isLocalEmulator', () {
final ProcessManager mockProcessManager = MockProcessManager();
String hardware;
String buildCharacteristics;
setUp(() {
hardware = 'unknown';
buildCharacteristics = 'unused';
when(mockProcessManager.run(
argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'),
)).thenAnswer((_) {
final StringBuffer buf = StringBuffer()
..writeln('[ro.hardware]: [$hardware]')
..writeln('[ro.build.characteristics]: [$buildCharacteristics]');
final ProcessResult result = ProcessResult(1, 0, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
});
testUsingContext('knownPhysical', () async {
hardware = 'samsungexynos7420';
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, false); expect(await device.isLocalEmulator, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('knownPhysical Samsung SM G570M', () async { testWithoutContext('AndroidDevice can detect unknown emulator', () async {
hardware = 'samsungexynos7570'; final AndroidDevice device = setUpAndroidDevice(
final AndroidDevice device = AndroidDevice('test'); processManager: FakeProcessManager.list(<FakeCommand>[
expect(await device.isLocalEmulator, false); const FakeCommand(
}, overrides: <Type, Generator>{ command: <String>[
ProcessManager: () => mockProcessManager, 'adb', '-s', '1234', 'shell', 'getprop',
}); ],
stdout: '[ro.hardware]: [unknown]\n'
testUsingContext('knownEmulator', () async { '[ro.build.characteristics]: [att,emulator]'
hardware = 'goldfish'; ),
final AndroidDevice device = AndroidDevice('test'); ])
expect(await device.isLocalEmulator, true); );
expect(await device.supportsHardwareRendering, true);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('unknownPhysical', () async {
buildCharacteristics = 'att';
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
testUsingContext('unknownEmulator', () async {
buildCharacteristics = 'att,emulator';
final AndroidDevice device = AndroidDevice('test');
expect(await device.isLocalEmulator, true); expect(await device.isLocalEmulator, true);
expect(await device.supportsHardwareRendering, true); expect(await device.supportsHardwareRendering, true);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
}); });
testUsingContext('isSupportedForProject is true on module project', () async { testWithoutContext('isSupportedForProject is true on module project', () async {
globals.fs.file('pubspec.yaml') final FileSystem fileSystem = MemoryFileSystem.test();
fileSystem.file('pubspec.yaml')
..createSync() ..createSync()
..writeAsStringSync(r''' ..writeAsStringSync(r'''
name: example name: example
...@@ -264,174 +175,188 @@ name: example ...@@ -264,174 +175,188 @@ name: example
flutter: flutter:
module: {} module: {}
'''); ''');
globals.fs.file('.packages').createSync(); fileSystem.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProject.current(); final FlutterProject flutterProject = FlutterProjectFactory(
fileSystem: fileSystem,
logger: BufferLogger.test(),
).fromDirectory(fileSystem.currentDirectory);
final AndroidDevice device = setUpAndroidDevice(fileSystem: fileSystem);
expect(AndroidDevice('test').isSupportedForProject(flutterProject), true); expect(device.isSupportedForProject(flutterProject), true);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('isSupportedForProject is true with editable host app', () async { testWithoutContext('isSupportedForProject is true with editable host app', () async {
globals.fs.file('pubspec.yaml').createSync(); final FileSystem fileSystem = MemoryFileSystem.test();
globals.fs.file('.packages').createSync(); fileSystem.file('pubspec.yaml').createSync();
globals.fs.directory('android').createSync(); fileSystem.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProject.current(); fileSystem.directory('android').createSync();
final FlutterProject flutterProject = FlutterProjectFactory(
fileSystem: fileSystem,
logger: BufferLogger.test(),
).fromDirectory(fileSystem.currentDirectory);
expect(AndroidDevice('test').isSupportedForProject(flutterProject), true); final AndroidDevice device = setUpAndroidDevice(fileSystem: fileSystem);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(), expect(device.isSupportedForProject(flutterProject), true);
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('isSupportedForProject is false with no host app and no module', () async { testWithoutContext('isSupportedForProject is false with no host app and no module', () async {
globals.fs.file('pubspec.yaml').createSync(); final FileSystem fileSystem = MemoryFileSystem.test();
globals.fs.file('.packages').createSync(); fileSystem.file('pubspec.yaml').createSync();
final FlutterProject flutterProject = FlutterProject.current(); fileSystem.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProjectFactory(
fileSystem: fileSystem,
logger: BufferLogger.test(),
).fromDirectory(fileSystem.currentDirectory);
expect(AndroidDevice('test').isSupportedForProject(flutterProject), false); final AndroidDevice device = setUpAndroidDevice(fileSystem: fileSystem);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
});
group('emulatorId', () { expect(device.isSupportedForProject(flutterProject), false);
final ProcessManager mockProcessManager = MockProcessManager();
const String dummyEmulatorId = 'dummyEmulatorId';
final Future<Socket> Function(String host, int port) unresponsiveSocket =
(String host, int port) async => MockUnresponsiveAndroidConsoleSocket();
final Future<Socket> Function(String host, int port) disconnectingSocket =
(String host, int port) async => MockDisconnectingAndroidConsoleSocket();
final Future<Socket> Function(String host, int port) workingSocket =
(String host, int port) async => MockWorkingAndroidConsoleSocket(dummyEmulatorId);
String hardware;
bool socketWasCreated;
setUp(() {
hardware = 'goldfish'; // Known emulator
socketWasCreated = false;
when(mockProcessManager.run(
argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'),
)).thenAnswer((_) {
final StringBuffer buf = StringBuffer()
..writeln('[ro.hardware]: [$hardware]');
final ProcessResult result = ProcessResult(1, 0, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
}); });
testUsingContext('returns correct ID for responsive emulator', () async { testWithoutContext('AndroidDevice returns correct ID for responsive emulator', () async {
final AndroidDevice device = AndroidDevice('emulator-5555'); final AndroidDevice device = setUpAndroidDevice(
expect(await device.emulatorId, equals(dummyEmulatorId)); processManager: FakeProcessManager.list(<FakeCommand>[
}, overrides: <Type, Generator>{ const FakeCommand(
AndroidConsoleSocketFactory: () => workingSocket, command: <String>['adb', '-s', 'emulator-5555', 'shell', 'getprop'],
ProcessManager: () => mockProcessManager, stdout: '[ro.hardware]: [goldfish]'
)
]),
id: 'emulator-5555',
androidConsoleSocketFactory: (String host, int port) async =>
MockWorkingAndroidConsoleSocket('dummyEmulatorId'),
);
expect(await device.emulatorId, equals('dummyEmulatorId'));
}); });
testUsingContext('does not create socket for non-emulator devices', () async { testWithoutContext('AndroidDevice does not create socket for non-emulator devices', () async {
hardware = 'samsungexynos7420'; bool socketWasCreated = false;
// Still use an emulator-looking ID so we can be sure the failure is due // Still use an emulator-looking ID so we can be sure the failure is due
// to the isLocalEmulator field and not because the ID doesn't contain a // to the isLocalEmulator field and not because the ID doesn't contain a
// port. // port.
final AndroidDevice device = AndroidDevice('emulator-5555'); final AndroidDevice device = setUpAndroidDevice(
expect(await device.emulatorId, isNull); id: 'emulator-5555',
expect(socketWasCreated, isFalse); processManager: FakeProcessManager.list(<FakeCommand>[
}, overrides: <Type, Generator>{ const FakeCommand(
AndroidConsoleSocketFactory: () => (String host, int port) async { command: <String>['adb', '-s', 'emulator-5555', 'shell', 'getprop'],
stdout: '[ro.hardware]: [samsungexynos7420]'
)
]),
androidConsoleSocketFactory: (String host, int port) async {
socketWasCreated = true; socketWasCreated = true;
throw 'Socket was created for non-emulator'; throw 'Socket was created for non-emulator';
}, }
ProcessManager: () => mockProcessManager, );
});
testUsingContext('does not create socket for emulators with no port', () async {
final AndroidDevice device = AndroidDevice('emulator-noport');
expect(await device.emulatorId, isNull); expect(await device.emulatorId, isNull);
expect(socketWasCreated, isFalse); expect(socketWasCreated, isFalse);
}, overrides: <Type, Generator>{ });
AndroidConsoleSocketFactory: () => (String host, int port) async {
testWithoutContext('AndroidDevice does not create socket for emulators with no port', () async {
bool socketWasCreated = false;
final AndroidDevice device = setUpAndroidDevice(
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
stdout: '[ro.hardware]: [goldfish]'
)
]),
androidConsoleSocketFactory: (String host, int port) async {
socketWasCreated = true; socketWasCreated = true;
throw 'Socket was created for emulator without port in ID'; throw 'Socket was created for emulator without port in ID';
}, },
ProcessManager: () => mockProcessManager, );
});
testUsingContext('returns null for connection error', () async {
final AndroidDevice device = AndroidDevice('emulator-5555');
expect(await device.emulatorId, isNull); expect(await device.emulatorId, isNull);
}, overrides: <Type, Generator>{ expect(socketWasCreated, isFalse);
AndroidConsoleSocketFactory: () {
return (String host, int port) => throw Exception('Fake socket error');
},
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('returns null for unresponsive device', () async { testWithoutContext('AndroidDevice.emulatorId is null for connection error', () async {
final AndroidDevice device = AndroidDevice('emulator-5555'); final AndroidDevice device = setUpAndroidDevice(
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
stdout: '[ro.hardware]: [goldfish]'
)
]),
androidConsoleSocketFactory: (String host, int port) => throw Exception('Fake socket error'),
);
expect(await device.emulatorId, isNull); expect(await device.emulatorId, isNull);
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => unresponsiveSocket,
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('returns null on early disconnect', () async { testWithoutContext('AndroidDevice.emulatorId is null for unresponsive device', () async {
final AndroidDevice device = AndroidDevice('emulator-5555'); final AndroidDevice device = setUpAndroidDevice(
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
stdout: '[ro.hardware]: [goldfish]'
)
]),
androidConsoleSocketFactory: (String host, int port) async =>
MockUnresponsiveAndroidConsoleSocket(),
);
expect(await device.emulatorId, isNull); expect(await device.emulatorId, isNull);
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => disconnectingSocket,
ProcessManager: () => mockProcessManager,
}); });
testWithoutContext('AndroidDevice.emulatorId is null on early disconnect', () async {
final AndroidDevice device = setUpAndroidDevice(
processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
stdout: '[ro.hardware]: [goldfish]'
)
]),
androidConsoleSocketFactory: (String host, int port) async =>
MockDisconnectingAndroidConsoleSocket()
);
expect(await device.emulatorId, isNull);
}); });
group('logcat', () { testWithoutContext('AndroidDevice lastLogcatTimestamp returns null if shell command failed', () async {
final ProcessManager mockProcessManager = MockProcessManager(); final AndroidDevice device = setUpAndroidDevice(
final AndroidDevice device = AndroidDevice('1234'); processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', '-x', 'logcat', '-v', 'time', '-t', '1'],
exitCode: 1,
)
])
);
testUsingContext('lastLogcatTimestamp returns null if shell command failed', () async {
when(mockProcessManager.runSync(argThat(contains('logcat'))))
.thenReturn(ProcessResult(0, 1, '', ''));
expect(device.lastLogcatTimestamp, isNull); expect(device.lastLogcatTimestamp, isNull);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('AdbLogReaders for past+future and future logs are not the same', () async { testWithoutContext('AndroidDevice AdbLogReaders for past+future and future logs are not the same', () async {
when(mockProcessManager.run( final AndroidDevice device = setUpAndroidDevice(
argThat(contains('getprop')), processManager: FakeProcessManager.list(<FakeCommand>[
stderrEncoding: anyNamed('stderrEncoding'), const FakeCommand(
stdoutEncoding: anyNamed('stdoutEncoding'), command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
)).thenAnswer((_) { stdout: '[ro.build.version.sdk]: [23]',
final StringBuffer buf = StringBuffer() exitCode: 1,
..writeln('[ro.build.version.sdk]: [23]'); ),
final ProcessResult result = ProcessResult(1, exitCode, buf.toString(), ''); const FakeCommand(
return Future<ProcessResult>.value(result); command: <String>['adb', '-s', '1234', 'logcat', '-v', 'time', '-s', 'flutter'],
}); ),
when(mockProcessManager.run( const FakeCommand(
argThat(contains('shell')), command: <String>['adb', '-s', '1234', 'logcat', '-v', 'time'],
stderrEncoding: anyNamed('stderrEncoding'), )
stdoutEncoding: anyNamed('stdoutEncoding'), ])
)).thenAnswer((_) { );
final StringBuffer buf = StringBuffer()
..writeln('11-27 15:39:04.506');
final ProcessResult result = ProcessResult(1, exitCode, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
final DeviceLogReader pastLogReader = await device.getLogReader(includePastLogs: true); final DeviceLogReader pastLogReader = await device.getLogReader(includePastLogs: true);
final DeviceLogReader defaultLogReader = await device.getLogReader(); final DeviceLogReader defaultLogReader = await device.getLogReader();
expect(pastLogReader, isNot(equals(defaultLogReader))); expect(pastLogReader, isNot(equals(defaultLogReader)));
// Getting again is cached. // Getting again is cached.
expect(pastLogReader, equals(await device.getLogReader(includePastLogs: true))); expect(pastLogReader, equals(await device.getLogReader(includePastLogs: true)));
expect(defaultLogReader, equals(await device.getLogReader())); expect(defaultLogReader, equals(await device.getLogReader()));
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
}); });
test('Can parse adb shell dumpsys info', () { testWithoutContext('Can parse adb shell dumpsys info', () {
const String exampleOutput = r''' const String exampleOutput = r'''
Applications Memory Usage (in Kilobytes): Applications Memory Usage (in Kilobytes):
Uptime: 441088659 Realtime: 521464097 Uptime: 441088659 Realtime: 521464097
...@@ -517,6 +442,28 @@ Uptime: 441088659 Realtime: 521464097 ...@@ -517,6 +442,28 @@ Uptime: 441088659 Realtime: 521464097
}); });
} }
AndroidDevice setUpAndroidDevice({
String id,
AndroidSdk androidSdk,
FileSystem fileSystem,
ProcessManager processManager,
Platform platform,
AndroidConsoleSocketFactory androidConsoleSocketFactory = kAndroidConsoleSocketFactory,
}) {
androidSdk ??= MockAndroidSdk();
when(androidSdk.adbPath).thenReturn('adb');
return AndroidDevice(id ?? '1234',
logger: BufferLogger.test(),
platform: platform ?? FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
processManager: processManager ?? FakeProcessManager.any(),
androidConsoleSocketFactory: androidConsoleSocketFactory,
timeoutConfiguration: const TimeoutConfiguration(),
);
}
class MockAndroidSdk extends Mock implements AndroidSdk {}
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
const String kAdbShellGetprop = ''' const String kAdbShellGetprop = '''
...@@ -737,10 +684,3 @@ class MockDisconnectingAndroidConsoleSocket extends Mock implements Socket { ...@@ -737,10 +684,3 @@ class MockDisconnectingAndroidConsoleSocket extends Mock implements Socket {
_controller.close(); _controller.close();
} }
} }
class AndroidPackageTest extends ApplicationPackage {
AndroidPackageTest() : super(id: 'app-id');
@override
String get name => 'app-package';
}
...@@ -11,7 +11,6 @@ import 'package:flutter_tools/src/android/android_sdk.dart' ...@@ -11,7 +11,6 @@ import 'package:flutter_tools/src/android/android_sdk.dart'
import 'package:flutter_tools/src/android/android_emulator.dart'; import 'package:flutter_tools/src/android/android_emulator.dart';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart'; import 'package:quiver/testing/async.dart';
...@@ -131,7 +130,7 @@ void main() { ...@@ -131,7 +130,7 @@ void main() {
testUsingContext('succeeds', () async { testUsingContext('succeeds', () async {
final AndroidEmulator emulator = AndroidEmulator(emulatorID); final AndroidEmulator emulator = AndroidEmulator(emulatorID);
expect(getEmulatorPath(globals.androidSdk), mockSdk.emulatorPath); expect(getEmulatorPath(mockSdk), mockSdk.emulatorPath);
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
FakeAsync().run((FakeAsync time) { FakeAsync().run((FakeAsync time) {
unawaited(emulator.launch().whenComplete(completer.complete)); unawaited(emulator.launch().whenComplete(completer.complete));
......
...@@ -7,7 +7,8 @@ import 'package:flutter_tools/src/android/android_device.dart'; ...@@ -7,7 +7,8 @@ import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import '../../src/common.dart'; import '../../src/common.dart';
...@@ -28,66 +29,50 @@ const FakeCommand kStoreShaCommand = FakeCommand( ...@@ -28,66 +29,50 @@ const FakeCommand kStoreShaCommand = FakeCommand(
); );
void main() { void main() {
testUsingContext('Cannot install app on API level below 16', () async { testWithoutContext('Cannot install app on API level below 16', () async {
final File apk = globals.fs.file('app.apk')..createSync(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
final AndroidApk androidApk = AndroidApk(
file: apk,
id: 'app',
versionCode: 22,
launchActivity: 'Main',
);
final AndroidDevice androidDevice = AndroidDevice('1234');
when(globals.androidSdk.adbPath).thenReturn('adb');
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
expect(await androidDevice.installApp(androidApk), false);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
kAdbVersionCommand, kAdbVersionCommand,
kAdbStartServerCommand, kAdbStartServerCommand,
const FakeCommand( const FakeCommand(
command: <String>['adb', '-s', '1234', 'shell', 'getprop'], command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
stdout: '[ro.build.version.sdk]: [11]', stdout: '[ro.build.version.sdk]: [11]',
), ),
]), ]);
FileSystem: () => MemoryFileSystem.test(), final FileSystem fileSystem = MemoryFileSystem.test();
AndroidSdk: () => MockAndroidSdk(), final File apk = fileSystem.file('app.apk')..createSync();
});
testUsingContext('Cannot install app if APK file is missing', () async {
final File apk = globals.fs.file('app.apk');
final AndroidApk androidApk = AndroidApk( final AndroidApk androidApk = AndroidApk(
file: apk, file: apk,
id: 'app', id: 'app',
versionCode: 22, versionCode: 22,
launchActivity: 'Main', launchActivity: 'Main',
); );
final AndroidDevice androidDevice = AndroidDevice('1234'); final AndroidDevice androidDevice = setUpAndroidDevice(
fileSystem: fileSystem,
processManager: processManager,
);
expect(await androidDevice.installApp(androidApk), false); expect(await androidDevice.installApp(androidApk), false);
}, overrides: <Type, Generator>{ expect(processManager.hasRemainingExpectations, false);
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
FileSystem: () => MemoryFileSystem.test(),
AndroidSdk: () => MockAndroidSdk(),
}); });
testUsingContext('Can install app on API level 16 or greater', () async { testWithoutContext('Cannot install app if APK file is missing', () async {
final File apk = globals.fs.file('app.apk')..createSync(); final FileSystem fileSystem = MemoryFileSystem.test();
final File apk = fileSystem.file('app.apk');
final AndroidApk androidApk = AndroidApk( final AndroidApk androidApk = AndroidApk(
file: apk, file: apk,
id: 'app', id: 'app',
versionCode: 22, versionCode: 22,
launchActivity: 'Main', launchActivity: 'Main',
); );
final AndroidDevice androidDevice = AndroidDevice('1234'); final AndroidDevice androidDevice = setUpAndroidDevice(
when(globals.androidSdk.adbPath).thenReturn('adb'); fileSystem: fileSystem,
final FakeProcessManager processManager = globals.processManager as FakeProcessManager; );
expect(await androidDevice.installApp(androidApk), true); expect(await androidDevice.installApp(androidApk), false);
expect(processManager.hasRemainingExpectations, false); });
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ testWithoutContext('Can install app on API level 16 or greater', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
kAdbVersionCommand, kAdbVersionCommand,
kAdbStartServerCommand, kAdbStartServerCommand,
const FakeCommand( const FakeCommand(
...@@ -96,27 +81,26 @@ void main() { ...@@ -96,27 +81,26 @@ void main() {
), ),
kInstallCommand, kInstallCommand,
kStoreShaCommand, kStoreShaCommand,
]), ]);
FileSystem: () => MemoryFileSystem.test(), final FileSystem fileSystem = MemoryFileSystem.test();
AndroidSdk: () => MockAndroidSdk(), final File apk = fileSystem.file('app.apk')..createSync();
});
testUsingContext('Defaults to API level 16 if adb returns a null response', () async {
final File apk = globals.fs.file('app.apk')..createSync();
final AndroidApk androidApk = AndroidApk( final AndroidApk androidApk = AndroidApk(
file: apk, file: apk,
id: 'app', id: 'app',
versionCode: 22, versionCode: 22,
launchActivity: 'Main', launchActivity: 'Main',
); );
final AndroidDevice androidDevice = AndroidDevice('1234'); final AndroidDevice androidDevice = setUpAndroidDevice(
when(globals.androidSdk.adbPath).thenReturn('adb'); fileSystem: fileSystem,
final FakeProcessManager processManager = globals.processManager as FakeProcessManager; processManager: processManager,
);
expect(await androidDevice.installApp(androidApk), true); expect(await androidDevice.installApp(androidApk), true);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ });
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
testWithoutContext('Defaults to API level 16 if adb returns a null response', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
kAdbVersionCommand, kAdbVersionCommand,
kAdbStartServerCommand, kAdbStartServerCommand,
const FakeCommand( const FakeCommand(
...@@ -124,10 +108,39 @@ void main() { ...@@ -124,10 +108,39 @@ void main() {
), ),
kInstallCommand, kInstallCommand,
kStoreShaCommand, kStoreShaCommand,
]), ]);
FileSystem: () => MemoryFileSystem.test(), final FileSystem fileSystem = MemoryFileSystem.test();
AndroidSdk: () => MockAndroidSdk(), final File apk = fileSystem.file('app.apk')..createSync();
final AndroidApk androidApk = AndroidApk(
file: apk,
id: 'app',
versionCode: 22,
launchActivity: 'Main',
);
final AndroidDevice androidDevice = setUpAndroidDevice(
fileSystem: fileSystem,
processManager: processManager,
);
expect(await androidDevice.installApp(androidApk), true);
expect(processManager.hasRemainingExpectations, false);
}); });
} }
AndroidDevice setUpAndroidDevice({
AndroidSdk androidSdk,
FileSystem fileSystem,
ProcessManager processManager,
}) {
androidSdk ??= MockAndroidSdk();
when(androidSdk.adbPath).thenReturn('adb');
return AndroidDevice('1234',
logger: BufferLogger.test(),
platform: FakePlatform(operatingSystem: 'linux'),
androidSdk: androidSdk,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
processManager: processManager ?? FakeProcessManager.any(),
);
}
class MockAndroidSdk extends Mock implements AndroidSdk {} class MockAndroidSdk extends Mock implements AndroidSdk {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment