/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Currently the mozdevice API is not safe for multiple requests at the same // time. It is recommended to run each of the unit tests on its own. Also adb // specific tests cannot be run in CI yet. To check those locally, also run // the ignored tests. // // Use the following command to accomplish that: // // $ cargo test -- --ignored --test-threads=1 use crate::*; use std::collections::BTreeSet; use std::panic; use std::path::PathBuf; 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, &UnixPath) + panic::UnwindSafe, { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Auto) .expect("device_or_default"); let tmp_dir = tempdir().expect("create temp dir"); let response = device .execute_host_shell_command("echo $EXTERNAL_STORAGE") .unwrap(); let mut test_root = UnixPathBuf::from(response.trim_end_matches('\n')); test_root.push("mozdevice"); let _ = device.remove(&test_root); let result = panic::catch_unwind(|| test(&device, &tmp_dir, &test_root)); let _ = device.kill_forward_all_ports(); // let _ = device.kill_reverse_all_ports(); assert!(result.is_ok()) } #[test] #[ignore] fn host_features() { let host = Host { ..Default::default() }; let set = host.features::>().expect("to query features"); assert!(set.contains("cmd")); assert!(set.contains("shell_v2")); } #[test] #[ignore] fn host_devices() { let host = Host { ..Default::default() }; let set: BTreeSet<_> = host.devices().expect("to query devices"); assert_eq!(1, set.len()); } #[test] #[ignore] fn host_device_or_default() { 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), AndroidStorageInput::App) .expect("connected device with serial"); assert_eq!(device.run_as_package, None); assert_eq!(device.serial, expected_device.serial); assert!(device.tempfile.starts_with("/data/local/tmp")); } #[test] #[ignore] fn host_device_or_default_invalid_serial() { let host = Host { ..Default::default() }; host.device_or_default::(Some(&"foobar".to_owned()), AndroidStorageInput::Auto) .expect_err("invalid serial"); } #[test] #[ignore] 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, AndroidStorageInput::Auto) .expect("connected device with serial"); assert_eq!(device.serial, expected_device.serial); } #[test] #[ignore] fn host_device_or_default_storage_as_app() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::App) .expect("connected device"); assert_eq!(device.storage, AndroidStorage::App); } #[test] #[ignore] fn host_device_or_default_storage_as_auto() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Auto) .expect("connected device"); assert_eq!(device.storage, AndroidStorage::Sdcard); } #[test] #[ignore] fn host_device_or_default_storage_as_internal() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Internal) .expect("connected device"); assert_eq!(device.storage, AndroidStorage::Internal); } #[test] #[ignore] fn host_device_or_default_storage_as_sdcard() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Sdcard) .expect("connected device"); assert_eq!(device.storage, AndroidStorage::Sdcard); } #[test] #[ignore] fn device_shell_command() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { assert_eq!( "Linux\n", device .execute_host_shell_command("uname") .expect("to have shell output") ); }); } #[test] #[ignore] fn device_forward_port_hardcoded() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { assert_eq!( 3035, device .forward_port(3035, 3036) .expect("forwarded local port") ); // TODO: check with forward --list }); } // #[test] // #[ignore] // 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, _: &UnixPath| { // let local_port = device.forward_port(0, 3037).expect("local_port"); // assert_ne!(local_port, 0); // // TODO: check with forward --list // }); // } #[test] #[ignore] fn device_kill_forward_port_no_forwarded_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { device .kill_forward_port(3038) .expect_err("adb error: listener 'tcp:3038' "); }); } #[test] #[ignore] fn device_kill_forward_port_twice() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { 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] #[ignore] fn device_kill_forward_all_ports_no_forwarded_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { device .kill_forward_all_ports() .expect("to not fail for no forwarded ports"); }); } #[test] #[ignore] fn device_kill_forward_all_ports_twice() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { 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] #[ignore] fn device_reverse_port_hardcoded() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { assert_eq!(4035, device.reverse_port(4035, 4036).expect("remote_port")); // TODO: check with reverse --list }); } // #[test] // #[ignore] // TODO: No adb response: ParseInt(ParseIntError { kind: Empty }) // fn device_reverse_port_system_allocated() { // run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { // let reverse_port = device.reverse_port(0, 4037).expect("remote port"); // assert_ne!(reverse_port, 0); // // TODO: check with reverse --list // }); // } #[test] #[ignore] fn device_kill_reverse_port_no_reverse_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { device .kill_reverse_port(4038) .expect_err("listener 'tcp:4038' not found"); }); } // #[test] // #[ignore] // TODO: "adb error: adb server response did not contain expected hexstring length: \"\"" // fn device_kill_reverse_port_twice() { // run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { // 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] #[ignore] fn device_kill_reverse_all_ports_no_reversed_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { device .kill_reverse_all_ports() .expect("to not fail for no reversed ports"); }); } #[test] #[ignore] fn device_kill_reverse_all_ports_twice() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { 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] #[ignore] fn device_push_pull_text_file() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { let content = "test"; let remote_path = remote_root_path.join("foo.txt"); device .push( &mut io::BufReader::new(content.as_bytes()), &remote_path, 0o777, ) .expect("file has been pushed"); 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); // And as second step pull it off the device. let mut buffer = Vec::new(); device .pull(&remote_path, &mut buffer) .expect("file has been pulled"); assert_eq!(buffer, content.as_bytes()); }, ); } #[test] #[ignore] fn device_push_pull_large_binary_file() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { let remote_path = remote_root_path.join("foo.binary"); let mut content = Vec::new(); // Needs to be larger than 64kB to test multiple chunks. for i in 0..100000u32 { content.push('0' as u8 + (i % 10) as u8); } device .push( &mut std::io::Cursor::new(content.clone()), &remote_path, 0o777, ) .expect("large file has been pushed"); let output = device .execute_host_shell_command(&format!("ls -l {}", remote_path.display())) .expect("host shell command for 'ls' to succeed"); assert!(output.contains(remote_path.to_str().unwrap())); let mut buffer = Vec::new(); device .pull(&remote_path, &mut buffer) .expect("large binary file has been pulled"); assert_eq!(buffer, content); }, ); } #[test] #[ignore] fn device_push_permission() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { fn adjust_mode(mode: u32) -> u32 { // Adjust the mode by copying the user permissions to // group and other as indicated in // [send_impl](https://android.googlesource.com/platform/system/core/+/master/adb/daemon/file_sync_service.cpp#516). // This ensures that group and other can both access a // file if the user can access it. let mut m = mode & 0o777; m |= (m >> 3) & 0o070; m |= (m >> 3) & 0o007; m } fn get_permissions(mode: u32) -> String { // Convert the mode integer into the string representation // of the mode returned by `ls`. This assumes the object is // a file and not a directory. let mut perms = vec!["-", "r", "w", "x", "r", "w", "x", "r", "w", "x"]; let mut bit_pos = 0; while bit_pos < 9 { if (1 << bit_pos) & mode == 0 { perms[9 - bit_pos] = "-" } bit_pos += 1; } perms.concat() } let content = "test"; let remote_path = remote_root_path.join("foo.bar"); // First push the file to the device let modes = vec![0o421, 0o644, 0o666, 0o777]; for mode in modes { let adjusted_mode = adjust_mode(mode); let adjusted_perms = get_permissions(adjusted_mode); device .push( &mut io::BufReader::new(content.as_bytes()), &remote_path, mode, ) .expect("file has been pushed"); let output = device .execute_host_shell_command(&format!("ls -l {}", remote_path.display())) .expect("host shell command for 'ls' to succeed"); assert!(output.contains(remote_path.to_str().unwrap())); assert!(output.starts_with(&adjusted_perms)); } let output = device .execute_host_shell_command(&format!("ls -ld {}", remote_root_path.display())) .expect("host shell command for 'ls parent' to succeed"); assert!(output.contains(remote_root_path.to_str().unwrap())); assert!(output.starts_with("drwxrwxrwx")); }, ); } #[test] #[ignore] fn device_pull_fails_for_missing_file() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { let mut buffer = Vec::new(); device .pull(&remote_root_path.join("missing"), &mut buffer) .expect_err("missing file should not be pulled"); }, ); } #[test] #[ignore] fn device_push_and_list_dir() { run_device_test( |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"]; for file in files.iter() { let path = tmp_dir.path().join(Path::new(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(file.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 = append_components(remote_root_path, Path::new(file)).unwrap(); 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())); } let mut listings = device.list_dir(&remote_root_path).expect("to list_dir"); listings.sort(); assert_eq!( listings, vec![ RemoteDirEntry { depth: 0, name: "foo1.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 8 }) }, RemoteDirEntry { depth: 0, name: "foo2.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 8 }) }, RemoteDirEntry { depth: 0, name: "bar".to_string(), metadata: RemoteMetadata::RemoteDir }, RemoteDirEntry { depth: 1, name: "bar/foo3.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 12 }) }, RemoteDirEntry { depth: 1, name: "bar/more".to_string(), metadata: RemoteMetadata::RemoteDir }, RemoteDirEntry { depth: 2, name: "bar/more/foo3.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 17 }) } ] ); }, ); } #[test] #[ignore] fn device_push_and_pull_dir() { run_device_test( |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"]; let src_dir = tmp_dir.path().join(Path::new("src")); let dest_dir = tmp_dir.path().join(Path::new("src")); for file in files.iter() { let path = src_dir.join(Path::new(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(file.as_bytes()).expect("to write data"); } device .push_dir(&src_dir, &remote_root_path, 0o777) .expect("to push_dir"); device .pull_dir(remote_root_path, &dest_dir) .expect("to pull_dir"); for file in files.iter() { let path = dest_dir.join(Path::new(file)); let mut f = File::open(path).expect("to open file"); let mut buf = String::new(); f.read_to_string(&mut buf).expect("to read content"); assert_eq!(buf, *file); } }, ) } #[test] #[ignore] fn device_push_and_list_dir_flat() { run_device_test( |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { let content = "test"; let files = [ PathBuf::from("foo1.bar"), PathBuf::from("foo2.bar"), PathBuf::from("bar").join("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 = append_components(remote_root_path, file).unwrap(); 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())); } let mut listings = device .list_dir_flat(&remote_root_path, 7, "prefix".to_string()) .expect("to list_dir_flat"); listings.sort(); assert_eq!( listings, vec![ RemoteDirEntry { depth: 7, metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 4 }), name: "prefix/foo1.bar".to_string(), }, RemoteDirEntry { depth: 7, metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 4 }), name: "prefix/foo2.bar".to_string(), }, RemoteDirEntry { depth: 7, metadata: RemoteMetadata::RemoteDir, name: "prefix/bar".to_string(), }, ] ); }, ); } #[test] fn format_own_device_error_types() { assert_eq!( format!("{}", DeviceError::InvalidStorage), "Invalid storage".to_string() ); assert_eq!( format!("{}", DeviceError::MissingPackage), "Missing package".to_string() ); assert_eq!( format!("{}", DeviceError::MultipleDevices), "Multiple Android devices online".to_string() ); assert_eq!( format!("{}", DeviceError::Adb("foo".to_string())), "foo".to_string() ); }