use crate::cli; use crate::commands; use crate::device; use crate::fingerprint; use crate::metrics; use crate::progress; use crate::restart_chooser; use crate::tracking::Config; use anyhow::{anyhow, bail, Context, Result}; use fingerprint::{DiffMode, FileMetadata}; use itertools::Itertools; use metrics::MetricSender; use rayon::prelude::*; use regex::Regex; use restart_chooser::RestartChooser; use tracing::{debug, Level}; use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::fs; use std::fs::File; use std::io::{stdin, Write}; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; use std::time::Duration; /// Methods that interact with the host, like fingerprinting and calling ninja to get deps. pub trait Host { /// Return all files in the given partitions at the partition_root along with metadata for those files. /// The keys in the returned hashmap will be relative to partition_root. fn fingerprint( &self, partition_root: &Path, partitions: &[PathBuf], ) -> Result>; /// Return a list of all files that compose `droid` or whatever base and tracked /// modules are listed in `config`. /// Result strings are device relative. (i.e. start with system) fn tracked_files(&self, config: &Config) -> Result>; } /// Methods to interact with the device, like adb, rebooting, and fingerprinting. pub trait Device { /// Run the `commands` and return the stdout as a string. If there is non-zero return code /// or output on stderr, then the result is an Err. fn run_adb_command(&self, args: &commands::AdbCommand) -> Result; fn run_raw_adb_command(&self, args: &[String]) -> Result; /// Send commands to reboot device. fn reboot(&self) -> Result; /// Send commands to do a soft restart. fn soft_restart(&self) -> Result; /// Call the fingerprint program on the device. fn fingerprint(&self, partitions: &[String]) -> Result>; /// Return the list apks that are currently installed, i.e. `adb install` /// which live on the /data partition. /// Returns the package name, i.e. "com.android.shell". fn get_installed_apks(&self) -> Result>; /// Wait for the device to be ready after reboots/restarts. /// Returns any relevant output from waiting. fn wait(&self, profiler: &mut Profiler) -> Result; /// Run the commands needed to prep a userdebug device after a flash. fn prep_after_flash(&self, profiler: &mut Profiler) -> Result<()>; } pub struct RealHost {} impl Default for RealHost { fn default() -> Self { Self::new() } } impl RealHost { pub fn new() -> RealHost { RealHost {} } } impl Host for RealHost { fn fingerprint( &self, partition_root: &Path, partitions: &[PathBuf], ) -> Result> { fingerprint::fingerprint_partitions(partition_root, partitions) } fn tracked_files(&self, config: &Config) -> Result> { config.tracked_files() } } /// Time how long it takes to run the function and store the /// result in the given profiler field. // TODO(rbraunstein): Ideally, use tracing or flamegraph crate or // use Map rather than name all the fields. // See: https://docs.rs/tracing/latest/tracing/index.html#using-the-macros and span! #[macro_export] macro_rules! time { ($fn:expr, $ident:expr) => {{ let start = std::time::Instant::now(); let result = $fn; $ident = start.elapsed(); result }}; } pub fn adevice( host: &impl Host, device: &impl Device, cli: &cli::Cli, stdout: &mut impl Write, metrics: &mut impl MetricSender, opt_log_file: Option, profiler: &mut Profiler, ) -> Result<()> { // If we can initialize a log file, then setup the tracing/log subscriber to write there. // Otherwise, logs will be dropped. if let Some(log_file) = opt_log_file { let subscriber = tracing_subscriber::fmt() .with_max_level(Level::DEBUG) .with_writer(Mutex::new(log_file)) .finish(); tracing::subscriber::set_global_default(subscriber)?; } let restart_choice = cli.global_options.restart_choice.clone(); let product_out = match &cli.global_options.product_out { Some(po) => PathBuf::from(po), None => get_product_out_from_env().ok_or(anyhow!( "ANDROID_PRODUCT_OUT is not set. Please run source build/envsetup.sh and lunch." ))?, }; let track_time = std::time::Instant::now(); let mut config = Config::load(&cli.global_options.config_path)?; let command_line = std::env::args().collect::>().join(" "); metrics.add_start_event(&command_line, &config.src_root()?); // Early return for track/untrack commands. match &cli.command { cli::Commands::Track(names) => return config.track(&names.modules), cli::Commands::TrackBase(base) => return config.trackbase(&base.base), cli::Commands::Untrack(names) => return config.untrack(&names.modules), _ => (), } config.print(); writeln!(stdout, " * Checking for files to push to device")?; progress::start("Checking ninja installed files"); let mut ninja_installed_files = time!(host.tracked_files(&config)?, profiler.ninja_deps_computer); let partitions = &validate_partitions(&product_out, &ninja_installed_files, &cli.global_options.partitions)?; // Filter to paths on any partitions. ninja_installed_files .retain(|nif| partitions.iter().any(|p| PathBuf::from(nif).starts_with(p))); debug!("Stale file tracking took {} millis", track_time.elapsed().as_millis()); progress::update("Checking files on device"); let mut device_tree: HashMap = time!(device.fingerprint(partitions)?, profiler.device_fingerprint); // We expect the device to create lost+found dirs when mounting // new partitions. Filter them out as if they don't exist. // However, if there are file inside of them, don't filter the // inner files. for p in partitions { device_tree.remove(&PathBuf::from(p).join("lost+found")); } progress::update("Checking files on host"); let partition_paths: Vec = partitions.iter().map(PathBuf::from).collect(); let host_tree = time!(host.fingerprint(&product_out, &partition_paths)?, profiler.host_fingerprint); progress::update("Calculating diffs"); // For now ignore diffs in permissions. This will allow us to have a new adevice host tool // still working with an older adevice_fingerprint device tool. // [It also works on windows hosts] // Version 0.2 of the device tool will support permission mode. // We can check for that version of the tool or check to see if the metadata // on a well-known file (like system/bin/adevice_fingerprint) contains permission // bits before we change this to UsePermissions. let diff_mode = fingerprint::DiffMode::IgnorePermissions; let commands = &get_update_commands( &device_tree, &host_tree, &ninja_installed_files, product_out.clone(), &device.get_installed_apks()?, diff_mode, &partition_paths, cli.global_options.force, stdout, )?; progress::stop(); #[allow(clippy::collapsible_if)] if matches!(cli.command, cli::Commands::Status) { if commands.is_empty() { println!(" Device already up to date."); } } let max_changes = cli.global_options.max_allowed_changes; if matches!(cli.command, cli::Commands::Clean { .. }) { let deletes = &commands.deletes; if deletes.is_empty() { println!(" Nothing to clean."); return Ok(()); } if deletes.len() > max_changes { bail!("There are {} files to be deleted which exceeds the configured limit of {}.\n It is recommended that you reimage your device instead. For small increases in the limit, you can run `adevice clean --max-allowed-changes={}.", deletes.len(), max_changes, deletes.len()); } if matches!(cli.command, cli::Commands::Clean { force } if !force) { println!( "You are about to delete {} [untracked pushed] files. Are you sure? y/N", deletes.len() ); let mut should_delete = String::new(); stdin().read_line(&mut should_delete)?; if should_delete.trim().to_lowercase() != "y" { bail!("Not deleting"); } } // Consider always reboot instead of soft restart after a clean. let restart_chooser = &RestartChooser::new(&restart_choice); device::update(restart_chooser, deletes, profiler, device, cli.should_wait())?; } if matches!(cli.command, cli::Commands::Update) { // Status if commands.is_empty() { println!(" Device already up to date."); return Ok(()); } let all_cmds: HashMap = commands.upserts.clone().into_iter().chain(commands.deletes.clone()).collect(); if all_cmds.len() > max_changes { bail!("There are {} files out of date on the device, which exceeds the configured limit of {}.\n It is recommended to reimage your device. For small increases in the limit, you can run `adevice update --max-allowed-changes={}.", all_cmds.len(), max_changes, all_cmds.len()); } writeln!(stdout, "\n * Updating {} files on device.", all_cmds.len())?; let changed_files = all_cmds.iter().map(|cmd| format!("{:?}", cmd.1.file)).collect(); metrics.add_action_event_with_files_changed( "file_updates", Duration::new(0, 0), changed_files, ); // Send the update commands, but retry once if we need to remount rw an extra time after a flash. for retry in 0..=1 { let update_result = device::update( &RestartChooser::new(&restart_choice), &all_cmds, profiler, device, cli.should_wait(), ); progress::stop(); if update_result.is_ok() { break; } if let Err(problem) = update_result { if retry == 1 { println!("\n\n"); bail!(" !! Error. Unable to push to device event after remount/reboot.\n !! ADB command error: {:?}", problem); } // TODO(rbraunstein): Avoid string checks. Either check mounts directly for this case // or return json with the error message and code from adevice_fingerprint. if problem.root_cause().to_string().contains("Read-only file system") { println!(" * The device has a read-only file system. "); println!(" After a fresh image, the device needs an extra `remount` and `reboot` to adb push files."); println!(" Performing remount and reboot."); println!(); } time!(device.prep_after_flash(profiler)?, profiler.first_remount_rw); } println!(" * Trying update again after remount and reboot."); } } metrics.display_survey(); println!("New android update workflow tool available! go/a-update"); Ok(()) } /// Returns the commands to update the device for every file that should be updated. /// If there are errors, like some files in the staging set have not been built, then /// an error result is returned. #[allow(clippy::too_many_arguments)] fn get_update_commands( device_tree: &HashMap, host_tree: &HashMap, ninja_installed_files: &[String], product_out: PathBuf, installed_packages: &HashSet, diff_mode: DiffMode, partitions: &[PathBuf], force: bool, stdout: &mut impl Write, ) -> Result { // NOTE: The Ninja deps list can be _ahead_of_ the product tree output list. // i.e. m `nothing` will update our ninja list even before someone // does a build to populate product out. // We don't have a way to know if we are in this case or if the user // ever did a `m droid` // We add implicit dirs up to the partition name to the tracked set so the set matches the staging set. let mut ninja_installed_dirs: HashSet = ninja_installed_files.iter().flat_map(|p| parents(p, partitions)).collect(); for p in partitions { ninja_installed_dirs.insert(PathBuf::from(p)); } let tracked_set: HashSet = ninja_installed_files.iter().map(PathBuf::from).chain(ninja_installed_dirs).collect(); let host_set: HashSet = host_tree.keys().map(PathBuf::clone).collect(); // Files that are in the tracked set but NOT in the build directory. These need // to be built. let needs_building: HashSet<&PathBuf> = tracked_set.difference(&host_set).collect(); let status_per_file = &collect_status_per_file( &tracked_set, host_tree, device_tree, &product_out, installed_packages, diff_mode, )?; progress::stop(); print_status(stdout, status_per_file)?; // Shadow apks are apks that are installed outside the system partition with `adb install` // If they exist, we should print instructions to uninstall and stop the update. shadow_apk_check(stdout, status_per_file)?; #[allow(clippy::len_zero)] if needs_building.len() > 0 { if force { println!("UNSAFE: The above modules should be built, but were not. This may cause the device to crash:\nProceeding due to \"--force\" flag."); } else { bail!("ERROR: Please build the above modules before updating.\nIf you want to continue anyway (which may cause the device to crash), rerun adevice with the \"--force\" flag."); } } // Restrict the host set down to the ones that are in the tracked set and not installed in the data partition. let filtered_host_set: HashMap = host_tree .iter() .filter_map(|(key, value)| { if tracked_set.contains(key) { Some((key.clone(), value.clone())) } else { None } }) .collect(); let filtered_changes = fingerprint::diff(&filtered_host_set, device_tree, diff_mode); Ok(commands::compose(&filtered_changes, &product_out)) } // These are the partitions we will try to install to. // ADB sync also has data, oem and vendor. // There are some partition images (like boot.img) that we don't have a good way of determining // the changed status of. (i.e. did they touch files that forces a flash/reimage). // By default we will clean all the default partitions of stale files. const DEFAULT_PARTITIONS: &[&str] = &["system", "system_ext", "odm", "product"]; /// If a user explicitly passes a partition, but that doesn't exist in the tracked files, /// then bail. /// Otherwise, if one of the default partitions does not exist (like system_ext), then /// just remove it from the default. fn validate_partitions( partition_root: &Path, tracked_files: &[String], cli_partitions: &Option>, ) -> Result> { // NOTE: We use PathBuf instead of String so starts_with matches path components. // Use the partitions the user passed in or default to system and system_ext if let Some(partitions) = cli_partitions { for partition in partitions { if !tracked_files.iter().any(|t| PathBuf::from(t).starts_with(partition)) { bail!("{partition:?} is not a valid partition for current lunch target."); } } for partition in partitions { if fs::read_dir(partition_root.join(partition)).is_err() { bail!("{partition:?} partition does not exist on host. Try rebuilding with m"); } } return Ok(partitions.clone()); } let found_partitions: Vec = DEFAULT_PARTITIONS .iter() .filter_map(|part| match tracked_files.iter().any(|t| PathBuf::from(t).starts_with(part)) { true => Some(part.to_string()), false => None, }) .collect(); for partition in &found_partitions { if fs::read_dir(partition_root.join(partition)).is_err() { bail!("{partition:?} partition does not exist on host. Try rebuilding with m"); } } Ok(found_partitions) } #[derive(Clone, PartialEq)] enum PushState { Push, /// File is tracked and the device and host fingerprints match. UpToDate, /// File is not tracked but exists on device and host. TrackOrClean, /// File is on the device, but not host and not tracked. TrackAndBuildOrClean, /// File is tracked and on host but not on device. //PushNew, /// File is on host, but not tracked and not on device. TrackOrMakeClean, /// File is tracked and on the device, but is not in the build tree. /// `m` the module to build it. UntrackOrBuild, /// The apk was `installed` on top of the system image. It will shadow any push /// we make to the system partitions. It should be explicitly installed or uninstalled, not pushed. // TODO(rbraunstein): Store package name and path to file on disk so we can print a better // message to the user. ApkInstalled, } impl PushState { /// Message to print indicating what actions the user should take based on the /// state of the file. pub fn get_action_msg(self) -> String { match self { PushState::Push => "Ready to push:\n (These files are out of date on the device and will be pushed when you run `adevice update`)".to_string(), // Note: we don't print up to date files. PushState::UpToDate => "Up to date: (These files are up to date on the device. There is nothing to do.)".to_string(), PushState::TrackOrClean => "Untracked pushed files:\n (These files are not tracked but exist on the device and host.)\n (Use `adevice track` for the appropriate module to have them pushed.)".to_string(), PushState::TrackAndBuildOrClean => "Stale device files:\n (These files are on the device, but not built or tracked.)\n (They will be cleaned with `adevice update` or `adevice clean`.)".to_string(), PushState::TrackOrMakeClean => "Untracked built files:\n (These files are in the build tree but not tracked or on the device.)\n (You might want to `adevice track` the module. It is safe to do nothing.)".to_string(), PushState::UntrackOrBuild => "Unbuilt files:\n (These files should be built so the device can be updated.)\n (Rebuild and `adevice update`)".to_string(), PushState::ApkInstalled => format!("ADB Installed files:\n{RED_WARNING_LINE} (These files were installed with `adb install` or similar. Pushing to the system partition will not make them available.)\n (Either `adb uninstall` these packages or `adb install` by hand.`)"), } } } // TODO(rbraunstein): Create a struct for each of the sections above for better formatting. const RED_WARNING_LINE: &str = " \x1b[1;31m!! Warning: !!\x1b[0m\n"; /// Group each file by state and print the state message followed by the files in that state. fn print_status(stdout: &mut impl Write, files: &HashMap) -> Result<()> { for state in [ PushState::Push, // Skip UpToDate and TrackOrMakeClean, don't print those. PushState::TrackOrClean, PushState::TrackAndBuildOrClean, PushState::UntrackOrBuild, // Skip APKInstalled, it is handleded in shadow_apk_check. ] { print_files_in_state(stdout, files, state)?; } Ok(()) } /// Determine if file is an apk and decide if we need to give a warning /// about pushing to a system directory because it is already installed in /data /// and will shadow a system apk if we push it. fn installed_apk_action( file: &Path, product_out: &Path, installed_packages: &HashSet, ) -> Result { if file.extension() != Some(OsString::from("apk").as_os_str()) { return Ok(PushState::Push); } // See if this file was installed. if is_apk_installed(&product_out.join(file), installed_packages)? { Ok(PushState::ApkInstalled) } else { Ok(PushState::Push) } } /// Determine if the given apk has been installed via `adb install`. /// This will allow us to decide if pushing to /system will cause problems because the /// version we push would be shadowed by the `installed` version. /// Run PackageManager commands from the shell to check if something is installed. /// If this is a problem, we can build something in to adevice_fingerprint that /// calls PackageManager#getInstalledApplications. /// adb exec-out pm list packages -s -f fn is_apk_installed(host_path: &Path, installed_packages: &HashSet) -> Result { let host_apk_path = host_path.as_os_str().to_str().unwrap(); let aapt_output = std::process::Command::new("aapt2") .args(["dump", "permissions", host_apk_path]) .output() .context(format!("Running aapt2 on host to see if apk installed: {}", host_apk_path))?; if !aapt_output.status.success() { let stderr = String::from_utf8(aapt_output.stderr)?; bail!("Unable to run aapt2 to get installed packages {:?}", stderr); } match package_from_aapt_dump_output(aapt_output.stdout) { Ok(package) => { debug!("AAPT dump found package: {package}"); Ok(installed_packages.contains(&package)) } Err(e) => bail!("Unable to run aapt2 to get package information {e:?}"), } } static AAPT_PACKAGE_MATCHER: LazyLock = LazyLock::new(|| Regex::new(r"^package: (.+)$").expect("regex does not compile")); /// Filter aapt2 dump output to parse out the package name for the apk. fn package_from_aapt_dump_output(stdout: Vec) -> Result { let package_match = String::from_utf8(stdout)? .lines() .filter_map(|line| AAPT_PACKAGE_MATCHER.captures(line).map(|x| x[1].to_string())) .collect(); Ok(package_match) } /// Go through all files that exist on the host, device, and tracking set. /// Ignore any file that is in all three and has the same fingerprint on the host and device. /// States where the user should take action: /// Build /// Clean /// Track /// Untrack fn collect_status_per_file( tracked_set: &HashSet, host_tree: &HashMap, device_tree: &HashMap, product_out: &Path, installed_packages: &HashSet, diff_mode: DiffMode, ) -> Result> { let mut all_files: Vec<&PathBuf> = host_tree.keys().chain(device_tree.keys()).chain(tracked_set.iter()).collect(); all_files.dedup(); let states: HashMap = all_files .par_iter() .map(|f| { let on_device = device_tree.contains_key(*f); let on_host = host_tree.contains_key(*f); let tracked = tracked_set.contains(*f); // I think keeping tracked/untracked else is clearer than collapsing. #[allow(clippy::collapsible_else_if)] let push_state = if tracked { if on_device && on_host { if fingerprint::is_metadata_diff( device_tree.get(*f).unwrap(), host_tree.get(*f).unwrap(), diff_mode, ) { // PushDiff installed_apk_action(f, product_out, installed_packages).expect("checking if apk installed") } else { // Else normal case, do nothing. // TODO(rbraunstein): Do we need to check for installed apk and warn. // 1) User updates apk // 2) User adb install // 3) User reverts code and builds // (host and device match but installed apk shadows system version). // For now, don't look for extra problems. PushState::UpToDate } } else if !on_host { // We don't care if it is on the device or not, it has to built if it isn't // on the host. PushState::UntrackOrBuild } else { assert!( !on_device && on_host, "Unexpected state for file: {f:?}, tracked: {tracked} on_device: {on_device}, on_host: {on_host}" ); // TODO(rbraunstein): Is it possible for an apk to be adb installed, but not in the system image? // I guess so, but seems weird. Add check InstalledApk here too. // PushNew PushState::Push } } else { if on_device && on_host { PushState::TrackOrClean } else if on_device && !on_host { PushState::TrackAndBuildOrClean } else { // Note: case of !tracked, !on_host, !on_device is not possible. // So only one case left. assert!( !on_device && on_host, "Unexpected state for file: {f:?}, tracked: {tracked} on_device: {on_device}, on_host: {on_host}" ); PushState::TrackOrMakeClean } }; (PathBuf::from(f), push_state) }) .collect(); Ok(states) } /// Find all files in a given state, and if that file list is not empty, print the /// state message and all the files (sorted). /// Only prints stages that files in that stage. fn print_files_in_state( stdout: &mut impl Write, files: &HashMap, push_state: PushState, ) -> Result<()> { let filtered_files: HashMap<&PathBuf, &PushState> = files.iter().filter(|(_, state)| *state == &push_state).collect(); if filtered_files.is_empty() { return Ok(()); } writeln!(stdout, "{}", &push_state.get_action_msg())?; let file_list_output = filtered_files .keys() .sorted() .map(|path| format!("\t{}", path.display())) .collect::>() .join("\n"); writeln!(stdout, "{}", file_list_output)?; Ok(()) } fn get_product_out_from_env() -> Option { match std::env::var("ANDROID_PRODUCT_OUT") { Ok(x) if !x.is_empty() => Some(PathBuf::from(x)), _ => None, } } /// Prints uninstall commands for every package installed /// Bails if there are any installed packages. fn shadow_apk_check(stdout: &mut impl Write, files: &HashMap) -> Result<()> { let filtered_files: HashMap<&PathBuf, &PushState> = files.iter().filter(|(_, state)| *state == &PushState::ApkInstalled).collect(); if filtered_files.is_empty() { return Ok(()); } writeln!(stdout, "{}", PushState::ApkInstalled.get_action_msg())?; let file_list_output = filtered_files .keys() .sorted() .map(|path| format!("adb uninstall {};", path.display())) .collect::>() .join("\n"); writeln!(stdout, "{}", file_list_output)?; bail!("{} shadowing apks found. Uninstall to continue.", filtered_files.keys().len()); } /// Return all path components of file_path up to a passed partition. /// Given system/bin/logd and partition "system", /// return ["system/bin/logd", "system/bin"], not "system" or "" fn parents(file_path: &str, partitions: &[PathBuf]) -> Vec { PathBuf::from(file_path) .ancestors() .map(|p| p.to_path_buf()) .take_while(|p| !partitions.contains(p)) .collect() } #[allow(missing_docs)] #[derive(Default)] pub struct Profiler { pub device_fingerprint: Duration, pub host_fingerprint: Duration, pub ninja_deps_computer: Duration, /// Time to run all the "adb push" or "adb rm" commands. pub adb_cmds: Duration, /// Time to run "adb reboot" or "exec-out start". pub restart: Duration, pub restart_type: String, /// Time for device to respond to "wait-for-device". pub wait_for_device: Duration, /// Time for sys.boot_completed to be 1 after wait-for-device. pub wait_for_boot_completed: Duration, /// The first time after a userdebug build is flashed/created, we need /// to mount rw and reboot. pub first_remount_rw: Duration, pub total: Duration, } impl std::fmt::Display for Profiler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", [ " Operation profile: (secs)".to_string(), format!("Device Fingerprint - {}", self.device_fingerprint.as_secs()), format!("Host fingerprint - {}", self.host_fingerprint.as_secs()), format!("Ninja - {}", self.ninja_deps_computer.as_secs()), format!("Adb Cmds - {}", self.adb_cmds.as_secs()), format!("Restart({})- {}", self.restart_type, self.restart.as_secs()), format!("Wait For device connected - {}", self.wait_for_device.as_secs()), format!("Wait For boot completed - {}", self.wait_for_boot_completed.as_secs()), format!("First remount RW - {}", self.first_remount_rw.as_secs()), format!("TOTAL - {}", self.total.as_secs()), ] .join("\n\t") ) } } #[cfg(test)] mod tests { use super::*; use crate::fingerprint::{self, DiffMode}; use std::path::PathBuf; use tempfile::TempDir; // TODO(rbraunstein): Capture/test stdout and logging. // Test stdout: https://users.rust-lang.org/t/how-to-test-functions-that-use-println/67188/5 #[test] fn empty_inputs() -> Result<()> { let device_files: HashMap = HashMap::new(); let host_files: HashMap = HashMap::new(); let ninja_deps: Vec = vec![]; let product_out = PathBuf::from(""); let installed_apks = HashSet::::new(); let partitions = Vec::new(); let force = false; let mut stdout = Vec::new(); let results = get_update_commands( &device_files, &host_files, &ninja_deps, product_out, &installed_apks, DiffMode::UsePermissions, &partitions, force, &mut stdout, )?; assert_eq!(results.upserts.values().len(), 0); Ok(()) } #[test] fn host_and_ninja_file_not_on_device() -> Result<()> { // Relative to product out? let product_out = PathBuf::from(""); let installed_apks = HashSet::::new(); let partitions = Vec::new(); let mut stdout = Vec::new(); let force = true; let results = get_update_commands( // Device files &HashMap::new(), // Host files &HashMap::from([ (PathBuf::from("system/myfile"), file_metadata("digest1")), (PathBuf::from("system"), dir_metadata()), ]), // Ninja deps &["system".to_string(), "system/myfile".to_string()], product_out, &installed_apks, DiffMode::UsePermissions, &partitions, force, &mut stdout, )?; assert_eq!(results.upserts.values().len(), 2); Ok(()) } #[test] fn host_and_ninja_file_not_on_device_force_false() -> Result<()> { let product_out = PathBuf::from(""); let installed_apks = HashSet::::new(); let partitions = Vec::new(); let mut stdout = Vec::new(); let force = false; let results = get_update_commands( // Device files &HashMap::new(), // Host files &HashMap::from([ (PathBuf::from("system/myfile"), file_metadata("digest1")), (PathBuf::from("system"), dir_metadata()), ]), // Ninja deps &["system".to_string(), "system/myfile".to_string()], product_out, &installed_apks, DiffMode::UsePermissions, &partitions, force, &mut stdout, ); assert!(results.is_err()); if let Err(e) = results { assert!(e .to_string() .contains("ERROR: Please build the above modules before updating.")); } Ok(()) } #[test] fn test_shadow_apk_check_no_shadowing_apks() -> Result<()> { let mut output = Vec::new(); let files = &HashMap::from([(PathBuf::from("/system/app1.apk"), PushState::Push)]); let result = shadow_apk_check(&mut output, files); assert!(result.is_ok()); assert!(output.is_empty()); Ok(()) } #[test] fn test_shadow_apk_check_with_shadowing_apks() -> Result<()> { let mut output = Vec::new(); let files = &HashMap::from([ (PathBuf::from("/system/app1.apk"), PushState::Push), (PathBuf::from("/data/app2.apk"), PushState::ApkInstalled), (PathBuf::from("/data/app3.apk"), PushState::ApkInstalled), ]); let result = shadow_apk_check(&mut output, files); assert!(result.is_err()); let output_str = String::from_utf8(output).unwrap(); assert!( output_str.contains("Either `adb uninstall` these packages or `adb install` by hand.") ); assert!(output_str.contains("adb uninstall /data/app2.apk;")); assert!(output_str.contains("adb uninstall /data/app3.apk;")); Ok(()) } #[test] fn on_host_not_in_tracked_on_device() -> Result<()> { let results = call_update(&FakeState { device_data: &["system/f1"], host_data: &["system/f1"], tracked_set: &[], })? .upserts; assert_eq!(0, results.values().len()); Ok(()) } #[test] fn in_host_not_in_tracked_not_on_device() -> Result<()> { let results = call_update(&FakeState { device_data: &[""], host_data: &["system/f1"], tracked_set: &[], })? .upserts; assert_eq!(0, results.values().len()); Ok(()) } #[test] fn test_parents_stops_at_partition() { assert_eq!( vec![ PathBuf::from("some/long/path/file"), PathBuf::from("some/long/path"), PathBuf::from("some/long"), ], parents("some/long/path/file", &[PathBuf::from("some")]), ); } #[test] fn validate_partition_removes_unused_default_partition() -> Result<()> { let tmp_root = TempDir::new().unwrap(); fs::create_dir_all(tmp_root.path().join("system")).unwrap(); // No system_ext here, so remove from default partitions let ninja_deps = vec![ "system/file1".to_string(), "file3".to_string(), "system/dir2/file1".to_string(), "data/sys/file4".to_string(), ]; assert_eq!( vec!["system".to_string(),], validate_partitions(tmp_root.path(), &ninja_deps, &None)? ); Ok(()) } #[test] fn validate_partition_bails_on_bad_partition_name() { let tmp_root = TempDir::new().unwrap(); fs::create_dir_all(tmp_root.path().join("system")).unwrap(); fs::create_dir_all(tmp_root.path().join("sys")).unwrap(); let ninja_deps = vec![ "system/file1".to_string(), "file3".to_string(), "system/dir2/file1".to_string(), "data/sys/file4".to_string(), ]; // "sys" isn't a valid partition name, but it matches a prefix of "system". // Should bail. match validate_partitions(tmp_root.path(), &ninja_deps, &Some(vec!["sys".to_string()])) { Ok(_) => panic!("Expected error"), Err(e) => { assert!( e.to_string().contains("\"sys\" is not a valid partition"), "{}", e.to_string() ) } } } #[test] fn validate_partition_bails_on_no_partition_on_host() { let tmp_root = TempDir::new().unwrap(); let ninja_deps = vec!["system/file1".to_string()]; match validate_partitions(tmp_root.path(), &ninja_deps, &Some(vec!["system".to_string()])) { Ok(_) => panic!("Expected error"), Err(e) => { assert!( e.to_string().contains("\"system\" partition does not exist on host"), "{}", e.to_string() ) } } } // TODO(rbraunstein): Test case where on device and up to date, but not tracked. struct FakeState { device_data: &'static [&'static str], host_data: &'static [&'static str], tracked_set: &'static [&'static str], } // Helper to call update. // Uses filename for the digest in the fingerprint // Add directories for every file on the host like walkdir would do. // `update` adds the directories for the tracked set so we don't do that here. fn call_update(fake_state: &FakeState) -> Result { let product_out = PathBuf::from(""); let installed_apks = HashSet::::new(); let partitions = Vec::new(); let force = false; let mut device_files: HashMap = HashMap::new(); let mut host_files: HashMap = HashMap::new(); for d in fake_state.device_data { // Set the digest to the filename for now. device_files.insert(PathBuf::from(d), file_metadata(d)); } for h in fake_state.host_data { // Set the digest to the filename for now. host_files.insert(PathBuf::from(h), file_metadata(h)); // Add the dir too. } let tracked_set: Vec = fake_state.tracked_set.iter().map(|s| s.to_string()).collect(); let mut stdout = Vec::new(); get_update_commands( &device_files, &host_files, &tracked_set, product_out, &installed_apks, DiffMode::UsePermissions, &partitions, force, &mut stdout, ) } fn file_metadata(digest: &str) -> FileMetadata { FileMetadata { file_type: fingerprint::FileType::File, digest: digest.to_string(), ..Default::default() } } fn dir_metadata() -> FileMetadata { FileMetadata { file_type: fingerprint::FileType::Directory, ..Default::default() } } // TODO(rbraunstein): Add tests for collect_status_per_file after we decide on output. }