From 2b151b0ce65281941f1f8fe242c09d7e09547aa4 Mon Sep 17 00:00:00 2001 From: Henrik Skupin Date: Wed, 25 Sep 2019 23:01:56 +0000 Subject: [PATCH] Bug 1525126 - [mozbase] Add Rust `mozdevice` crate speaking ADB over TCP/IP. r=jgraham,webdriver-reviewers,nalexander This implementation speaks the ADB wire protocol over TCP/IP. This is in constrast to the Python implementation, which generally invokes adb on the command line. In thousands of runs across multiple devices, this implementation has proved surprisingly robust. Differential Revision: https://phabricator.services.mozilla.com/D44895 --HG-- extra : moz-landing-system : lando --- testing/mozbase/rust/mozdevice/Cargo.toml | 15 + testing/mozbase/rust/mozdevice/src/adb.rs | 34 ++ testing/mozbase/rust/mozdevice/src/lib.rs | 561 ++++++++++++++++++++ testing/mozbase/rust/mozdevice/src/shell.rs | 58 ++ testing/mozbase/rust/mozdevice/src/test.rs | 316 +++++++++++ 5 files changed, 984 insertions(+) create mode 100644 testing/mozbase/rust/mozdevice/Cargo.toml create mode 100644 testing/mozbase/rust/mozdevice/src/adb.rs create mode 100644 testing/mozbase/rust/mozdevice/src/lib.rs create mode 100644 testing/mozbase/rust/mozdevice/src/shell.rs create mode 100644 testing/mozbase/rust/mozdevice/src/test.rs diff --git a/testing/mozbase/rust/mozdevice/Cargo.toml b/testing/mozbase/rust/mozdevice/Cargo.toml new file mode 100644 index 000000000000..291f2e789043 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mozdevice" +version = "0.1.0" +authors = ["Nick Alexander ", "Henrik Skupin "] +description = "Client library for the Android Debug Bridge (adb)" +keywords = ["mozilla", "firefox", "geckoview", "android", "adb"] +repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/rust/mozdevice" +license = "MPL-2.0" + +[dependencies] +log = { version = "0.4", features = ["std"] } +regex = "1" +tempfile = "3" +walkdir = "2" + diff --git a/testing/mozbase/rust/mozdevice/src/adb.rs b/testing/mozbase/rust/mozdevice/src/adb.rs new file mode 100644 index 000000000000..37857254cec4 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/adb.rs @@ -0,0 +1,34 @@ +#[derive(Debug, PartialEq)] +pub enum SyncCommand { + Data, + Dent, + Done, + Fail, + List, + Okay, + Quit, + Recv, + Send, + Stat, +} + +impl SyncCommand { + // Returns the byte serialisation of the protocol status. + pub fn code(&self) -> &'static [u8; 4] { + use self::SyncCommand::*; + match *self { + Data => b"DATA", + Dent => b"DENT", + Done => b"DONE", + Fail => b"FAIL", + List => b"LIST", + Okay => b"OKAY", + Quit => b"QUIT", + Recv => b"RECV", + Send => b"SEND", + Stat => b"STAT", + } + } +} + +pub type DeviceSerial = String; diff --git a/testing/mozbase/rust/mozdevice/src/lib.rs b/testing/mozbase/rust/mozdevice/src/lib.rs new file mode 100644 index 000000000000..de500e59dc86 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/lib.rs @@ -0,0 +1,561 @@ +#[macro_use] +extern crate log; +extern crate regex; +extern crate tempfile; +extern crate walkdir; + +pub mod adb; +pub mod shell; + +#[cfg(test)] +pub mod test; + +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::fmt; +use std::fs::File; +use std::io::{self, Read, Write}; +use std::iter::FromIterator; +use std::net::TcpStream; +use std::num::{ParseIntError, TryFromIntError}; +use std::path::Path; +use std::str::Utf8Error; +use std::time::{Duration, SystemTime}; +use walkdir::{WalkDir}; + +use crate::adb::{DeviceSerial, SyncCommand}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum DeviceError { + Adb(String), + Io(io::Error), + FromInt(TryFromIntError), + MultipleDevices, + ParseInt(ParseIntError), + UnknownDevice(String), + Utf8(Utf8Error), + WalkDir(walkdir::Error), +} + +impl fmt::Display for DeviceError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + DeviceError::Adb(ref message) => message.fmt(f), + DeviceError::MultipleDevices => + write!(f, "Multiple Android devices online"), + DeviceError::UnknownDevice(ref serial) => + write!(f, "Unknown Android device with serial '{}'", serial), + _ => self.to_string().fmt(f) + } + + } +} + +impl From for DeviceError { + fn from(value: io::Error) -> DeviceError { + DeviceError::Io(value) + } +} + +impl From for DeviceError { + fn from(value: ParseIntError) -> DeviceError { + DeviceError::ParseInt(value) + } +} + +impl From for DeviceError { + fn from(value: TryFromIntError) -> DeviceError { + DeviceError::FromInt(value) + } +} + +impl From for DeviceError { + fn from(value: Utf8Error) -> DeviceError { + DeviceError::Utf8(value) + } +} + +impl From for DeviceError { + fn from(value: walkdir::Error) -> DeviceError { + DeviceError::WalkDir(value) + } +} + +fn encode_message(payload: &str) -> Result { + let hex_length = u16::try_from(payload.len()) + .map(|len| format!("{:0>4X}", len))?; + + Ok(format!("{}{}", hex_length, payload).to_owned()) +} + +fn parse_device_info(line: &str) -> Option { + // Turn "serial\tdevice key1:value1 key2:value2 ..." into a `DeviceInfo`. + let mut pairs = line.split_whitespace(); + let serial = pairs.next(); + let state = pairs.next(); + if let (Some(serial), Some("device")) = (serial, state) { + let info: BTreeMap = pairs + .filter_map(|pair| { + let mut kv = pair.split(':'); + if let (Some(k), Some(v), None) = (kv.next(), kv.next(), kv.next()) { + Some((k.to_owned(), v.to_owned())) + } else { + None + } + }) + .collect(); + + Some(DeviceInfo { + serial: serial.to_owned(), + info, + }) + } else { + None + } +} + +/// Reads the payload length of a host message from the stream. +fn read_length(stream: &mut R) -> Result { + let mut bytes: [u8; 4] = [0; 4]; + stream.read_exact(&mut bytes)?; + + let response = std::str::from_utf8(&bytes)?; + + Ok(usize::from_str_radix(&response, 16)?) +} + +/// Reads the payload length of a device message from the stream. +fn read_length_little_endian(reader: &mut dyn Read) -> Result { + let mut bytes: [u8; 4] = [0; 4]; + reader.read_exact(&mut bytes)?; + + let n: usize = (bytes[0] as usize) + + ((bytes[1] as usize) << 8) + + ((bytes[2] as usize) << 16) + + ((bytes[3] as usize) << 24); + + Ok(n) +} + +/// Writes the payload length of a device message to the stream. +fn write_length_little_endian(writer: &mut dyn Write, n: usize) -> Result { + let mut bytes = [0; 4]; + bytes[0] = (n & 0xFF) as u8; + bytes[1] = ((n >> 8) & 0xFF) as u8; + bytes[2] = ((n >> 16) & 0xFF) as u8; + bytes[3] = ((n >> 24) & 0xFF) as u8; + + writer.write(&bytes[..]) + .map_err(DeviceError::Io) +} + +fn read_response( + stream: &mut TcpStream, + has_output: bool, + has_length: bool, +) -> Result> { + let mut bytes: [u8; 1024] = [0; 1024]; + + stream.read_exact(&mut bytes[0..4])?; + + if &bytes[0..4] != SyncCommand::Okay.code() { + let n = bytes.len().min(read_length(stream)?); + stream.read_exact(&mut bytes[0..n])?; + + let message = std::str::from_utf8(&bytes[0..n]) + .map(|s| format!("adb error: {}", s))?; + + return Err(DeviceError::Adb(message)); + } + + let mut response = Vec::new(); + + if has_output { + stream.read_to_end(&mut response)?; + + if response.len() >= 4 && &response[0..4] == SyncCommand::Okay.code() { + // Sometimes the server produces OKAYOKAY. Sometimes there is a transport OKAY and + // then the underlying command OKAY. This is straight from `chromedriver`. + response = response.split_off(4); + } + + if response.len() >= 4 && &response[0..4] == SyncCommand::Fail.code() { + // The server may even produce OKAYFAIL, which means the underlying + // command failed. First split-off the `FAIL` and length of the message. + response = response.split_off(8); + + let message = std::str::from_utf8(&*response) + .map(|s| format!("adb error: {}", s))?; + + return Err(DeviceError::Adb(message)); + } + + if has_length { + if response.len() >= 4 { + let message = response.split_off(4); + let slice: &mut &[u8] = &mut &*response; + + let n = read_length(slice)?; + warn!( + "adb server response contained hexstring length {} and message length was {} \ + and message was {:?}", + n, + message.len(), + std::str::from_utf8(&message)? + ); + + return Ok(message); + } else { + return Err(DeviceError::Adb( + format!( + "adb server response did not contain expected hexstring length: {:?}", + std::str::from_utf8(&response)? + ), + )); + } + } + } + + Ok(response) +} + +/// Detailed information about an ADB device. +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct DeviceInfo { + pub serial: DeviceSerial, + pub info: BTreeMap, +} + +/// Represents a connection to an ADB host, which multiplexes the connections to +/// individual devices. +#[derive(Debug)] +pub struct Host { + /// The TCP host to connect to. Defaults to `"localhost"`. + pub host: Option, + /// The TCP port to connect to. Defaults to `5037`. + pub port: Option, + /// Optional TCP read timeout duration. Defaults to 2s. + pub read_timeout: Option, + /// Optional TCP write timeout duration. Defaults to 2s. + pub write_timeout: Option, +} + +impl Default for Host { + fn default() -> Host { + Host { + host: Some("localhost".to_string()), + port: Some(5037), + read_timeout: Some(Duration::from_secs(2)), + write_timeout: Some(Duration::from_secs(2)), + } + } +} + +impl Host { + /// Searches for available devices, and selects the one as specified by `device_serial`. + /// + /// If multiple devices are online, and no device has been specified, + /// the `ANDROID_SERIAL` environment variable can be used to select one. + pub fn device_or_default>(self, device_serial: Option<&T>) -> Result { + let serials: Vec = self.devices::>()? + .into_iter() + .map(|d| d.serial) + .collect(); + + if let Some(ref serial) = device_serial + .map(|v| v.as_ref().to_owned()) + .or_else(|| std::env::var("ANDROID_SERIAL").ok()) + { + if !serials.contains(serial) { + return Err(DeviceError::UnknownDevice(serial.clone())); + } + + return Ok(Device { host: self, serial: serial.to_owned() }); + } + + if serials.len() > 1 { + return Err(DeviceError::MultipleDevices); + } + + if let Some(ref serial) = serials.first() { + return Ok(Device { + host: self, + serial: serial.to_string(), + }); + } + + Err(DeviceError::Adb("No Android devices are online".to_owned())) + } + + pub fn connect(&self) -> Result { + let stream = TcpStream::connect(format!( + "{}:{}", + self.host.clone().unwrap_or_else(|| "localhost".to_owned()), + self.port.unwrap_or(5037) + ))?; + stream.set_read_timeout(self.read_timeout)?; + stream.set_write_timeout(self.write_timeout)?; + Ok(stream) + } + + pub fn execute_command( + &self, + command: &str, + has_output: bool, + has_length: bool, + ) -> Result { + let mut stream = self.connect()?; + + stream.write_all(encode_message(command)?.as_bytes())?; + let bytes = read_response(&mut stream, has_output, has_length)?; + // TODO: should we assert no bytes were read? + + let response = std::str::from_utf8(&bytes)?; + + Ok(response.to_owned()) + } + + pub fn execute_host_command( + &self, + host_command: &str, + has_length: bool, + has_output: bool, + ) -> Result { + self.execute_command(&format!("host:{}", host_command), has_output, has_length) + } + + pub fn features>(&self) -> Result { + let features = self.execute_host_command("features", true, true)?; + Ok(features.split(',').map(|x| x.to_owned()).collect()) + } + + pub fn devices>(&self) -> Result { + let response = self.execute_host_command("devices-l", true, true)?; + + let infos: B = response + .lines() + .filter_map(parse_device_info) + .collect(); + + Ok(infos) + } +} + +/// Represents an ADB device. +#[derive(Debug)] +pub struct Device { + /// ADB host that controls this device. + pub host: Host, + + /// Serial number uniquely identifying this ADB device. + pub serial: DeviceSerial, +} + +impl Device { + pub fn execute_host_command( + &self, + command: &str, + has_output: bool, + has_length: bool, + ) -> Result { + let mut stream = self.host.connect()?; + + let switch_command = format!("host:transport:{}", self.serial); + debug!("execute_host_command: >> {:?}", &switch_command); + stream.write_all(encode_message(&switch_command)?.as_bytes())?; + let _bytes = read_response(&mut stream, false, false)?; + debug!("execute_host_command: << {:?}", _bytes); + // TODO: should we assert no bytes were read? + + debug!("execute_host_command: >> {:?}", &command); + stream.write_all(encode_message(command)?.as_bytes())?; + let bytes = read_response(&mut stream, has_output, has_length)?; + + let response = std::str::from_utf8(&bytes)?; + debug!("execute_host_command: << {:?}", response); + + Ok(response.to_owned()) + } + + pub fn execute_host_shell_command(&self, shell_command: &str) -> Result { + let response = + self.execute_host_command(&format!("shell:{}", shell_command), true, false)?; + + Ok(response) + } + + pub fn is_app_installed(&self, package: &str) -> Result { + self.execute_host_shell_command(&format!("pm path {}", package)) + .map(|v| v.contains("package:")) + } + + pub fn clear_app_data(&self, package: &str) -> Result { + self.execute_host_shell_command(&format!("pm clear {}", package)) + .map(|v| v.contains("Success")) + } + + pub fn launch>( + &self, + package: &str, + activity: &str, + am_start_args: &[T], + ) -> Result { + let mut am_start = format!("am start -W -n {}/{}", package, activity); + + for arg in am_start_args { + am_start.push_str(" "); + am_start.push_str(&shell::escape(arg.as_ref())); + } + + self.execute_host_shell_command(&am_start) + .map(|v| v.contains("Complete")) + } + + pub fn force_stop(&self, package: &str) -> Result<()> { + debug!("Force stopping Android package: {}", package); + self.execute_host_shell_command(&format!("am force-stop {}", package)) + .and(Ok(())) + } + + pub fn forward_port(&self, local: u16, remote: u16) -> Result { + let command = format!("forward:tcp:{};tcp:{}", local, remote); + let response = self.host.execute_host_command(&command, true, false)?; + + if local == 0 { + Ok(response.parse::()?) + } else { + Ok(local) + } + } + + pub fn kill_forward_port(&self, local: u16) -> Result<()> { + let command = format!("killforward:tcp:{}", local); + self.host.execute_host_command(&command, true, false).and(Ok(())) + } + + pub fn kill_forward_all_ports(&self) -> Result<()> { + self.host.execute_host_command(&"killforward-all".to_owned(), false, false).and(Ok(())) + } + + pub fn reverse_port(&self, remote: u16, local: u16) -> Result { + let command = format!("reverse:forward:tcp:{};tcp:{}", remote, local); + let response = self.execute_host_command(&command, true, false)?; + + if remote == 0 { + Ok(response.parse::()?) + } else { + Ok(remote) + } + } + + pub fn kill_reverse_port(&self, remote: u16) -> Result<()> { + let command = format!("reverse:killforward:tcp:{}", remote); + self.execute_host_command(&command, true, true).and(Ok(())) + } + + pub fn kill_reverse_all_ports(&self) -> Result<()> { + let command = "reverse:killforward-all".to_owned(); + self.execute_host_command(&command, false, false).and(Ok(())) + } + + pub fn push(&self, buffer: &mut dyn Read, dest: &Path, mode: u32) -> Result<()> { + // Implement the ADB protocol to send a file to the device. + // The protocol consists of the following steps: + // * Send "host:transport" command with device serial + // * Send "sync:" command to initialize file transfer + // * Send "SEND" command with name and mode of the file + // * Send "DATA" command one or more times for the file content + // * Send "DONE" command to indicate end of file transfer + let mut stream = self.host.connect()?; + + let message = encode_message(&format!("host:transport:{}", self.serial))?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + let message = encode_message("sync:")?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + stream.write_all(SyncCommand::Send.code())?; + let args_ = format!("{},{}", dest.display(), mode); + let args = args_.as_bytes(); + write_length_little_endian(&mut stream, args.len())?; + stream.write_all(args)?; + + // Use a 32KB buffer to transfer the file contents + // TODO: Maybe adjust to maxdata (256KB) + let mut buf = [0; 32 * 1024]; + + loop { + let len = buffer.read(&mut buf)?; + + if len == 0 { + break; + } + + stream.write_all(SyncCommand::Data.code())?; + write_length_little_endian(&mut stream, len)?; + stream.write_all(&buf[0..len])?; + } + + // https://android.googlesource.com/platform/system/core/+/master/adb/SYNC.TXT#66 + // + // When the file is transferred a sync request "DONE" is sent, where length is set + // to the last modified time for the file. The server responds to this last + // request (but not to chunk requests) with an "OKAY" sync response (length can + // be ignored). + let time: u32 = ((SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)) + .unwrap() + .as_secs() + & 0xFFFF_FFFF) as u32; + + stream.write_all(SyncCommand::Done.code())?; + write_length_little_endian(&mut stream, time as usize)?; + + // Status. + stream.read_exact(&mut buf[0..4])?; + + if &buf[0..4] == SyncCommand::Okay.code() { + Ok(()) + } else if &buf[0..4] == SyncCommand::Fail.code() { + let n = buf.len().min(read_length_little_endian(&mut stream)?); + + stream.read_exact(&mut buf[0..n])?; + + let message = std::str::from_utf8(&buf[0..n]) + .map(|s| format!("adb error: {}", s)) + .unwrap_or_else(|_| "adb error was not utf-8".into()); + + Err(DeviceError::Adb(message)) + } else { + Err(DeviceError::Adb("FAIL (unknown)".to_owned())) + } + } + + pub fn push_dir(&self, source: &Path, dest_dir: &Path, mode: u32) -> Result<()> { + let walker = WalkDir::new(source) + .follow_links(false) + .into_iter(); + + for entry in walker { + let entry = entry?; + let path = entry.path(); + + if !entry.metadata()?.is_file() { + continue; + } + + let mut file = File::open(path)?; + + let mut dest = dest_dir.to_path_buf(); + dest.push(path.strip_prefix(source) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?); + + self.push(&mut file, &dest, mode)?; + } + + Ok(()) + } +} diff --git a/testing/mozbase/rust/mozdevice/src/shell.rs b/testing/mozbase/rust/mozdevice/src/shell.rs new file mode 100644 index 000000000000..ffec347b44f9 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/shell.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2017 Jimmy Cuadra +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +use regex::Regex; + +/// Escapes a string so it will be interpreted as a single word by the UNIX Bourne shell. +/// +/// If the input string is empty, this function returns an empty quoted string. +pub fn escape(input: &str) -> String { + // Stolen from https://docs.rs/shellwords/1.0.0/src/shellwords/lib.rs.html#24-37. + let escape_pattern: Regex = Regex::new(r"([^A-Za-z0-9_\-.,:/@\n])").unwrap(); + let line_feed: Regex = Regex::new(r"\n").unwrap(); + + if input.is_empty() { + return "''".to_owned(); + } + + let output = &escape_pattern.replace_all(input, "\\$1"); + + line_feed.replace_all(output, "'\n'").to_string() +} + +#[cfg(test)] +mod tests { + use super::escape; + + #[test] + fn empty_escape() { + assert_eq!(escape(""), "''"); + } + + #[test] + fn full_escape() { + assert_eq!(escape("foo '\"' bar"), "foo\\ \\'\\\"\\'\\ bar"); + } + + #[test] + fn escape_multibyte() { + assert_eq!(escape("あい"), "\\あ\\い"); + } +} \ No newline at end of file diff --git a/testing/mozbase/rust/mozdevice/src/test.rs b/testing/mozbase/rust/mozdevice/src/test.rs new file mode 100644 index 000000000000..dd25e7a489cc --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/test.rs @@ -0,0 +1,316 @@ +use crate::*; + +// Currently the API is not safe for multiple requests at the same time. It is +// recommended to run each of the unit tests on its own. Use the following +// command to accomplish that: +// +// $ cargo test -- --test-threads=1 + +use std::collections::BTreeSet; +use std::panic; +use tempfile::{tempdir, TempDir}; + +#[test] +fn read_length_from_valid_string() { + + fn test(message: &str) -> Result { + read_length(&mut io::BufReader::new(message.as_bytes())) + } + + assert_eq!(test("0000").unwrap(), 0); + assert_eq!(test("0001").unwrap(), 1); + assert_eq!(test("000F").unwrap(), 15); + assert_eq!(test("00FF").unwrap(), 255); + assert_eq!(test("0FFF").unwrap(), 4095); + assert_eq!(test("FFFF").unwrap(), 65535); + + assert_eq!(test("FFFF0").unwrap(), 65535); +} + +#[test] +fn read_length_from_invalid_string() { + + fn test(message: &str) -> Result { + read_length(&mut io::BufReader::new(message.as_bytes())) + } + + test("").expect_err("empty string"); + test("G").expect_err("invalid hex character"); + test("-1").expect_err("negative number"); + test("000").expect_err("shorter than 4 bytes"); +} + +#[test] +fn encode_message_with_valid_string() { + assert_eq!(encode_message("").unwrap(), "0000".to_string()); + assert_eq!(encode_message("a").unwrap(), "0001a".to_string()); + assert_eq!(encode_message(&"a".repeat(15)).unwrap(), format!("000F{}", "a".repeat(15))); + assert_eq!(encode_message(&"a".repeat(255)).unwrap(), format!("00FF{}", "a".repeat(255))); + assert_eq!( + encode_message(&"a".repeat(4095)).unwrap(), format!("0FFF{}", "a".repeat(4095)) + ); + assert_eq!( + encode_message(&"a".repeat(65535)).unwrap(), format!("FFFF{}", "a".repeat(65535)) + ); +} + +#[test] +fn encode_message_with_invalid_string() { + encode_message(&"a".repeat(65536)).expect_err("string lengths exceeds 4 bytes"); +} + +fn run_device_test(test: F) -> () + where F: FnOnce(&Device, &TempDir, &Path) -> () + panic::UnwindSafe +{ + fn clean_remote_dir(device: &Device, path: &Path) { + let command = format!("rm -r {}", path.display()); + let _ = device.execute_host_shell_command(&command); + } + + let host = Host { ..Default::default() }; + let device = host.device_or_default::(None) + .expect("device_or_default"); + + let tmp_dir = tempdir().expect("create temp dir"); + let remote_path = Path::new("/mnt/sdcard/mozdevice/"); + + clean_remote_dir(&device, remote_path); + + let result = panic::catch_unwind(|| { + test(&device, &tmp_dir, &remote_path) + }); + + let _ = device.kill_forward_all_ports(); + // let _ = device.kill_reverse_all_ports(); + + assert!(result.is_ok()) +} + +#[test] +fn host_features() { + let host = Host { ..Default::default() }; + + let mut set = BTreeSet::new(); + set.insert("cmd".to_owned()); + set.insert("shell_v2".to_owned()); + assert_eq!(set, host.features().expect("to query features")); +} + +#[test] +fn host_devices() { + let host = Host { ..Default::default() }; + + let set: BTreeSet<_> = host.devices().expect("to query devices"); + assert_eq!(1, set.len()); +} + +#[test] +fn host_device_or_default_valid_serial() { + let host = Host { ..Default::default() }; + + let devices: Vec<_> = host.devices().expect("to query devices"); + let expected_device = devices.first().expect("found a device"); + + let device = host.device_or_default::(Some(&expected_device.serial)) + .expect("connected device with serial"); + assert_eq!(device.serial, expected_device.serial); +} + +#[test] +fn host_device_or_default_invalid_serial() { + let host = Host { ..Default::default() }; + + host.device_or_default::(Some(&"foobar".to_owned())).expect_err("invalid serial"); +} + +#[test] +fn host_device_or_default_no_serial() { + let host = Host { ..Default::default() }; + + let devices: Vec<_> = host.devices().expect("to query devices"); + let expected_device = devices.first().expect("found a device"); + + let device = host.device_or_default::(None).expect("connected device with serial"); + assert_eq!(device.serial, expected_device.serial); +} + +#[test] +fn device_shell_command() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + assert_eq!( + "Linux\n", + device.execute_host_shell_command("uname").expect("to have shell output") + ); + }); +} + +#[test] +fn device_forward_port_hardcoded() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + assert_eq!(3035, device.forward_port(3035, 3036).expect("forwarded local port")); + // TODO: check with forward --list + }); +} + +// #[test] +// TODO: "adb server response to `forward tcp:0 ...` was not a u16: \"000559464\"") +fn device_forward_port_system_allocated() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + let local_port = device.forward_port(0, 3037).expect("local_port"); + assert_ne!(local_port, 0); + // TODO: check with forward --list + }); +} + +#[test] +fn device_kill_forward_port_no_forwarded_port() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + device.kill_forward_port(3038).expect_err("adb error: listener 'tcp:3038' "); + }); +} + +#[test] +fn device_kill_forward_port_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + let local_port = device.forward_port(3039, 3040).expect("forwarded local port"); + assert_eq!(local_port, 3039); + // TODO: check with forward --list + device.kill_forward_port(local_port).expect("to remove forwarded port"); + device.kill_forward_port(local_port).expect_err("adb error: listener 'tcp:3039' "); + }); +} + +#[test] +fn device_kill_forward_all_ports_no_forwarded_port() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + device.kill_forward_all_ports().expect("to not fail for no forwarded ports"); + }); +} + +#[test] +fn device_kill_forward_all_ports_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + let local_port1 = device.forward_port(3039, 3040).expect("forwarded local port"); + assert_eq!(local_port1, 3039); + let local_port2 = device.forward_port(3041, 3042).expect("forwarded local port"); + assert_eq!(local_port2, 3041); + // TODO: check with forward --list + device.kill_forward_all_ports().expect("to remove all forwarded ports"); + device.kill_forward_all_ports().expect("to not fail for no forwarded ports"); + }); +} + +#[test] +fn device_reverse_port_hardcoded() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + assert_eq!(4035, device.reverse_port(4035, 4036).expect("remote_port")); + // TODO: check with reverse --list + }); +} + +// #[test] +// TODO: No adb response: ParseInt(ParseIntError { kind: Empty }) +fn device_reverse_port_system_allocated() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + let reverse_port = device.reverse_port(0, 4037).expect("remote port"); + assert_ne!(reverse_port, 0); + // TODO: check with reverse --list + }); +} + +#[test] +fn device_kill_reverse_port_no_reverse_port() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + device.kill_reverse_port(4038).expect_err("listener 'tcp:4038' not found"); + }); +} + +// #[test] +// TODO: "adb error: adb server response did not contain expected hexstring length: \"\"" +fn device_kill_reverse_port_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + let remote_port = device.reverse_port(4039, 4040).expect("reversed local port"); + assert_eq!(remote_port, 4039); + // TODO: check with reverse --list + device.kill_reverse_port(remote_port).expect("to remove reverse port"); + device.kill_reverse_port(remote_port).expect_err("listener 'tcp:4039' not found"); + }); +} + +#[test] +fn device_kill_reverse_all_ports_no_reversed_port() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + device.kill_reverse_all_ports().expect("to not fail for no reversed ports"); + }); +} + +#[test] +fn device_kill_reverse_all_ports_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &Path| { + let local_port1 = device.forward_port(4039, 4040).expect("forwarded local port"); + assert_eq!(local_port1, 4039); + let local_port2 = device.forward_port(4041, 4042).expect("forwarded local port"); + assert_eq!(local_port2, 4041); + // TODO: check with reverse --list + device.kill_reverse_all_ports().expect("to remove all reversed ports"); + device.kill_reverse_all_ports().expect("to not fail for no reversed ports"); + }); +} + +#[test] +fn device_push() { + run_device_test(|device: &Device, _: &TempDir, remote_root_path: &Path| { + let content = "test"; + let remote_path = remote_root_path.join("foo.bar"); + + device.push(&mut io::BufReader::new(content.as_bytes()), &remote_path, 0o777) + .expect("file has been pushed"); + + let output = device + .execute_host_shell_command(&format!("ls {}", remote_path.display())) + .expect("host shell command for 'ls' to succeed"); + + assert!(output.contains(remote_path.to_str().unwrap())); + + let file_content = device + .execute_host_shell_command(&format!("cat {}", remote_path.display())) + .expect("host shell command for 'cat' to succeed"); + + assert_eq!(file_content, content); + }); +} + +#[test] +fn device_push_dir() { + run_device_test(|device: &Device, tmp_dir: &TempDir, remote_root_path: &Path| { + let content = "test"; + + let files = [ + Path::new("foo1.bar"), + Path::new("foo2.bar"), + Path::new("bar/foo3.bar"), + Path::new("bar/more/foo3.bar"), + ]; + + for file in files.iter() { + let path = tmp_dir.path().join(file); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + + let f = File::create(path).expect("to create file"); + let mut f = io::BufWriter::new(f); + f.write_all(content.as_bytes()).expect("to write data"); + } + + device.push_dir(tmp_dir.path(), &remote_root_path, 0o777) + .expect("to push_dir"); + + for file in files.iter() { + let path = remote_root_path.join(file); + let output = device + .execute_host_shell_command(&format!("ls {}", path.display())) + .expect("host shell command for 'ls' to succeed"); + + assert!(output.contains(path.to_str().unwrap())); + } + }); +}