/* 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<usize> {
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<usize> {
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<F>(test: F)
where
F: FnOnce(&Device, &TempDir, &UnixPath) + panic::UnwindSafe,
{
let host = Host {
..Default::default()
};
let device = host
.device_or_default::<String>(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::<BTreeSet<_>>().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::<String>(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::<String>(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::<String>(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::<String>(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::<String>(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::<String>(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::<String>(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,
0 o777,
)
.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 ..100000 u32 {
content.push('0' as u8 + (i % 10 ) as u8);
}
device
.push(
&mut std::io::Cursor::new(content.clone()),
&remote_path,
0 o777,
)
.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 & 0 o777;
m |= (m >> 3 ) & 0 o070;
m |= (m >> 3 ) & 0 o007;
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![0 o421, 0 o644, 0 o666, 0 o777];
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, 0 o777)
.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: 0 b110110000,
size: 8
})
},
RemoteDirEntry {
depth: 0 ,
name: "foo2.bar" .to_string(),
metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
mode: 0 b110110000,
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: 0 b110110000,
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: 0 b110110000,
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, 0 o777)
.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, 0 o777)
.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: 0 b110110000,
size: 4
}),
name: "prefix/foo1.bar" .to_string(),
},
RemoteDirEntry {
depth: 7 ,
metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
mode: 0 b110110000,
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()
);
}
Messung V0.5 in Prozent C=89 H=95 G=91
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-20)
¤
*© Formatika GbR, Deutschland