| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| super::*, |
| chrono::prelude::*, |
| fidl_fuchsia_pkg::PackageUrl, |
| fidl_fuchsia_update_installer::{ |
| CompleteData, FetchData, InstallationProgress, Options, State, UpdateInfo, UpdateResult, |
| }, |
| matches::assert_matches, |
| pretty_assertions::assert_eq, |
| serde_json::json, |
| }; |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn succeeds_without_writable_data() { |
| let env = TestEnv::builder().mount_data(false).build().await; |
| |
| env.resolver |
| .register_package("update", "upd4t3") |
| .add_file("packages.json", make_packages_json([])) |
| .add_file("epoch.json", make_epoch_json(SOURCE_EPOCH)) |
| .add_file("zbi", "fake zbi"); |
| |
| env.run_update().await.expect("run system updater"); |
| |
| assert_eq!(env.read_history(), None); |
| |
| assert_eq!( |
| env.get_ota_metrics().await, |
| OtaMetrics { |
| initiator: metrics::OtaResultAttemptsMetricDimensionInitiator::UserInitiatedCheck |
| as u32, |
| phase: metrics::OtaResultAttemptsMetricDimensionPhase::SuccessPendingReboot as u32, |
| status_code: metrics::OtaResultAttemptsMetricDimensionStatusCode::Success as u32, |
| target: "".into(), |
| } |
| ); |
| |
| assert_eq!( |
| env.take_interactions(), |
| vec![ |
| Paver(PaverEvent::QueryCurrentConfiguration), |
| Paver(PaverEvent::ReadAsset { |
| configuration: paver::Configuration::A, |
| asset: paver::Asset::VerifiedBootMetadata |
| }), |
| Paver(PaverEvent::ReadAsset { |
| configuration: paver::Configuration::A, |
| asset: paver::Asset::Kernel |
| }), |
| Paver(PaverEvent::QueryCurrentConfiguration), |
| Paver(PaverEvent::QueryConfigurationStatus { configuration: paver::Configuration::A }), |
| Paver(PaverEvent::SetConfigurationUnbootable { |
| configuration: paver::Configuration::B |
| }), |
| Paver(PaverEvent::BootManagerFlush), |
| PackageResolve(UPDATE_PKG_URL.to_string()), |
| Gc, |
| BlobfsSync, |
| Paver(PaverEvent::WriteAsset { |
| configuration: paver::Configuration::B, |
| asset: paver::Asset::Kernel, |
| payload: b"fake zbi".to_vec(), |
| }), |
| Paver(PaverEvent::SetConfigurationActive { configuration: paver::Configuration::B }), |
| Paver(PaverEvent::DataSinkFlush), |
| Paver(PaverEvent::BootManagerFlush), |
| Reboot, |
| ] |
| ); |
| } |
| |
| /// Given a parsed update history value, verify each attempt contains an 'id', that no 2 'id' |
| /// fields are the same, and return the object with those fields removed. |
| fn strip_attempt_ids(mut value: serde_json::Value) -> serde_json::Value { |
| let _ids = value |
| .as_object_mut() |
| .expect("top level is object") |
| .get_mut("content") |
| .expect("top level 'content' key") |
| .as_array_mut() |
| .expect("'content' is array") |
| .iter_mut() |
| .map(|attempt| attempt.as_object_mut().expect("attempt is object")) |
| .fold(std::collections::HashSet::new(), |mut ids, attempt| { |
| let attempt_id = match attempt.remove("id").expect("'id' field to be present") { |
| serde_json::Value::String(id) => id, |
| _ => panic!("expect attempt id to be a str"), |
| }; |
| assert_eq!(ids.replace(attempt_id), None); |
| ids |
| }); |
| value |
| } |
| |
| /// Given a parsed update history value, verify each attempt contains a 'start' time that's later |
| /// than 01/01/2020, and return the object with those fields removed. |
| fn strip_start_time(mut value: serde_json::Value) -> serde_json::Value { |
| let min_start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0).timestamp_nanos() as u64; |
| value |
| .as_object_mut() |
| .expect("top level is object") |
| .get_mut("content") |
| .expect("top level 'content' key") |
| .as_array_mut() |
| .expect("'content' is array") |
| .iter_mut() |
| .map(|attempt| attempt.as_object_mut().expect("attempt is object")) |
| .for_each(|attempt| { |
| assert_matches!( |
| attempt.remove("start"), |
| Some(serde_json::Value::Number(start)) if start.as_u64().expect("start is u64") > min_start_time |
| ); |
| }); |
| value |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn writes_history() { |
| let env = TestEnv::builder().build().await; |
| |
| assert_eq!(env.read_history(), None); |
| |
| env.set_build_version("0.1"); |
| |
| env.resolver |
| .register_package("update", UPDATE_HASH) |
| .add_file("packages.json", make_packages_json(["fuchsia-pkg://fuchsia.com/system_image/0?hash=838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67"])) |
| .add_file("epoch.json", make_epoch_json(SOURCE_EPOCH)) |
| .add_file("zbi", "fake zbi") |
| .add_file("fuchsia.vbmeta", "vbmeta") |
| .add_file("version", "0.2"); |
| env.resolver.register_package( |
| "system_image/0?hash=838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67", |
| "838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67", |
| ); |
| |
| env.run_update_with_options( |
| UPDATE_PKG_URL, |
| fidl_fuchsia_update_installer_ext::Options { |
| initiator: Initiator::Service, |
| allow_attach_to_existing_attempt: false, |
| should_write_recovery: true, |
| }, |
| ) |
| .await |
| .unwrap(); |
| |
| assert_eq!( |
| env.read_history().map(strip_attempt_ids).map(strip_start_time), |
| Some(json!({ |
| "version": "1", |
| "content": [{ |
| "source": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "zbi_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "build_version": "0.1.0.0", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "target": { |
| "update_hash": UPDATE_HASH, |
| "system_image_hash": "838b5199d12c8ff4ef92bfd9771d2f8781b7b8fd739dd59bcf63f353a1a93f67", |
| "vbmeta_hash": "a0c6f07a4b3a17fb9348db981de3c5602e2685d626599be1bd909195c694a57b", |
| "zbi_hash": "543b8066d52d734f69794fd0594ba78a5b8e11124d51f4d549dd6534d46da73e", |
| "build_version": "0.2.0.0", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "options": { |
| "allow_attach_to_existing_attempt": false, |
| "initiator": "Service", |
| "should_write_recovery": true, |
| }, |
| "url": UPDATE_PKG_URL, |
| "state": { |
| "id": "reboot", |
| "info": { |
| "download_size": 0, |
| }, |
| "progress": { |
| "bytes_downloaded": 0, |
| "fraction_completed": 1.0, |
| }, |
| }, |
| }], |
| })) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn replaces_bogus_history() { |
| let env = TestEnv::builder().build().await; |
| |
| env.write_history(json!({ |
| "valid": "no", |
| })); |
| |
| env.resolver |
| .register_package("update", UPDATE_HASH) |
| .add_file("packages.json", make_packages_json([])) |
| .add_file("epoch.json", make_epoch_json(SOURCE_EPOCH)) |
| .add_file("zbi", "fake zbi"); |
| |
| env.run_update().await.unwrap(); |
| |
| assert_eq!( |
| env.read_history().map(strip_attempt_ids).map(strip_start_time), |
| Some(json!({ |
| "version": "1", |
| "content": [{ |
| "source": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "zbi_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "target": { |
| "update_hash": UPDATE_HASH, |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "543b8066d52d734f69794fd0594ba78a5b8e11124d51f4d549dd6534d46da73e", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "options": { |
| "allow_attach_to_existing_attempt": true, |
| "initiator": "User", |
| "should_write_recovery": true, |
| }, |
| "url": UPDATE_PKG_URL, |
| "state": { |
| "id": "reboot", |
| "info": { |
| "download_size": 0, |
| }, |
| "progress": { |
| "bytes_downloaded": 0, |
| "fraction_completed": 1.0, |
| }, |
| }, |
| }], |
| })) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn increments_attempts_counter_on_retry() { |
| let env = TestEnv::builder().build().await; |
| |
| env.resolver |
| .url("fuchsia-pkg://fuchsia.com/not-found") |
| .fail(fidl_fuchsia_pkg::ResolveError::PackageNotFound); |
| env.resolver |
| .register_package("update", UPDATE_HASH) |
| .add_file("packages.json", make_packages_json([])) |
| .add_file("epoch.json", make_epoch_json(SOURCE_EPOCH)) |
| .add_file("zbi", "fake zbi"); |
| |
| let _ = env |
| .run_update_with_options( |
| "fuchsia-pkg://fuchsia.com/not-found", |
| fidl_fuchsia_update_installer_ext::Options { |
| initiator: Initiator::Service, |
| allow_attach_to_existing_attempt: false, |
| should_write_recovery: true, |
| }, |
| ) |
| .await |
| .unwrap_err(); |
| |
| env.run_update().await.unwrap(); |
| |
| assert_eq!( |
| env.read_history().map(strip_attempt_ids).map(strip_start_time), |
| Some(json!({ |
| "version": "1", |
| "content": [ |
| { |
| "source": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "zbi_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "target": { |
| "update_hash": UPDATE_HASH, |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "543b8066d52d734f69794fd0594ba78a5b8e11124d51f4d549dd6534d46da73e", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "options": { |
| "allow_attach_to_existing_attempt": true, |
| "initiator": "User", |
| "should_write_recovery": true, |
| }, |
| "url": "fuchsia-pkg://fuchsia.com/update", |
| "state": { |
| "id": "reboot", |
| "info": { |
| "download_size": 0, |
| }, |
| "progress": { |
| "bytes_downloaded": 0, |
| "fraction_completed": 1.0, |
| }, |
| }, |
| }, |
| { |
| "source": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "zbi_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "target": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "options": { |
| "allow_attach_to_existing_attempt": false, |
| "initiator": "Service", |
| "should_write_recovery": true, |
| }, |
| "url": "fuchsia-pkg://fuchsia.com/not-found", |
| "state": { |
| "id": "fail_prepare", |
| "reason": "internal", |
| }, |
| } |
| ], |
| })) |
| ); |
| } |
| |
| /// When there's history, the history FIDL APIs should return that history. |
| #[fasync::run_singlethreaded(test)] |
| async fn serves_fidl_with_history_present() { |
| let env = TestEnv::builder() |
| .history(json!({ |
| "version": "1", |
| "content": [ |
| { |
| "id": "1", |
| "source": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "target": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "options": { |
| "allow_attach_to_existing_attempt": false, |
| "initiator": "User", |
| "should_write_recovery": true, |
| }, |
| "url": "fuchsia-pkg://fuchsia.com/second-attempt", |
| "start": 20, |
| "state": { |
| "id": "complete", |
| "info": { |
| "download_size": 0, |
| }, |
| "progress": { |
| "bytes_downloaded": 0, |
| "fraction_completed": 1.0, |
| }, |
| }, |
| }, |
| { |
| "id": "0", |
| "source": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "target": { |
| "update_hash": "", |
| "system_image_hash": "", |
| "vbmeta_hash": "", |
| "zbi_hash": "", |
| "build_version": "", |
| "epoch": SOURCE_EPOCH.to_string() |
| }, |
| "options": { |
| "allow_attach_to_existing_attempt": false, |
| "initiator": "User", |
| "should_write_recovery": true, |
| }, |
| "url": "fuchsia-pkg://fuchsia.com/first-attempt", |
| "start": 10, |
| "state": { |
| "id": "fetch", |
| "info": { |
| "download_size": 42, |
| }, |
| "progress": { |
| "bytes_downloaded": 36, |
| "fraction_completed": 0.8, |
| }, |
| }, |
| } |
| ], |
| })) |
| .build() |
| .await; |
| |
| let installer_proxy = env.installer_proxy(); |
| |
| assert_eq!( |
| installer_proxy.get_last_update_result().await.unwrap(), |
| UpdateResult { |
| attempt_id: Some("1".to_string()), |
| url: Some(PackageUrl { url: "fuchsia-pkg://fuchsia.com/second-attempt".to_string() }), |
| options: Some(Options { |
| initiator: Some(fidl_fuchsia_update_installer::Initiator::User,), |
| allow_attach_to_existing_attempt: Some(false), |
| should_write_recovery: Some(true), |
| ..Options::EMPTY |
| }), |
| state: Some(State::Complete(CompleteData { |
| info: Some(UpdateInfo { download_size: None, ..UpdateInfo::EMPTY }), |
| progress: Some(InstallationProgress { |
| fraction_completed: Some(1.0), |
| bytes_downloaded: None, |
| ..InstallationProgress::EMPTY |
| }), |
| ..CompleteData::EMPTY |
| })), |
| ..UpdateResult::EMPTY |
| } |
| ); |
| assert_eq!( |
| installer_proxy.get_update_result("0").await.unwrap(), |
| UpdateResult { |
| attempt_id: Some("0".to_string()), |
| url: Some(PackageUrl { url: "fuchsia-pkg://fuchsia.com/first-attempt".to_string() }), |
| options: Some(Options { |
| initiator: Some(fidl_fuchsia_update_installer::Initiator::User), |
| allow_attach_to_existing_attempt: Some(false), |
| should_write_recovery: Some(true), |
| ..Options::EMPTY |
| }), |
| state: Some(State::Fetch(FetchData { |
| info: Some(UpdateInfo { download_size: Some(42), ..UpdateInfo::EMPTY }), |
| progress: Some(InstallationProgress { |
| fraction_completed: Some(0.8), |
| bytes_downloaded: Some(36), |
| ..InstallationProgress::EMPTY |
| }), |
| ..FetchData::EMPTY |
| })), |
| ..UpdateResult::EMPTY |
| } |
| ); |
| assert_eq!( |
| installer_proxy.get_update_result("1").await.unwrap(), |
| UpdateResult { |
| attempt_id: Some("1".to_string()), |
| url: Some(PackageUrl { url: "fuchsia-pkg://fuchsia.com/second-attempt".to_string() }), |
| options: Some(Options { |
| initiator: Some(fidl_fuchsia_update_installer::Initiator::User), |
| allow_attach_to_existing_attempt: Some(false), |
| should_write_recovery: Some(true), |
| ..Options::EMPTY |
| }), |
| state: Some(State::Complete(CompleteData { |
| info: Some(UpdateInfo { download_size: None, ..UpdateInfo::EMPTY }), |
| progress: Some(InstallationProgress { |
| fraction_completed: Some(1.0), |
| bytes_downloaded: None, |
| ..InstallationProgress::EMPTY |
| }), |
| ..CompleteData::EMPTY |
| })), |
| ..UpdateResult::EMPTY |
| } |
| ); |
| } |
| |
| /// When there's no history, the history FIDL APIs should return results with empty fields. |
| #[fasync::run_singlethreaded(test)] |
| async fn serves_fidl_without_history_present() { |
| let env = TestEnv::new().await; |
| |
| let installer_proxy = env.installer_proxy(); |
| |
| assert_eq!( |
| installer_proxy.get_last_update_result().await.unwrap(), |
| UpdateResult { |
| attempt_id: None, |
| url: None, |
| options: None, |
| state: None, |
| ..UpdateResult::EMPTY |
| } |
| ); |
| assert_eq!( |
| installer_proxy.get_update_result("0").await.unwrap(), |
| UpdateResult { |
| attempt_id: None, |
| url: None, |
| options: None, |
| state: None, |
| ..UpdateResult::EMPTY |
| } |
| ); |
| } |