Chromium Code Reviews| Index: tools/testing/dart/android.dart |
| diff --git a/tools/testing/dart/android.dart b/tools/testing/dart/android.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..c28cdfc77b29748cc65dc6a16a6a578193d4cdeb |
| --- /dev/null |
| +++ b/tools/testing/dart/android.dart |
| @@ -0,0 +1,323 @@ |
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +library android; |
| + |
| +import "dart:io"; |
| +import "dart:async"; |
| +import "dart:core"; |
| +import "dart:utf"; |
| + |
| +import "utils.dart"; |
| + |
| +Future _executeCommand(String executable, |
| + List<String> args, |
| + [String stdin = ""]) { |
| + return _executeCommandRaw(executable, args, stdin).then((results) => null); |
| +} |
| + |
| +Future _executeCommandGetOutput(String executable, |
| + List<String> args, |
| + [String stdin = ""]) { |
| + return _executeCommandRaw(executable, args, stdin) |
| + .then((results) => results[0]); |
| +} |
| + |
| +/** |
| + * [_executeCommandRaw] will write [stdin] to the standard input of the created |
| + * process and will return a tuple (stdout, stderr). |
| + * |
| + * If the exit code of the process was nonzero it will complete with an error. |
| + * If starting the process failed, it will complete with an error as well. |
| + */ |
| +Future _executeCommandRaw(String executable, |
| + List<String> args, |
| + [String stdin = ""]) { |
| + Future<String> getOutput(Stream<List<int>> stream) { |
| + return stream.transform(new StringDecoder()) |
| + .reduce(new StringBuffer(), (buf, data) { |
| + buf.write(data); |
|
ricow1
2013/05/21 18:30:43
indentation seems off here, hard to follow flow
kustermann
2013/05/22 09:30:58
Done.
|
| + return buf; |
| + }).then((buf) => buf.toString()); |
| + } |
| + |
| + DebugLogger.info("Running: '\$ $executable ${args.join(' ')}'"); |
| + return Process.start(executable, args).then((Process process) { |
| + if (stdin != null && stdin != '') { |
| + process.stdin.write(stdin); |
| + } |
| + process.stdin.close(); |
| + |
| + var futures = [getOutput(process.stdout), |
| + getOutput(process.stderr), |
| + process.exitCode]; |
| + return Future.wait(futures).then((results) { |
| + bool success = results[2] == 0; |
| + if (!success) { |
| + var error = """Running: '\$ $executable ${args.join(' ')}' failed: |
| + stdout: \n ${results[0]} |
|
ricow1
2013/05/21 18:30:43
this may look sort of strange due to the whitespac
kustermann
2013/05/22 09:30:58
Done.
|
| + stderr: \n ${results[1]} |
| + exitCode: \n ${results[2]} |
| + """; |
| + throw new Exception(error); |
| + } |
| + return [results[0], results[1]]; |
|
ricow1
2013/05/21 18:30:43
how about a ProcessResult like class for returning
kustermann
2013/05/22 09:30:58
I think there is no public ProcessResult construct
ricow1
2013/05/22 13:13:36
That is why I wrote ProcessResult _like_ class :-)
|
| + }); |
| + }); |
| +} |
| + |
| +/** |
| + * Helper class to loop through all adb ports. |
| + * |
| + * The ports come in pairs: |
| + * - even number: console connection |
| + * - odd number: adb connection |
| + * Note that this code doesn't check if the ports are used. |
| + */ |
| +class AdbServerPortPool { |
| + static int MIN_PORT = 5554; |
| + static int MAX_PORT = 5584; |
| + |
| + static int _nextPort = MIN_PORT; |
| + |
| + static int next() { |
| + var port = _nextPort; |
| + if (port > MAX_PORT) { |
| + throw new Exception("All ports are used."); |
| + } |
| + _nextPort += 2; |
| + return port; |
| + } |
| +} |
| + |
| +/** |
| + * Represents the interface to the emulator. |
| + * New emulators can be launched by calling the static [launchNewEmulator] |
| + * method. |
| + */ |
| +class AndroidEmulator { |
| + int _port; |
| + Process _emulatorProcess; |
| + AdbDevice _adbDevice; |
| + |
| + int get port => _port; |
| + |
| + AdbDevice get adbDevice => _adbDevice; |
| + |
| + static Future<AndroidEmulator> launchNewEmulator(String avdName) { |
| + var portNumber = AdbServerPortPool.next(); |
| + var args = ['-avd', '$avdName', '-port', "$portNumber" /*, '-gpu', 'on'*/]; |
| + return Process.start("emulator64-arm", args).then((Process process) { |
| + var adbDevice = new AdbDevice('emulator-$portNumber'); |
| + return new AndroidEmulator(portNumber, adbDevice, process); |
| + }); |
| + } |
| + |
| + AndroidEmulator(this._port, this._adbDevice, this._emulatorProcess) { |
|
ricow1
2013/05/21 18:30:43
since you are using private methods and instance v
kustermann
2013/05/22 09:30:58
Done.
|
| + Stream<String> getLines(Stream s) { |
| + return s.transform(new StringDecoder()).transform(new LineTransformer()); |
| + } |
| + |
| + getLines(_emulatorProcess.stdout).listen((line) { |
| + log("stdout: ${line.trim()}"); |
| + }); |
| + getLines(_emulatorProcess.stderr).listen((line) { |
| + log("stderr: ${line.trim()}"); |
| + }); |
| + _emulatorProcess.exitCode.then((exitCode) { |
| + log("emulator exited with exitCode: $exitCode."); |
| + }); |
| + } |
| + |
| + Future<bool> kill() { |
| + var completer = new Completer(); |
| + if (_emulatorProcess.kill()) { |
| + _emulatorProcess.exitCode.then((exitCode) { |
| + completer.complete(true); |
| + }); |
| + } else { |
| + log("Sending kill signal to emulator process failed"); |
| + completer.complete(false); |
| + } |
| + return completer.future; |
| + } |
| + |
| + void log(String msg) { |
| + DebugLogger.info("AndroidEmulator(${_adbDevice.deviceId}): $msg"); |
| + } |
| +} |
| + |
| +/** |
| + * Helper class to create avd device configurations. |
| + */ |
| +class AndroidHelper { |
| + static Future createAvd(String name, String target) { |
| + var args = ['--silent', 'create', 'avd', '--name', '$name', |
| + '--target', '$target', '--force', '--abi', 'armeabi-v7a']; |
| + return _executeCommand("android", args, "\n\n\n\n"); |
|
ricow1
2013/05/21 18:30:43
add a comment stating why we add 4 new lines to st
kustermann
2013/05/22 09:30:58
Done. I don't remember if we actually need 4 of th
|
| + } |
| +} |
| + |
| +/** |
| + * Used for communicating with an emulator or with a real device. |
| + */ |
| +class AdbDevice { |
| + static const _adbServerStartupTime = const Duration(seconds: 3); |
| + String _deviceId; |
| + |
| + String get deviceId => _deviceId; |
| + |
| + AdbDevice(this._deviceId); |
| + |
| + /** |
| + * Blocks execution until the device is online |
| + */ |
| + Future waitForDevice() { |
| + return _adbCommand(['wait-for-device']); |
| + } |
| + |
| + /** |
| + * Polls the 'sys.boot_completed' property. Returns as soon as the property is |
| + * 1. |
| + */ |
| + Future waitForBootCompleted() { |
| + var timeout = const Duration(seconds: 2); |
| + var completer = new Completer(); |
| + |
| + checkUntilBooted() { |
| + _adbCommandGetOutput(['shell', 'getprop', 'sys.boot_completed']) |
| + .then((String stdout) { |
| + stdout = stdout.trim(); |
| + if (stdout == '1') { |
| + completer.complete(); |
| + } else { |
| + new Timer(timeout, checkUntilBooted); |
| + } |
| + }).catchError((error) { |
| + new Timer(timeout, checkUntilBooted); |
| + }); |
| + } |
| + checkUntilBooted(); |
| + return completer.future; |
| + } |
| + |
| + /** |
| + * Put adb in root mode. |
| + */ |
| + Future adbRoot() { |
| + var adbRootCompleter = new Completer(); |
| + return _adbCommand(['root']).then((_) { |
| + new Timer(_adbServerStartupTime, () => adbRootCompleter.complete(true)); |
| + }).catchError((error) => adbRootCompleter.completeError(error)); |
| + return adbRootCompleter.future; |
| + } |
| + |
| + /** |
| + * Download data form the device. |
| + */ |
| + Future pullData(Path remote, Path local) { |
| + return _adbCommand(['pull', '$remote', '$local']); |
| + } |
| + |
| + /** |
| + * Upload data to the device. |
| + */ |
| + Future pushData(Path local, Path remote) { |
| + return _adbCommand(['push', '$local', '$remote']); |
| + } |
| + |
| + /** |
| + * Change permission of directory recursively. |
| + */ |
| + Future chmod(String mode, Path directory) { |
| + var arguments = ['shell', 'chmod', '-R', mode, '$directory']; |
| + return _adbCommand(arguments); |
| + } |
| + |
| + /** |
| + * Install an application on the device. |
| + */ |
| + Future installApk(Path filename) { |
| + return _adbCommand( |
| + ['install', '-i', 'com.google.android.feedback', '-r', '$filename']); |
| + } |
| + |
| + /** |
| + * Start the given intent on the device. |
| + */ |
| + Future startActivity(Intent intent) { |
| + var arguments = ['shell', 'am', 'start', '-W', |
| + '-a', intent.action, |
| + '-n', "${intent.package}/${intent.activity}"]; |
| + if (intent.dataUri != null) { |
| + arguments.addAll(['-d', intent.dataUri]); |
| + } |
| + return _adbCommand(arguments); |
| + } |
| + |
| + /** |
| + * Force to stop everything associated with [package]. |
| + */ |
| + Future forceStop(String package) { |
| + var arguments = ['shell', 'am', 'force-stop', package]; |
| + return _adbCommand(arguments); |
| + } |
| + |
| + /** |
| + * Kill all background processes. |
| + */ |
| + Future killAll() { |
| + var arguments = ['shell', 'am', 'kill-all']; |
| + return _adbCommand(arguments); |
| + } |
| + |
| + Future _adbCommand(List<String> adbArgs) { |
| + if (_deviceId != null) { |
| + var extendedAdbArgs = ['-s', _deviceId]; |
| + extendedAdbArgs.addAll(adbArgs); |
| + adbArgs = extendedAdbArgs; |
| + } |
| + return _executeCommand("adb", adbArgs); |
| + } |
| + |
| + Future<String> _adbCommandGetOutput(List<String> adbArgs) { |
| + if (_deviceId != null) { |
| + var extendedAdbArgs = ['-s', _deviceId]; |
| + extendedAdbArgs.addAll(adbArgs); |
| + adbArgs = extendedAdbArgs; |
| + } |
| + return _executeCommandGetOutput("adb", adbArgs); |
| + } |
| +} |
| + |
| +/** |
| + * Helper to list all adb devicess available. |
| + */ |
| +class AdbHelper { |
| + static RegExp _deviceLineRegexp = |
| + new RegExp(r'^([a-zA-Z0-9_-]+)[ \t]+device$', multiLine: true); |
| + |
| + static Future<List<String>> listDevices() { |
| + return Process.run('adb', ['devices']).then((ProcessResult result) { |
| + if (result.exitCode != 0) { |
| + throw new Exception("Could not list devices [stdout: ${result.stdout}," |
| + "stderr: ${result.stderr}]"); |
| + } |
| + return _deviceLineRegexp.allMatches(result.stdout).map((Match m) => m.group(1)).toList(); |
|
ricow1
2013/05/21 18:30:43
long line
kustermann
2013/05/22 09:30:58
Done.
|
| + }); |
| + } |
| +} |
| + |
| +/** |
| + * Represents an android intent. |
| + */ |
| +class Intent { |
| + String action; |
| + String package; |
| + String activity; |
| + String dataUri; |
| + |
| + Intent(this.action, this.package, this.activity, [this.dataUri]); |
| +} |
| + |