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())); + } + }); +}