blob: f79ec5b6001bb20d47e0726ad4527e7515130be7 [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.
#![cfg(test)]
use {
fidl_fuchsia_paver::Configuration,
fidl_fuchsia_sys::{LauncherProxy, TerminationReason},
fidl_fuchsia_update as fidl_update,
fidl_fuchsia_update_ext::{
InstallationErrorData, InstallationProgress, InstallingData, State, UpdateInfo,
},
fidl_fuchsia_update_installer as fidl_installer,
fidl_fuchsia_update_installer_ext::{self as installer},
fuchsia_async as fasync,
fuchsia_component::{
client::{AppBuilder, Output},
server::{NestedEnvironment, ServiceFs},
},
fuchsia_zircon::{self as zx, EventPair, HandleBased, Peered},
futures::prelude::*,
matches::assert_matches,
mock_installer::{
CapturedRebootControllerRequest, CapturedUpdateInstallerRequest, MockUpdateInstallerService,
},
mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent},
mock_reboot::{MockRebootService, RebootReason},
parking_lot::Mutex,
pretty_assertions::assert_eq,
std::sync::Arc,
};
async fn run_commit_status_provider_service(
mut stream: fidl_update::CommitStatusProviderRequestStream,
p: Arc<EventPair>,
) {
while let Some(req) = stream.try_next().await.unwrap() {
let fidl_update::CommitStatusProviderRequest::IsCurrentSystemCommitted { responder } = req;
let pair = p.duplicate_handle(zx::Rights::BASIC).unwrap();
let () = responder.send(pair).unwrap();
}
}
#[derive(Default)]
struct TestEnvBuilder {
manager_states: Vec<State>,
installer_states: Vec<installer::State>,
commit_status_provider_response: Option<EventPair>,
paver_service: Option<MockPaverService>,
reboot_service: Option<MockRebootService>,
}
impl TestEnvBuilder {
fn manager_states(self, manager_states: Vec<State>) -> Self {
Self { manager_states, ..self }
}
fn installer_states(self, installer_states: Vec<installer::State>) -> Self {
Self { installer_states, ..self }
}
fn commit_status_provider_response(self, response: EventPair) -> Self {
Self { commit_status_provider_response: Some(response), ..self }
}
fn paver_service(self, paver_service: MockPaverService) -> Self {
Self { paver_service: Some(paver_service), ..self }
}
fn reboot_service(self, reboot_service: MockRebootService) -> Self {
Self { reboot_service: Some(reboot_service), ..self }
}
fn build(self) -> TestEnv {
let mut fs = ServiceFs::new();
let update_manager = Arc::new(MockUpdateManagerService::new(self.manager_states));
let update_manager_clone = Arc::clone(&update_manager);
fs.add_fidl_service(move |stream| {
fasync::Task::spawn(Arc::clone(&update_manager_clone).run_service(stream)).detach()
});
let update_installer =
Arc::new(MockUpdateInstallerService::with_states(self.installer_states));
let update_installer_clone = Arc::clone(&update_installer);
fs.add_fidl_service(move |stream| {
fasync::Task::spawn(Arc::clone(&update_installer_clone).run_service(stream)).detach()
});
if let Some(response) = self.commit_status_provider_response {
let response = Arc::new(response);
fs.add_fidl_service(move |stream| {
fasync::Task::spawn(run_commit_status_provider_service(
stream,
Arc::clone(&response),
))
.detach()
});
}
if let Some(paver_service) = self.paver_service {
let paver_service = Arc::new(paver_service);
fs.add_fidl_service(move |stream| {
fasync::Task::spawn(
Arc::clone(&paver_service)
.run_paver_service(stream)
.unwrap_or_else(|e| panic!("error running paver service: {:?}", e)),
)
.detach()
});
}
if let Some(reboot_service) = self.reboot_service {
let reboot_service = Arc::new(reboot_service);
fs.add_fidl_service(move |stream| {
fasync::Task::spawn(
Arc::clone(&reboot_service)
.run_reboot_service(stream)
.unwrap_or_else(|e| panic!("error running reboot service: {:?}", e)),
)
.detach()
});
}
let env = fs
.create_salted_nested_environment("update_env")
.expect("nested environment to create successfully");
fasync::Task::spawn(fs.collect()).detach();
TestEnv { env, update_manager, update_installer }
}
}
struct TestEnv {
env: NestedEnvironment,
update_manager: Arc<MockUpdateManagerService>,
update_installer: Arc<MockUpdateInstallerService>,
}
impl TestEnv {
fn builder() -> TestEnvBuilder {
TestEnvBuilder::default()
}
fn launcher(&self) -> &LauncherProxy {
self.env.launcher()
}
fn new() -> Self {
Self::builder().build()
}
async fn run_update<'a>(&'a self, args: Vec<&'a str>) -> Output {
let launcher = self.launcher();
let update =
AppBuilder::new("fuchsia-pkg://fuchsia.com/update-integration-tests#meta/update.cmx")
.args(args);
let output = update
.output(launcher)
.expect("update to launch")
.await
.expect("no errors while waiting for exit");
assert_eq!(output.exit_status.reason(), TerminationReason::Exited);
output
}
fn assert_update_manager_called_with(&self, expected_args: Vec<CapturedUpdateManagerRequest>) {
assert_eq!(*self.update_manager.captured_args.lock(), expected_args);
}
fn assert_update_installer_called_with(
&self,
expected_args: Vec<CapturedUpdateInstallerRequest>,
) {
self.update_installer.assert_installer_called_with(expected_args);
}
fn assert_reboot_controller_called_with(
&self,
expected_requests: Vec<CapturedRebootControllerRequest>,
) {
self.update_installer.assert_reboot_controller_called_with(expected_requests);
}
}
#[derive(PartialEq, Debug)]
enum CapturedUpdateManagerRequest {
CheckNow { options: fidl_update::CheckOptions, monitor_present: bool },
}
// fidl_update::CheckOptions does not impl Eq, but it is semantically Eq.
impl Eq for CapturedUpdateManagerRequest {}
struct MockUpdateManagerService {
states: Vec<State>,
captured_args: Mutex<Vec<CapturedUpdateManagerRequest>>,
check_now_response: Mutex<Result<(), fidl_update::CheckNotStartedReason>>,
}
impl MockUpdateManagerService {
fn new(states: Vec<State>) -> Self {
Self { states, captured_args: Mutex::new(vec![]), check_now_response: Mutex::new(Ok(())) }
}
async fn run_service(self: Arc<Self>, mut stream: fidl_update::ManagerRequestStream) {
while let Some(req) = stream.try_next().await.unwrap() {
match req {
fidl_update::ManagerRequest::CheckNow { options, monitor, responder } => {
self.captured_args.lock().push(CapturedUpdateManagerRequest::CheckNow {
options,
monitor_present: monitor.is_some(),
});
if let Some(monitor) = monitor {
let proxy = fidl_update::MonitorProxy::new(
fasync::Channel::from_channel(monitor.into_channel()).unwrap(),
);
fasync::Task::spawn(Self::send_states(proxy, self.states.clone())).detach();
}
responder.send(&mut *self.check_now_response.lock()).unwrap();
}
fidl_update::ManagerRequest::PerformPendingReboot { responder: _ } => {
panic!("update tool should not be calling perform pending reboot!");
}
}
}
}
async fn send_states(monitor: fidl_update::MonitorProxy, states: Vec<State>) {
for state in states.into_iter() {
monitor.on_state(&mut state.into()).await.unwrap();
}
}
}
fn assert_output(output: &Output, expected_stdout: &str, expected_stderr: &str, exit_code: i64) {
assert_eq!(output.exit_status.reason(), fidl_fuchsia_sys::TerminationReason::Exited);
let actual_stdout = std::str::from_utf8(&output.stdout).unwrap();
assert_eq!(actual_stdout, expected_stdout);
let actual_stderr = std::str::from_utf8(&output.stderr).unwrap();
assert_eq!(actual_stderr, expected_stderr);
assert_eq!(output.exit_status.code(), exit_code, "stdout: {}", actual_stdout);
}
#[fasync::run_singlethreaded(test)]
async fn force_install_fails_on_invalid_pkg_url() {
let env = TestEnv::new();
let output =
env.run_update(vec!["force-install", "not-fuchsia-pkg://fuchsia.com/update"]).await;
assert_matches!(output.exit_status.ok(), Err(_));
let stderr = std::str::from_utf8(&output.stderr).unwrap();
assert!(stderr.contains("Error: parsing update package url"), "stderr: {}", stderr);
env.assert_update_installer_called_with(vec![]);
env.assert_reboot_controller_called_with(vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn force_install_reboot() {
let update_info = installer::UpdateInfo::builder().download_size(1000).build();
let env = TestEnv::builder()
.installer_states(vec![
installer::State::Prepare,
installer::State::Fetch(
installer::UpdateInfoAndProgress::new(update_info, installer::Progress::none())
.unwrap(),
),
installer::State::Stage(
installer::UpdateInfoAndProgress::new(
update_info,
installer::Progress::builder()
.fraction_completed(0.5)
.bytes_downloaded(500)
.build(),
)
.unwrap(),
),
installer::State::WaitToReboot(installer::UpdateInfoAndProgress::done(update_info)),
installer::State::Reboot(installer::UpdateInfoAndProgress::done(update_info)),
])
.build();
let output = env.run_update(vec!["force-install", "fuchsia-pkg://fuchsia.com/update"]).await;
assert_output(
&output,
"Installing an update.\n\
State: Prepare\n\
State: Fetch(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 0.0, bytes_downloaded: 0 } })\n\
State: Stage(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 0.5, bytes_downloaded: 500 } })\n\
State: WaitToReboot(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 1.0, bytes_downloaded: 1000 } })\n",
"",
0,
);
env.assert_update_installer_called_with(vec![CapturedUpdateInstallerRequest::StartUpdate {
url: "fuchsia-pkg://fuchsia.com/update".into(),
options: fidl_installer::Options {
initiator: Some(fidl_installer::Initiator::User),
should_write_recovery: Some(true),
allow_attach_to_existing_attempt: Some(true),
..fidl_installer::Options::EMPTY
},
reboot_controller_present: true,
}]);
env.assert_reboot_controller_called_with(vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn force_install_no_reboot() {
let update_info = installer::UpdateInfo::builder().download_size(1000).build();
let env = TestEnv::builder()
.installer_states(vec![
installer::State::Prepare,
installer::State::Fetch(
installer::UpdateInfoAndProgress::new(update_info, installer::Progress::none())
.unwrap(),
),
installer::State::Stage(
installer::UpdateInfoAndProgress::new(
update_info,
installer::Progress::builder()
.fraction_completed(0.5)
.bytes_downloaded(500)
.build(),
)
.unwrap(),
),
installer::State::WaitToReboot(installer::UpdateInfoAndProgress::done(update_info)),
installer::State::DeferReboot(installer::UpdateInfoAndProgress::done(update_info)),
])
.build();
let output = env
.run_update(vec!["force-install", "fuchsia-pkg://fuchsia.com/update", "--reboot", "false"])
.await;
assert_output(
&output,
"Installing an update.\n\
State: Prepare\n\
State: Fetch(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 0.0, bytes_downloaded: 0 } })\n\
State: Stage(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 0.5, bytes_downloaded: 500 } })\n\
State: WaitToReboot(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 1.0, bytes_downloaded: 1000 } })\n\
State: DeferReboot(UpdateInfoAndProgress { info: UpdateInfo { download_size: 1000 }, progress: Progress { fraction_completed: 1.0, bytes_downloaded: 1000 } })\n",
"",
0,
);
env.assert_update_installer_called_with(vec![CapturedUpdateInstallerRequest::StartUpdate {
url: "fuchsia-pkg://fuchsia.com/update".into(),
options: fidl_installer::Options {
initiator: Some(fidl_installer::Initiator::User),
should_write_recovery: Some(true),
allow_attach_to_existing_attempt: Some(true),
..fidl_installer::Options::EMPTY
},
reboot_controller_present: true,
}]);
env.assert_reboot_controller_called_with(vec![CapturedRebootControllerRequest::Detach]);
}
#[fasync::run_singlethreaded(test)]
async fn force_install_failure_state() {
let env = TestEnv::builder()
.installer_states(vec![
installer::State::Prepare,
installer::State::FailPrepare(installer::PrepareFailureReason::Internal),
])
.build();
let output = env.run_update(vec!["force-install", "fuchsia-pkg://fuchsia.com/update"]).await;
assert_output(
&output,
"Installing an update.\n\
State: Prepare\n\
State: FailPrepare(Internal)\n",
"Error: Encountered failure state\n",
1,
);
env.assert_update_installer_called_with(vec![CapturedUpdateInstallerRequest::StartUpdate {
url: "fuchsia-pkg://fuchsia.com/update".into(),
options: fidl_installer::Options {
initiator: Some(fidl_installer::Initiator::User),
should_write_recovery: Some(true),
allow_attach_to_existing_attempt: Some(true),
..fidl_installer::Options::EMPTY
},
reboot_controller_present: true,
}]);
env.assert_reboot_controller_called_with(vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn force_install_unexpected_end() {
let env = TestEnv::builder().installer_states(vec![installer::State::Prepare]).build();
let output = env.run_update(vec!["force-install", "fuchsia-pkg://fuchsia.com/update"]).await;
assert_output(
&output,
"Installing an update.\n\
State: Prepare\n",
"Error: Installation ended unexpectedly\n",
1,
);
env.assert_update_installer_called_with(vec![CapturedUpdateInstallerRequest::StartUpdate {
url: "fuchsia-pkg://fuchsia.com/update".into(),
options: fidl_installer::Options {
initiator: Some(fidl_installer::Initiator::User),
should_write_recovery: Some(true),
allow_attach_to_existing_attempt: Some(true),
..fidl_installer::Options::EMPTY
},
reboot_controller_present: true,
}]);
env.assert_reboot_controller_called_with(vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn force_install_service_initiated_flag() {
let env = TestEnv::new();
let _output = env
.run_update(vec![
"force-install",
"fuchsia-pkg://fuchsia.com/update",
"--service-initiated",
])
.await;
env.assert_update_installer_called_with(vec![CapturedUpdateInstallerRequest::StartUpdate {
url: "fuchsia-pkg://fuchsia.com/update".into(),
options: fidl_installer::Options {
initiator: Some(fidl_installer::Initiator::Service),
should_write_recovery: Some(true),
allow_attach_to_existing_attempt: Some(true),
..fidl_installer::Options::EMPTY
},
reboot_controller_present: true,
}]);
}
#[fasync::run_singlethreaded(test)]
async fn check_now_service_initiated_flag() {
let env = TestEnv::new();
let output = env.run_update(vec!["check-now", "--service-initiated"]).await;
assert_output(&output, "Checking for an update.\n", "", 0);
env.assert_update_manager_called_with(vec![CapturedUpdateManagerRequest::CheckNow {
options: fidl_update::CheckOptions {
initiator: Some(fidl_update::Initiator::Service),
allow_attaching_to_existing_update_check: Some(true),
..fidl_update::CheckOptions::EMPTY
},
monitor_present: false,
}]);
}
#[fasync::run_singlethreaded(test)]
async fn check_now_error_if_throttled() {
let env = TestEnv::new();
*env.update_manager.check_now_response.lock() =
Err(fidl_update::CheckNotStartedReason::Throttled);
let output = env.run_update(vec!["check-now"]).await;
assert_output(&output, "", "Error: Update check failed to start: Throttled\n", 1);
env.assert_update_manager_called_with(vec![CapturedUpdateManagerRequest::CheckNow {
options: fidl_update::CheckOptions {
initiator: Some(fidl_update::Initiator::User),
allow_attaching_to_existing_update_check: Some(true),
..fidl_update::CheckOptions::EMPTY
},
monitor_present: false,
}]);
}
#[fasync::run_singlethreaded(test)]
async fn check_now_monitor_flag() {
let env = TestEnv::builder()
.manager_states(vec![
State::CheckingForUpdates,
State::InstallingUpdate(InstallingData {
update: Some(UpdateInfo {
version_available: Some("fake-versions".into()),
download_size: Some(4),
}),
installation_progress: Some(InstallationProgress {
fraction_completed: Some(0.5f32),
}),
}),
])
.build();
let output = env.run_update(vec!["check-now", "--monitor"]).await;
assert_output(
&output,
"Checking for an update.\n\
State: CheckingForUpdates\n\
State: InstallingUpdate(InstallingData { update: Some(UpdateInfo { version_available: Some(\"fake-versions\"), download_size: Some(4) }), installation_progress: Some(InstallationProgress { fraction_completed: Some(0.5) }) })\n",
"",
0,
);
env.assert_update_manager_called_with(vec![CapturedUpdateManagerRequest::CheckNow {
options: fidl_update::CheckOptions {
initiator: Some(fidl_update::Initiator::User),
allow_attaching_to_existing_update_check: Some(true),
..fidl_update::CheckOptions::EMPTY
},
monitor_present: true,
}]);
}
#[fasync::run_singlethreaded(test)]
async fn check_now_monitor_error_checking() {
let env = TestEnv::builder()
.manager_states(vec![State::CheckingForUpdates, State::ErrorCheckingForUpdate])
.build();
let output = env.run_update(vec!["check-now", "--monitor"]).await;
assert_output(
&output,
"Checking for an update.\n\
State: CheckingForUpdates\n",
"Error: Update failed: ErrorCheckingForUpdate\n",
1,
);
env.assert_update_manager_called_with(vec![CapturedUpdateManagerRequest::CheckNow {
options: fidl_update::CheckOptions {
initiator: Some(fidl_update::Initiator::User),
allow_attaching_to_existing_update_check: Some(true),
..fidl_update::CheckOptions::EMPTY
},
monitor_present: true,
}]);
}
#[fasync::run_singlethreaded(test)]
async fn check_now_monitor_error_installing() {
let env = TestEnv::builder()
.manager_states(vec![
State::CheckingForUpdates,
State::InstallingUpdate(InstallingData {
update: Some(UpdateInfo {
version_available: Some("fake-versions".into()),
download_size: Some(4),
}),
installation_progress: Some(InstallationProgress {
fraction_completed: Some(0.5f32),
}),
}),
State::InstallationError(InstallationErrorData {
update: Some(UpdateInfo {
version_available: Some("fake-versions".into()),
download_size: Some(4),
}),
installation_progress: Some(InstallationProgress {
fraction_completed: Some(0.5f32),
}),
}),
])
.build();
let output = env.run_update(vec!["check-now", "--monitor"]).await;
assert_output(
&output,
"Checking for an update.\n\
State: CheckingForUpdates\n\
State: InstallingUpdate(InstallingData { update: Some(UpdateInfo { version_available: Some(\"fake-versions\"), download_size: Some(4) }), installation_progress: Some(InstallationProgress { fraction_completed: Some(0.5) }) })\n",
"Error: Update failed: InstallationError(InstallationErrorData { update: Some(UpdateInfo { version_available: Some(\"fake-versions\"), download_size: Some(4) }), installation_progress: Some(InstallationProgress { fraction_completed: Some(0.5) }) })\n",
1,
);
env.assert_update_manager_called_with(vec![CapturedUpdateManagerRequest::CheckNow {
options: fidl_update::CheckOptions {
initiator: Some(fidl_update::Initiator::User),
allow_attaching_to_existing_update_check: Some(true),
..fidl_update::CheckOptions::EMPTY
},
monitor_present: true,
}]);
}
#[fasync::run_singlethreaded(test)]
async fn wait_for_commit_success() {
let (p0, p1) = EventPair::create().unwrap();
let env = TestEnv::builder().commit_status_provider_response(p1).build();
let () = p0.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).unwrap();
let output = env.run_update(vec!["wait-for-commit"]).await;
assert_output(
&output,
"Waiting for commit.\n\
Committed!\n",
"",
0,
);
}
#[fasync::run_singlethreaded(test)]
async fn revert_success() {
#[derive(Debug, PartialEq)]
enum Interaction {
Paver(PaverEvent),
Reboot(RebootReason),
}
let interactions = Arc::new(Mutex::new(vec![]));
let env = TestEnv::builder()
.paver_service({
let interactions = Arc::clone(&interactions);
MockPaverServiceBuilder::new()
.event_hook(move |event| {
interactions.lock().push(Interaction::Paver(event.clone()));
})
.build()
})
.reboot_service({
let interactions = Arc::clone(&interactions);
MockRebootService::new(Box::new(move |reason| {
interactions.lock().push(Interaction::Reboot(reason));
Ok(())
}))
})
.build();
let output = env.run_update(vec!["revert"]).await;
assert_output(&output, "Reverting the update.\n", "", 0);
assert_eq!(
interactions.lock().as_slice(),
&[
Interaction::Paver(PaverEvent::QueryCurrentConfiguration),
Interaction::Paver(PaverEvent::SetConfigurationUnbootable {
configuration: Configuration::A
}),
Interaction::Paver(PaverEvent::BootManagerFlush),
Interaction::Reboot(RebootReason::UserRequest)
]
);
}