1 use crate::adevice::{Device, Profiler};
2 use crate::commands::{restart_type, split_string, AdbCommand};
3 use crate::progress;
4 use crate::restart_chooser::{RestartChooser, RestartType};
5 use crate::{fingerprint, time};
6
7 use anyhow::{anyhow, bail, Context, Result};
8 use itertools::Itertools;
9 use regex::Regex;
10 use serde::__private::ToString;
11 use std::cmp::Ordering;
12 use std::collections::{HashMap, HashSet};
13 use std::path::PathBuf;
14 use std::process;
15 use std::sync::LazyLock;
16 use std::thread::sleep;
17 use std::time::Duration;
18 use std::time::Instant;
19 use tracing::{debug, info};
20
21 pub struct RealDevice {
22 // If set, pass to all adb commands with --serial,
23 // otherwise let adb default to the only connected device or use ANDROID_SERIAL env variable.
24 android_serial: Option<String>,
25 }
26
27 impl Device for RealDevice {
28 /// Runs `adb` with the given args.
29 /// If there is a non-zero exit code or non-empty stderr, then
30 /// creates a Result Err string with the details.
run_adb_command(&self, cmd: &AdbCommand) -> Result<String>31 fn run_adb_command(&self, cmd: &AdbCommand) -> Result<String> {
32 self.run_raw_adb_command(&cmd.args())
33 }
34
reboot(&self) -> Result<String>35 fn reboot(&self) -> Result<String> {
36 self.run_raw_adb_command(&["reboot".to_string()])
37 }
38
soft_restart(&self) -> Result<String>39 fn soft_restart(&self) -> Result<String> {
40 self.run_raw_adb_command(&split_string("exec-out start"))
41 }
42
fingerprint( &self, partitions: &[String], ) -> Result<HashMap<PathBuf, fingerprint::FileMetadata>>43 fn fingerprint(
44 &self,
45 partitions: &[String],
46 ) -> Result<HashMap<PathBuf, fingerprint::FileMetadata>> {
47 self.fingerprint_device(partitions)
48 }
49
50 /// Get the apks that are installed (i.e. with `adb install`)
51 /// Count anything in the /data partition as installed.
get_installed_apks(&self) -> Result<HashSet<String>>52 fn get_installed_apks(&self) -> Result<HashSet<String>> {
53 // TODO(rbraunstein): See if there is a better way to do this that doesn't look for /data
54 let package_manager_output = self
55 .run_raw_adb_command(&split_string("exec-out pm list packages -s -f"))
56 .context("Running pm list packages")?;
57
58 let packages = apks_from_pm_list_output(&package_manager_output);
59 debug!("adb pm list packages found packages: {packages:?}");
60 Ok(packages)
61 }
62
63 /// Wait for the device to be ready to use.
64 /// First ask adb to wait for the device, then poll for sys.boot_completed on the device.
wait(&self, profiler: &mut Profiler) -> Result<String>65 fn wait(&self, profiler: &mut Profiler) -> Result<String> {
66 // Typically the reboot on acloud is 25 secs
67 // It can take 130 seconds after for a full boot.
68 // Setting timeouts to have at least 2x that.
69 progress::start(" * [1/2] Waiting for device to connect.");
70 time!(
71 {
72 let args = self.adjust_adb_args(&["wait-for-device".to_string()]);
73 self.wait_for_adb_with_timeout(&args, Duration::from_secs(75))?;
74 },
75 profiler.wait_for_device
76 );
77
78 progress::update(" * [2/2] Waiting for property sys.boot_completed.");
79 time!(
80 {
81 let args = self.adjust_adb_args(&[
82 "wait-for-device".to_string(),
83 "shell".to_string(),
84 "while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done".to_string(),
85 ]);
86 let result = self.wait_for_adb_with_timeout(&args, Duration::from_secs(260));
87 progress::stop();
88 result
89 },
90 profiler.wait_for_boot_completed
91 )
92 }
93
prep_after_flash(&self, profiler: &mut Profiler) -> Result<()>94 fn prep_after_flash(&self, profiler: &mut Profiler) -> Result<()> {
95 progress::start(" * [1/2] Remounting device");
96 let timeout = Duration::from_secs(60);
97
98 self.run_cmd_with_retry_until_timeout(
99 "adb",
100 &self.adjust_adb_args(&["root".to_string()]),
101 timeout,
102 )?;
103 // Remount and reboot; rebooting will return status code 255 so ignore error.
104 let _ = self.run_raw_adb_command(&["remount".to_string(), "-R".to_string()]);
105 progress::stop();
106 self.wait(profiler)?;
107 self.run_cmd_with_retry_until_timeout(
108 "adb",
109 &self.adjust_adb_args(&["root".to_string()]),
110 timeout,
111 )?;
112 Ok(())
113 }
114
115 /// Runs `adb` with the given args.
116 /// If there is a non-zero exit code or non-empty stderr, then
117 /// creates a Result Err string with the details.
run_raw_adb_command(&self, cmd: &[String]) -> Result<String>118 fn run_raw_adb_command(&self, cmd: &[String]) -> Result<String> {
119 let adjusted_args = self.adjust_adb_args(cmd);
120 info!(" -- adb {adjusted_args:?}");
121 let output = process::Command::new("adb")
122 .args(adjusted_args)
123 .output()
124 .context("Error running adb commands")?;
125
126 if output.status.success() {
127 let stdout = String::from_utf8(output.stdout)?;
128 return Ok(stdout);
129 }
130
131 // It is some error.
132 let status = match output.status.code() {
133 Some(code) => format!("Exited with status code: {code}"),
134 None => "Process terminated by signal".to_string(),
135 };
136
137 // Adb writes bad commands to stderr. (adb badverb) with status 1
138 // Adb writes remount output to stderr (adb remount) but gives status 0
139 let stderr = match String::from_utf8(output.stderr) {
140 Ok(str) => str,
141 Err(e) => return Err(anyhow!("Error translating stderr {}", e)),
142 };
143
144 // Adb writes push errors to stdout.
145 let stdout = match String::from_utf8(output.stdout) {
146 Ok(str) => str,
147 Err(e) => return Err(anyhow!("Error translating stdout {}", e)),
148 };
149
150 Err(anyhow!("adb error, {status} {stdout} {stderr}"))
151 }
152 }
153
154 // Sample output, one installed, one not:
155 // % adb exec-out pm list packages -s -f | grep shell
156 // package:/product/app/Browser2/Browser2.apk=org.chromium.webview_shell
157 // package:/data/app/~~PxHDtZDEgAeYwRyl-R3bmQ==/com.android.shell--R0z7ITsapIPKnt4BT0xkg==/base.apk=com.android.shell
158 // # capture the package name (com.android.shell)
159 static PM_LIST_PACKAGE_MATCHER: LazyLock<Regex> = LazyLock::new(|| {
160 Regex::new(r"^package:/data/app/.*/base.apk=(.+)$").expect("regex does not compile")
161 });
162
163 /// Filter package manager output to figure out if the apk is installed in /data.
apks_from_pm_list_output(stdout: &str) -> HashSet<String>164 fn apks_from_pm_list_output(stdout: &str) -> HashSet<String> {
165 let package_match = stdout
166 .lines()
167 .filter_map(|line| PM_LIST_PACKAGE_MATCHER.captures(line).map(|x| x[1].to_string()))
168 .collect();
169 package_match
170 }
171
172 impl RealDevice {
new(android_serial: Option<String>) -> RealDevice173 pub fn new(android_serial: Option<String>) -> RealDevice {
174 RealDevice { android_serial }
175 }
176
177 /// Add -s DEVICE to the adb args based on global options.
adjust_adb_args(&self, args: &[String]) -> Vec<String>178 fn adjust_adb_args(&self, args: &[String]) -> Vec<String> {
179 match &self.android_serial {
180 Some(serial) => [vec!["-s".to_string(), serial.clone()], args.to_vec()].concat(),
181 None => args.to_vec(),
182 }
183 }
184
185 /// Given "partitions" at the root of the device,
186 /// return an entry for each file found. The entry contains the
187 /// digest of the file contents and stat-like data about the file.
188 /// Typically, dirs = ["system"]
fingerprint_device( &self, partitions: &[String], ) -> Result<HashMap<PathBuf, fingerprint::FileMetadata>>189 fn fingerprint_device(
190 &self,
191 partitions: &[String],
192 ) -> Result<HashMap<PathBuf, fingerprint::FileMetadata>> {
193 // Ensure we are root or we can't read some files.
194 // In userdebug builds, every reboot reverts back to the "shell" user.
195 self.run_raw_adb_command(&["root".to_string()])?;
196 self.run_raw_adb_command(&["wait-for-device".to_string()])?;
197 let mut adb_args = vec![
198 "shell".to_string(),
199 "/system/bin/adevice_fingerprint".to_string(),
200 "-p".to_string(),
201 ];
202 // -p system,system_ext
203 adb_args.push(partitions.join(","));
204 let fingerprint_result = self.run_raw_adb_command(&adb_args);
205 // Deal with some bootstrapping errors, like adevice_fingerprint isn't installed
206 // by printing diagnostics and exiting.
207 if let Err(problem) = fingerprint_result {
208 if problem
209 .root_cause()
210 .to_string()
211 // TODO(rbraunstein): Will this work in other locales?
212 .contains("adevice_fingerprint: inaccessible or not found")
213 {
214 // Running as root, but adevice_fingerprint not found.
215 // This should not happen after we tag it as an "eng" module.
216 bail!("\n Thank you for testing out adevice.\n Flashing a recent image should install the needed `adevice_fingerprint` binary.\n Otherwise, you can bootstrap by doing the following:\n\t ` adb remount; m adevice_fingerprint adevice && adb push $ANDROID_PRODUCT_OUT/system/bin/adevice_fingerprint system/bin/adevice_fingerprint`");
217 } else {
218 // If pontis is running, add to the error message to check pontis UI
219 let pontis_status = process::Command::new("pontis")
220 .args(vec!["status".to_string()])
221 .output()
222 .context("Error checking pontis status")?;
223
224 let error_msg = format!("Unknown problem running `adevice_fingerprint` on your device: {problem:?}.\n Your device may still be in a booting state. Try `adb get-state` to start debugging.");
225 if pontis_status.status.success() {
226 let pontis_error_msg = "\n If you are using go/pontis, make sure the device appears in the Pontis browser UI and if not re-add it there.";
227 bail!("{}{}", error_msg, pontis_error_msg);
228 }
229 bail!("{}", error_msg);
230 }
231 }
232
233 let stdout = fingerprint_result.unwrap();
234
235 let result: HashMap<String, fingerprint::FileMetadata> = match serde_json::from_str(&stdout)
236 {
237 Err(err) if err.line() == 1 && err.column() == 0 && err.is_eof() => {
238 // This means there was no data. Print a different error, and adb
239 // probably also just printed a line.
240 bail!("Device didn't return any data.");
241 }
242 Err(err) => return Err(err).context("Error reading json"),
243 Ok(file_map) => file_map,
244 };
245 Ok(result.into_iter().map(|(path, metadata)| (PathBuf::from(path), metadata)).collect())
246 }
247
248 /// Run "adb wait-for-device" ... but exit if adb doesn't return
249 /// in the `timeout` amount of time.
wait_for_adb_with_timeout(&self, args: &[String], timeout: Duration) -> Result<String>250 pub fn wait_for_adb_with_timeout(&self, args: &[String], timeout: Duration) -> Result<String> {
251 self.run_cmd_with_retry_until_timeout("adb", args, timeout)
252 }
253
254 /// run command with retry until timeout duration is reached
run_cmd_with_retry_until_timeout( &self, cmd: &str, args: &[String], timeout: Duration, ) -> Result<String>255 pub fn run_cmd_with_retry_until_timeout(
256 &self,
257 cmd: &str,
258 args: &[String],
259 timeout: Duration,
260 ) -> Result<String> {
261 run_process_with_retry_until_timeout(cmd, args, timeout)
262 }
263 }
264
265 // Attempts to run a command until the command is either:
266 // 1) Successful
267 // 2) The amount of retries exceeds 5
268 // 3) The timeout (total across all retries) runs out.
269 // This is used for adb wait-for-device on acloud which may return
270 // errors the first few times.
271 // Using timeout binary to simplify (not having to kill process in rust)
272 // TODO(kevindagostino): fix for windows. Use the wait_timeout crate.
273
run_process_with_retry_until_timeout( cmd: &str, args: &[String], timeout: Duration, ) -> Result<String>274 pub fn run_process_with_retry_until_timeout(
275 cmd: &str,
276 args: &[String],
277 timeout: Duration,
278 ) -> Result<String> {
279 let start_time = Instant::now();
280 let delay = Duration::from_secs(1);
281 let max_retries = 5;
282 let mut retry_count = 0;
283
284 while retry_count < max_retries {
285 let time_left = timeout.saturating_sub(start_time.elapsed());
286 if time_left <= Duration::ZERO {
287 break;
288 }
289 retry_count += 1;
290
291 let mut timeout_args = vec![format!("{}", time_left.as_secs()), cmd.to_string()];
292 timeout_args.extend_from_slice(args);
293
294 info!(" -- timeout {}", &timeout_args.join(" "));
295 let output = std::process::Command::new("timeout")
296 .args(timeout_args)
297 .output()
298 .expect("command executed");
299 if output.status.success() {
300 let msg = String::from_utf8(output.stdout)?;
301 info!(" {} {}", output.status, msg);
302 return Ok(msg);
303 }
304
305 if retry_count > 1 {
306 let update_message = format!("retry attempt {} - {:?}", retry_count, cmd.to_string());
307 progress::update(&update_message)
308 }
309
310 // error; log and retry if within timeout window
311 info!(" {} {:?}", output.status, String::from_utf8(output.stderr).expect("stderr"));
312 sleep(delay);
313 }
314 bail!("Command failed to execute {}", cmd.to_string());
315 }
316
update( restart_chooser: &RestartChooser, adb_commands: &HashMap<PathBuf, AdbCommand>, profiler: &mut Profiler, device: &impl Device, should_wait: crate::cli::Wait, ) -> Result<()>317 pub fn update(
318 restart_chooser: &RestartChooser,
319 adb_commands: &HashMap<PathBuf, AdbCommand>,
320 profiler: &mut Profiler,
321 device: &impl Device,
322 should_wait: crate::cli::Wait,
323 ) -> Result<()> {
324 if adb_commands.is_empty() {
325 return Ok(());
326 }
327
328 let installed_files =
329 adb_commands.keys().map(|p| p.clone().into_os_string().into_string().unwrap()).collect();
330
331 progress::start("Preparing to update files");
332 prep_for_push(device, should_wait.clone())?;
333 let mut i = 1;
334 time!(
335 for command in adb_commands.values().cloned().sorted_by(&mkdir_comes_first_rm_dfs) {
336 let update_message =
337 format!("Updating files [{}/{}] {:?}", i, adb_commands.len(), command.args());
338 progress::update(&update_message);
339 device.run_adb_command(&command)?;
340 i += 1;
341 },
342 profiler.adb_cmds
343 );
344 progress::stop();
345 println!(" * Update succeeded!");
346 println!();
347
348 let rtype = restart_type(restart_chooser, &installed_files);
349 profiler.restart_type = format!("{:?}", rtype);
350 match rtype {
351 RestartType::Reboot => time!(device.reboot(), profiler.restart),
352 RestartType::SoftRestart => time!(device.soft_restart(), profiler.restart),
353 RestartType::None => {
354 tracing::debug!("No restart command");
355 return Ok(());
356 }
357 }?;
358
359 if should_wait.into() {
360 device.wait(profiler)?;
361 }
362 Ok(())
363 }
364
365 /// Common command to prepare a device to receive new files.
366 /// Always: `exec-out stop`
367 /// Always: `remount`
368 /// # A remount may not be needed but doesn't slow things down.
369 /// If `should_wait`: Set the system property sys.boot_completed to 0.
370 /// # A reboot would do this anyway, but it doesn't hurt if we do it too.
371 /// # We poll for that property to be set back to 1.
372 /// # Both reboot and exec-out start will set it back to 1 when the
373 /// # system has booted and is ready to receive commands and run tests.
prep_for_push(device: &impl Device, should_wait: crate::cli::Wait) -> Result<()>374 fn prep_for_push(device: &impl Device, should_wait: crate::cli::Wait) -> Result<()> {
375 device.run_raw_adb_command(&split_string("exec-out stop"))?;
376 // We seem to need a remount after reboots to make the system writable.
377 device.run_raw_adb_command(&split_string("remount"))?;
378 // Set the prop to the empty string so our "-z" check in wait works.
379 if should_wait.into() {
380 device.run_raw_adb_command(&[
381 "exec-out".to_string(),
382 "setprop".to_string(),
383 "sys.boot_completed".to_string(),
384 "".to_string(),
385 ])?;
386 }
387 Ok(())
388 }
389
390 // 1) Ensure mkdir comes before other commands.
391 // 2) Do removes as a depth-first-search so we clean children before parents.
392 // 3) Sort rm before other commands, but it shouldn't matter.
393 // 4) Remove files before dirs.
394 // We would never remove a file or directory we are pushing to.
mkdir_comes_first_rm_dfs(a: &AdbCommand, b: &AdbCommand) -> Ordering395 fn mkdir_comes_first_rm_dfs(a: &AdbCommand, b: &AdbCommand) -> Ordering {
396 // Neither is mkdir
397 if !a.is_mkdir() && !b.is_mkdir() {
398 // Sort rm's with files before their parents.
399 let a_cmd = a.args().join(" ");
400 let b_cmd = b.args().join(" ");
401
402 if a.is_rm() && b.is_rm() {
403 // This also sorts files before dirs because of the "-rf" added to dirs.
404 return b_cmd.cmp(&a_cmd);
405 }
406 if a.is_rm() {
407 return Ordering::Less;
408 }
409 if b.is_rm() {
410 return Ordering::Greater;
411 }
412
413 // Sort everything by the args.
414 return a_cmd.cmp(&b_cmd);
415 }
416 // If both mkdir:
417 // Just compare the path, parents will come before subdirs.
418 if a.is_mkdir() && b.is_mkdir() {
419 return a.device_path().cmp(b.device_path());
420 }
421 if a.is_mkdir() {
422 return Ordering::Less;
423 }
424 if b.is_mkdir() {
425 return Ordering::Greater;
426 }
427 Ordering::Equal
428 }
429
430 #[cfg(test)]
431 mod tests {
432 use super::*;
433 use crate::commands::AdbAction;
434 use anyhow::{bail, Result};
435 use core::cmp::Ordering;
436 use std::time::Duration;
437
438 // Igoring the tests so they don't cause delays in CI, but can still be run by hand.
439 #[ignore]
440 #[test]
timeout_returns_when_process_returns() -> Result<()>441 fn timeout_returns_when_process_returns() -> Result<()> {
442 let timeout = Duration::from_secs(5);
443 let sleep_args = &["3".to_string()];
444 let output = run_process_with_retry_until_timeout("sleep", sleep_args, timeout);
445 match output {
446 Ok(_) => Ok(()),
447 _ => bail!("Expected an ok status code"),
448 }
449 }
450
451 #[ignore]
452 #[test]
timeout_exits_when_timeout_hit() -> Result<()>453 fn timeout_exits_when_timeout_hit() -> Result<()> {
454 let timeout = Duration::from_secs(5);
455 let sleep_args = &["7".to_string()];
456 let start_time = Instant::now();
457 let output = run_process_with_retry_until_timeout("sleep", sleep_args, timeout);
458
459 // smoke test to make sure process ran longer then timeout.
460 let duration = start_time.elapsed();
461 assert!(
462 duration > timeout,
463 "Expected process to take longer then timeout. Elapsed: {:?}, Timeout: {:?}",
464 duration,
465 timeout
466 );
467
468 match output {
469 Ok(_) => bail!("Expected error status code"),
470 _ => Ok(()),
471 }
472 }
473
474 #[ignore]
475 #[test]
timeout_deals_with_process_errors() -> Result<()>476 fn timeout_deals_with_process_errors() -> Result<()> {
477 let timeout = Duration::from_secs(5);
478 let sleep_args = &["--bad-arg".to_string(), "7".to_string()];
479 // Add a bad arg so the process we run errs out.
480 let output = run_process_with_retry_until_timeout("sleep", sleep_args, timeout);
481 match output {
482 Ok(_) => bail!("Expected error status code"),
483 _ => Ok(()),
484 }
485 }
486
487 #[ignore]
488 #[test]
reboot_wait() -> Result<()>489 fn reboot_wait() -> Result<()> {
490 let timeout = Duration::from_secs(5);
491 let sleep_args = &["--bad-arg".to_string(), "7".to_string()];
492 // Add a bad arg so the process we run errs out.
493 let output = run_process_with_retry_until_timeout("sleep", sleep_args, timeout);
494 match output {
495 Ok(_) => bail!("Expected error status code"),
496 _ => Ok(()),
497 }
498 }
499
delete_file_cmd(file: &str) -> AdbCommand500 fn delete_file_cmd(file: &str) -> AdbCommand {
501 AdbCommand::from_action(AdbAction::DeleteFile, &PathBuf::from(file))
502 }
503
delete_dir_cmd(dir: &str) -> AdbCommand504 fn delete_dir_cmd(dir: &str) -> AdbCommand {
505 AdbCommand::from_action(AdbAction::DeleteDir, &PathBuf::from(dir))
506 }
507
508 #[test]
deeper_rms_come_first()509 fn deeper_rms_come_first() {
510 assert_eq!(
511 Ordering::Less,
512 mkdir_comes_first_rm_dfs(
513 &delete_file_cmd("dir1/dir2/file1"),
514 &delete_dir_cmd("dir1/dir2"),
515 )
516 );
517 assert_eq!(
518 Ordering::Greater,
519 mkdir_comes_first_rm_dfs(
520 &delete_dir_cmd("dir1/dir2"),
521 &delete_file_cmd("dir1/dir2/file1"),
522 )
523 );
524 assert_eq!(
525 Ordering::Less,
526 mkdir_comes_first_rm_dfs(
527 &delete_dir_cmd("dir1/dir2/dir3"),
528 &delete_dir_cmd("dir1/dir2"),
529 )
530 );
531 assert_eq!(
532 Ordering::Greater,
533 mkdir_comes_first_rm_dfs(
534 &delete_dir_cmd("dir1/dir2"),
535 &delete_dir_cmd("dir1/dir2/dir3"),
536 )
537 );
538 }
539 #[test]
rm_all_files_before_dirs()540 fn rm_all_files_before_dirs() {
541 assert_eq!(
542 Ordering::Less,
543 mkdir_comes_first_rm_dfs(
544 &delete_file_cmd("system/app/FakeOemFeatures/FakeOemFeatures.apk"),
545 &delete_dir_cmd("system/app/FakeOemFeatures"),
546 )
547 );
548 assert_eq!(
549 Ordering::Greater,
550 mkdir_comes_first_rm_dfs(
551 &delete_dir_cmd("system/app/FakeOemFeatures"),
552 &delete_file_cmd("system/app/FakeOemFeatures/FakeOemFeatures.apk"),
553 )
554 );
555 }
556
557 #[test]
sort_many()558 fn sort_many() {
559 let dir = |d| AdbCommand::from_action(AdbAction::DeleteDir, &PathBuf::from(d));
560 let file = |d| AdbCommand::from_action(AdbAction::DeleteFile, &PathBuf::from(d));
561 let mut adb_commands: Vec<AdbCommand> = vec![
562 file("system/STALE_FILE"),
563 dir("system/bin/dir1/STALE_DIR"),
564 file("system/bin/dir1/STALE_DIR/stalefile1"),
565 file("system/bin/dir1/STALE_DIR/stalefile2"),
566 ];
567
568 adb_commands.sort_by(&mkdir_comes_first_rm_dfs);
569 assert_eq!(
570 // Expected sorted order, deepest first.
571 // files before dirs.
572 vec![
573 file("system/bin/dir1/STALE_DIR/stalefile2"),
574 file("system/bin/dir1/STALE_DIR/stalefile1"),
575 file("system/STALE_FILE"),
576 dir("system/bin/dir1/STALE_DIR"),
577 ],
578 adb_commands
579 );
580 }
581
582 #[test]
583 // NOTE: This test assumes we have adb in our path.
adb_command_success()584 fn adb_command_success() {
585 // Use real device for device tests.
586 let result = RealDevice::new(None)
587 .run_raw_adb_command(&["version".to_string()])
588 .expect("Error running command");
589 assert!(
590 result.contains("Android Debug Bridge version"),
591 "Expected a version string, but received:\n {result}"
592 );
593 }
594
595 #[test]
adb_command_failure()596 fn adb_command_failure() {
597 let result = RealDevice::new(None).run_raw_adb_command(&["improper_cmd".to_string()]);
598 if result.is_ok() {
599 panic!("Did not expect to succeed");
600 }
601
602 let expected_str =
603 "adb error, Exited with status code: 1 adb: unknown command improper_cmd\n";
604 assert_eq!(expected_str, format!("{:?}", result.unwrap_err()));
605 }
606
607 #[test]
package_manager_output_parsing()608 fn package_manager_output_parsing() {
609 let actual_output = r#"
610 package:/product/app/Browser2/Browser2.apk=org.chromium.webview_shell
611 package:/apex/com.google.aosp_cf_phone.rros/overlay/cuttlefish_overlay_frameworks_base_core.apk=android.cuttlefish.overlay
612 package:/data/app/~~f_ZzeFPKma_EfXRklotqFg==/com.android.shell-hrjEOvqv3dAautKdfqeAEA==/base.apk=com.android.shell
613 package:/apex/com.google.aosp_cf_phone.rros/overlay/cuttlefish_phone_overlay_frameworks_base_core.apk=android.cuttlefish.phone.overlay
614 "#;
615 let mut expected: HashSet<String> = HashSet::new();
616 expected.insert("com.android.shell".to_string());
617 assert_eq!(expected, apks_from_pm_list_output(actual_output));
618 }
619 }
620