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