blob: 94b2a6db93ad2cfe1167144d15484964e23b52c0 [file] [log] [blame]
// 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::*,
fidl_fuchsia_update_installer_ext::{
monitor_update, start_update, Initiator, Options, Progress, State, StateId,
UpdateAttemptError, UpdateInfo, UpdateInfoAndProgress,
},
matches::assert_matches,
pretty_assertions::assert_eq,
};
#[fasync::run_singlethreaded(test)]
async fn progress_reporting_fetch_multiple_packages() {
let env = TestEnv::builder().build();
let pkg1_url = pinned_pkg_url!("package1/0", "aa");
let pkg2_url = pinned_pkg_url!("package2/0", "bb");
let pkg3_url = pinned_pkg_url!("package3/0", "cc");
let update_pkg = env
.resolver
.package("update", UPDATE_HASH)
.add_file("packages.json", make_packages_json([pkg1_url, pkg2_url, pkg3_url]));
let pkg1 = env.resolver.package("package1", merkle_str!("aa"));
let pkg2 = env.resolver.package("package2", merkle_str!("bb"));
let pkg3 = env.resolver.package("package3", merkle_str!("cc"));
// We need to block all the resolves so that we can assert Fetch progress
// is emitted for each pkg fetch. Otherwise, the Fetch state updates could merge
// into one Fetch state.
let handle_update_pkg = env.resolver.url(UPDATE_PKG_URL).block_once();
let handle_pkg1 = env.resolver.url(pkg1_url).block_once();
let handle_pkg2 = env.resolver.url(pkg2_url).block_once();
let handle_pkg3 = env.resolver.url(pkg3_url).block_once();
// Start the system update.
let mut attempt = env.start_update().await.unwrap();
assert_eq!(attempt.next().await.unwrap().unwrap(), State::Prepare);
let info = UpdateInfo::builder().download_size(0).build();
handle_update_pkg.resolve(&update_pkg).await;
assert_eq!(
attempt.next().await.unwrap().unwrap(),
State::Fetch(
UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.0).bytes_downloaded(0).build())
.build()
)
);
handle_pkg1.resolve(&pkg1).await;
assert_eq!(
attempt.next().await.unwrap().unwrap(),
State::Fetch(
UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.25).bytes_downloaded(0).build())
.build()
)
);
handle_pkg2.resolve(&pkg2).await;
assert_eq!(
attempt.next().await.unwrap().unwrap(),
State::Fetch(
UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.5).bytes_downloaded(0).build())
.build()
)
);
handle_pkg3.resolve(&pkg3).await;
assert_eq!(
attempt.next().await.unwrap().unwrap(),
State::Fetch(
UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.75).bytes_downloaded(0).build())
.build()
)
);
// In this test, we are only testing Fetch updates. Let's assert the Fetch
// phase is over.
assert_eq!(attempt.next().await.unwrap().unwrap().id(), StateId::Stage);
}
#[fasync::run_singlethreaded(test)]
async fn monitor_fails_when_no_update_running() {
let env = TestEnv::builder().build();
// There is no update underway, so the monitor should not attach.
assert_matches!(monitor_update(None, &env.installer_proxy()).await, Ok(None));
assert_eq!(env.logger_factory.loggers.lock().len(), 0);
assert_eq!(env.take_interactions(), vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn monitor_connects_to_existing_attempt() {
let env = TestEnv::builder().build();
let update_pkg = env
.resolver
.package("update", UPDATE_HASH)
.add_file("packages.json", make_packages_json([]))
.add_file("zbi", "fake zbi");
// Block the update pkg resolve to ensure the update attempt is still in
// in progress when we try to attach a monitor.
let handle_update_pkg = env.resolver.url(UPDATE_PKG_URL).block_once();
// Start the system update.
let attempt0 = env.start_update().await.unwrap();
// Attach monitor.
let attempt1 =
monitor_update(Some(attempt0.attempt_id()), &env.installer_proxy()).await.unwrap().unwrap();
// Now that we attached both monitors to the current attempt, we can unblock the
// resolve and resume the update attempt.
handle_update_pkg.resolve(&update_pkg).await;
let monitor0_events: Vec<State> = attempt0.map(|res| res.unwrap()).collect().await;
let monitor1_events: Vec<State> = attempt1.map(|res| res.unwrap()).collect().await;
// Since we wait until the update attempt is over to read from monitor1 events,
// we should expect that the events in monitor1 merged.
assert_eq!(monitor1_events.len(), 5);
// While the number of events are different, the ordering should still be the same.
let expected_order =
[StateId::Prepare, StateId::Fetch, StateId::Stage, StateId::WaitToReboot, StateId::Reboot];
assert_success_monitor_states(monitor0_events, &expected_order);
assert_success_monitor_states(monitor1_events, &expected_order);
}
#[fasync::run_singlethreaded(test)]
async fn succeed_additional_start_requests_when_compatible() {
let env = TestEnv::builder().build();
let update_pkg = env
.resolver
.package("update", UPDATE_HASH)
.add_file("packages.json", make_packages_json([]))
.add_file("zbi", "fake zbi");
// Block the update pkg resolve to ensure the update attempt is still in
// in progress when we try to attach a monitor.
let handle_update_pkg = env.resolver.url(UPDATE_PKG_URL).block_once();
// Start the system update, making 2 start_update requests. The second start_update request
// is essentially just a monitor_update request in this case.
let attempt0 = env.start_update().await.unwrap();
let attempt1 = env
.start_update_with_options(
UPDATE_PKG_URL,
Options {
initiator: Initiator::User,
allow_attach_to_existing_attempt: true,
should_write_recovery: true,
},
)
.await
.unwrap();
// Now that we attached both monitors to the current attempt, we can unblock the
// resolve and resume the update attempt.
handle_update_pkg.resolve(&update_pkg).await;
let monitor0_events: Vec<State> = attempt0.map(|res| res.unwrap()).collect().await;
let monitor1_events: Vec<State> = attempt1.map(|res| res.unwrap()).collect().await;
// Since we waited for the update attempt to complete before reading from monitor1,
// the events in monitor1 should have merged.
assert_eq!(monitor1_events.len(), 5);
// While the number of events are different, the ordering should still be the same.
let expected_order =
[StateId::Prepare, StateId::Fetch, StateId::Stage, StateId::WaitToReboot, StateId::Reboot];
assert_success_monitor_states(monitor0_events, &expected_order);
assert_success_monitor_states(monitor1_events, &expected_order);
}
#[fasync::run_singlethreaded(test)]
async fn fail_additional_start_requests_when_not_compatible() {
let env = TestEnv::builder().build();
env.resolver
.package("update", UPDATE_HASH)
.add_file("packages.json", make_packages_json([]))
.add_file("zbi", "fake zbi");
// Block the update pkg resolve to ensure the update attempt is still in
// in progress when we try to make additional start_update requests.
let _handle_update_pkg = env.resolver.url(UPDATE_PKG_URL).block_once();
// Start the system update.
let installer_proxy = env.installer_proxy();
let compatible_url = UPDATE_PKG_URL;
let compatible_options = Options {
initiator: Initiator::User,
allow_attach_to_existing_attempt: true,
should_write_recovery: true,
};
let _attempt =
env.start_update_with_options(compatible_url, compatible_options.clone()).await.unwrap();
// Define incompatible options and url.
let incompatible_options0 = Options {
initiator: Initiator::User,
allow_attach_to_existing_attempt: true,
should_write_recovery: false,
};
let incompatible_options1 = Options {
initiator: Initiator::User,
allow_attach_to_existing_attempt: false,
should_write_recovery: true,
};
let incompatible_url = "fuchsia-pkg://fuchsia.com/different-url";
// Show that start_update requests fail with AlreadyInProgress errors.
assert_matches!(
env.start_update_with_options(compatible_url, incompatible_options0)
.await
.map(|_| ())
.unwrap_err(),
UpdateAttemptError::InstallInProgress
);
assert_matches!(
env.start_update_with_options(compatible_url, incompatible_options1)
.await
.map(|_| ())
.unwrap_err(),
UpdateAttemptError::InstallInProgress
);
assert_matches!(
env.start_update_with_options(incompatible_url, compatible_options.clone())
.await
.map(|_| ())
.unwrap_err(),
UpdateAttemptError::InstallInProgress
);
let (_, server_end) = fidl::endpoints::create_endpoints().unwrap();
assert_matches!(
start_update(
&compatible_url.parse().unwrap(),
compatible_options,
&installer_proxy,
Some(server_end)
)
.await
.map(|_| ())
.unwrap_err(),
UpdateAttemptError::InstallInProgress
);
}
pub fn assert_success_monitor_states(states: Vec<State>, ordering: &[StateId]) {
let res = util::verify_monitor_states(&states, &ordering, false);
if let Err(e) = res {
panic!("Error received when verifying monitor states: {:#}\nWant ordering: {:#?}\nGot states:{:#?}", e, ordering, states);
}
}
pub fn _assert_failure_monitor_states(states: Vec<State>, ordering: Vec<StateId>) {
let res = util::verify_monitor_states(&states, &ordering, false);
if let Err(e) = res {
panic!("Error received when verifying monitor states: {:#}\nWant ordering: {:#?}\nGot states:{:#?}", e, ordering, states);
}
}
mod util {
use {
fidl_fuchsia_update_installer_ext::{
Progress, State, StateId, UpdateInfo, UpdateInfoAndProgress,
},
std::collections::HashSet,
thiserror::Error,
};
#[derive(Debug, Error, PartialEq)]
pub enum VerifyMonitorStatesError {
#[error("there are more IDs in the ordering than in the provided states")]
TooFewStates,
#[error("the order of the states does not match the passed in ordering")]
OutOfOrder,
#[error("progress should be strictly nondecreasing")]
ProgressDecreased,
#[error("received a {0:?} state, which wasn't in the ordering")]
UnexpectedState(StateId),
#[error("the final fraction_completed should be 1.0 on successful attempts")]
FractionCompletedSuccessNot1,
}
/// Validate that
/// * states are in the right order (ignoring duplicates)
/// * progress is strictly nondecreasing
/// * fraction_completed stays within [0.0, 1,0] bounds
/// * on successful update attempts, the final progress should be 1.0.
pub fn verify_monitor_states(
states: &[State],
ordering: &[StateId],
expect_success: bool,
) -> Result<(), VerifyMonitorStatesError> {
// Sanity check input
if states.len() < ordering.len() {
return Err(VerifyMonitorStatesError::TooFewStates);
}
let ordering_set: HashSet<StateId> = ordering.iter().cloned().collect();
if ordering_set.len() != ordering.len() {
panic!("Ordering should not have duplicates: {:?} ", ordering);
}
for state in states.iter() {
if !ordering.contains(&state.id()) {
return Err(VerifyMonitorStatesError::UnexpectedState(state.id()));
}
}
let mut prev_fraction_completed = 0.0;
let mut state_index = 0;
for ordering_index in 0..ordering.len() {
// Check if it's out of order.
if states[state_index].id() != ordering[ordering_index] {
return Err(VerifyMonitorStatesError::OutOfOrder);
}
// Check progress.
while state_index < states.len() && states[state_index].id() == ordering[ordering_index]
{
if let Some(progress) = states[state_index].progress() {
// Verify we aren't decreasing.
if progress.fraction_completed() < prev_fraction_completed {
return Err(VerifyMonitorStatesError::ProgressDecreased);
}
prev_fraction_completed = progress.fraction_completed();
}
state_index += 1;
}
}
// The last progress should be 1.0 on success.
if expect_success {
let states_with_full_fraction_completion = states.iter().find(|state| {
if let Some(progress) = state.progress() {
return progress.fraction_completed() == 1.0;
}
false
});
if states_with_full_fraction_completion.is_none() {
return Err(VerifyMonitorStatesError::FractionCompletedSuccessNot1);
}
}
Ok(())
}
#[test]
fn fail_too_few_states() {
let states = vec![State::Prepare];
let ordering = vec![StateId::Prepare, StateId::Stage];
assert_eq!(
verify_monitor_states(&states, &ordering, true),
Err(VerifyMonitorStatesError::TooFewStates)
);
}
#[test]
#[should_panic]
fn fail_duplicates_in_ordering() {
let states = vec![State::Prepare, State::Prepare];
let ordering = vec![StateId::Prepare, StateId::Prepare];
verify_monitor_states(&states, &ordering, true).unwrap();
}
#[test]
fn fail_unexpected_state() {
let states = vec![State::Prepare, State::FailPrepare];
let ordering = vec![StateId::Prepare];
assert_eq!(
verify_monitor_states(&states, &ordering, true),
Err(VerifyMonitorStatesError::UnexpectedState(StateId::FailPrepare))
);
}
#[test]
fn fail_out_of_order() {
let states = vec![State::Prepare, State::FailPrepare];
let ordering = vec![StateId::FailPrepare, StateId::Prepare];
assert_eq!(
verify_monitor_states(&states, &ordering, true),
Err(VerifyMonitorStatesError::OutOfOrder)
);
}
#[test]
fn fail_progress_decreased() {
let info = UpdateInfo::builder().download_size(0).build();
let d0 = UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.0).bytes_downloaded(0).build())
.build();
let d1 = UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.4).bytes_downloaded(0).build())
.build();
let d2 = UpdateInfoAndProgress::builder()
.info(info)
.progress(Progress::builder().fraction_completed(0.2).bytes_downloaded(0).build())
.build();
let states = vec![State::Prepare, State::Fetch(d0), State::Fetch(d1), State::Fetch(d2)];
let ordering = vec![StateId::Prepare, StateId::Fetch];
assert_eq!(
verify_monitor_states(&states, &ordering, true),
Err(VerifyMonitorStatesError::ProgressDecreased)
);
}
#[test]
fn fail_fraction_completed_should_end_with_1_on_success() {
let info = UpdateInfo::builder().download_size(0).build();
let d0 = UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.0).bytes_downloaded(0).build())
.build();
let d1 = UpdateInfoAndProgress::builder()
.info(info)
.progress(Progress::builder().fraction_completed(0.9).bytes_downloaded(0).build())
.build();
let states = vec![State::Prepare, State::Fetch(d0), State::Fetch(d1)];
let ordering = vec![StateId::Prepare, StateId::Fetch];
assert_eq!(
verify_monitor_states(&states, &ordering, true),
Err(VerifyMonitorStatesError::FractionCompletedSuccessNot1)
);
// Sanity check failure method doesn't care if last fraction completed is not 1.0.
assert_eq!(verify_monitor_states(&states, &ordering, false), Ok(()));
}
#[test]
fn success() {
let info = UpdateInfo::builder().download_size(0).build();
let d0 = UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.0).bytes_downloaded(0).build())
.build();
let d1 = UpdateInfoAndProgress::builder()
.info(info.clone())
.progress(Progress::builder().fraction_completed(0.5).bytes_downloaded(0).build())
.build();
let d2 = UpdateInfoAndProgress::builder()
.info(info)
.progress(Progress::builder().fraction_completed(1.0).bytes_downloaded(0).build())
.build();
let states = vec![
State::Prepare,
State::Fetch(d0),
State::Fetch(d1.clone()),
State::Stage(d1),
State::Stage(d2.clone()),
State::Reboot(d2),
];
let ordering = vec![StateId::Prepare, StateId::Fetch, StateId::Stage, StateId::Reboot];
assert_eq!(verify_monitor_states(&states, &ordering, true), Ok(()));
}
}