blob: 43665a53c1179dcaf9d18ccc6648bc953fbd00d0 [file] [log] [blame]
// Copyright 2025 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 component_events::events::{EventStream, ExitStatus, Stopped};
use component_events::matcher::EventMatcher;
use fidl_fuchsia_feedback::{
LastReboot, LastRebootInfoProviderMarker, LastRebootInfoProviderRequest,
LastRebootInfoProviderRequestStream, RebootReason,
};
use fuchsia_async as fasync;
use fuchsia_component::server as fserver;
use fuchsia_component_test::{
Capability, ChildOptions, ChildRef, LocalComponentHandles, RealmBuilder, RealmBuilderParams,
RealmInstance, Route,
};
use futures::{StreamExt, TryStreamExt};
use log::info;
/// Builds a test realm where the LastReboot info is `reason`.
async fn build_test_realm(reason: &LastReboot) -> RealmInstance {
info!("Building realm last_reboot={reason:?}");
let builder = RealmBuilder::with_params(
RealmBuilderParams::new().from_relative_url("#meta/test_realm.cm"),
)
.await
.unwrap();
let stub_last_reboot_info = StubLastRebootInfo::new(reason);
let fake_last_reboot_info_ref = builder
.add_local_child(
"fake_last_reboot_info",
move |handles| Box::pin(stub_last_reboot_info.clone().serve(handles)),
ChildOptions::new(),
)
.await
.unwrap();
builder
.add_route(
Route::new()
.capability(Capability::protocol::<LastRebootInfoProviderMarker>())
.from(&fake_last_reboot_info_ref)
.to(&ChildRef::from("kernel")),
)
.await
.unwrap();
builder.build().await.unwrap()
}
/// Reads an arbitrary file within the starnix realm.
async fn read_starnix_file(filename: &str, realm: &RealmInstance) -> String {
let read_flags = fuchsia_fs::PERM_READABLE;
let file = fuchsia_fs::directory::open_file(realm.root.get_exposed_dir(), filename, read_flags)
.await
.unwrap();
let contents = fuchsia_fs::file::read(&file).await.unwrap();
String::from_utf8(contents).unwrap()
}
#[fasync::run_singlethreaded(test)]
async fn test_no_errors_reboot_normal() {
let reboot = LastReboot {
graceful: Some(true),
uptime: Some(65000),
reason: Some(RebootReason::UserRequest),
..Default::default()
};
let realm_instance = build_test_realm(&reboot).await;
let cmdline = read_starnix_file("/fs_root/proc/cmdline", &realm_instance).await;
assert!(
cmdline.contains("androidboot.bootreason=reboot,userrequested"),
"cmdline ({cmdline}) has wrong bootreason"
);
}
#[fasync::run_singlethreaded(test)]
async fn test_kernel_panic() {
let reboot = LastReboot {
graceful: Some(false),
uptime: Some(65000),
reason: Some(RebootReason::KernelPanic),
..Default::default()
};
let realm_instance = build_test_realm(&reboot).await;
let cmdline = read_starnix_file("/fs_root/proc/cmdline", &realm_instance).await;
assert!(
cmdline.contains("androidboot.bootreason=kernel_panic"),
"cmdline ({cmdline}) has wrong bootreason"
);
}
#[fasync::run_singlethreaded(test)]
async fn test_pstore_present() {
let reboot = LastReboot {
graceful: Some(true),
uptime: Some(65000),
reason: Some(RebootReason::KernelPanic),
..Default::default()
};
let mut events = EventStream::open().await.unwrap();
let realm_instance = build_test_realm(&reboot).await;
let realm_moniker = format!("realm_builder:{}", realm_instance.root.child_name());
let mount_pstore_moniker = format!("{realm_moniker}/mount_pstore");
// Wait for /sys and /sys/fs/pstore to be mounted inside the starnix realm.
info!(mount_pstore_moniker:%; "waiting for mount_pstore to exit");
let stopped = EventMatcher::ok()
.moniker(&mount_pstore_moniker)
.wait::<Stopped>(&mut events)
.await
.unwrap();
let status = stopped.result().unwrap().status;
info!(status:?; "mount_pstore stopped");
assert_eq!(status, ExitStatus::Clean);
let console =
read_starnix_file("/fs_root/sys/fs/pstore/console-ramoops", &realm_instance).await;
assert!(
console.contains("Last Reboot Reason: "),
"console-ramoops ({console}) doesn't have the last reboot reason"
);
}
#[fasync::run_singlethreaded(test)]
async fn test_pstore_present_but_no_ramoops_created() {
let reboot = LastReboot {
graceful: Some(true),
uptime: Some(65000),
reason: Some(RebootReason::UserRequest),
..Default::default()
};
let mut events = EventStream::open().await.unwrap();
let realm_instance = build_test_realm(&reboot).await;
let realm_moniker = format!("realm_builder:{}", realm_instance.root.child_name());
let mount_pstore_moniker = format!("{realm_moniker}/mount_pstore");
// Wait for /sys and /sys/fs/pstore to be mounted inside the starnix realm.
info!(mount_pstore_moniker:%; "waiting for mount_pstore to exit");
let stopped = EventMatcher::ok()
.moniker(&mount_pstore_moniker)
.wait::<Stopped>(&mut events)
.await
.unwrap();
let status = stopped.result().unwrap().status;
info!(status:?; "mount_pstore stopped");
assert_eq!(status, ExitStatus::Clean);
let file_path = "/fs_root/sys/fs/pstore/console-ramoops";
let exposed_dir = realm_instance.root.get_exposed_dir();
let open_result =
fuchsia_fs::directory::open_file(exposed_dir, file_path, fuchsia_fs::PERM_READABLE).await;
assert!(
open_result.is_err(),
"console-ramoops should NOT exist, but open succeeded. File content if opened: {:?}",
open_result.map(|_| "...file was opened...").err()
);
}
#[derive(Clone)]
struct StubLastRebootInfo {
last_reboot: LastReboot,
}
impl StubLastRebootInfo {
fn new(last_reboot: &LastReboot) -> Self {
Self { last_reboot: last_reboot.clone() }
}
async fn serve(self, handles: LocalComponentHandles) -> Result<(), anyhow::Error> {
let mut fs = fserver::ServiceFs::new();
fs.dir("svc").add_fidl_service(|client: LastRebootInfoProviderRequestStream| client);
fs.serve_connection(handles.outgoing_dir)?;
fs.for_each_concurrent(0, |stream: LastRebootInfoProviderRequestStream| async {
stream
.try_for_each(|request| {
let last_reboot = self.last_reboot.clone();
async move {
match request {
LastRebootInfoProviderRequest::Get { responder } => {
responder.send(&last_reboot).expect("failed to send LastReboot");
}
}
Ok(())
}
})
.await
.expect("failed to serve LastReboot request stream");
})
.await;
Ok(())
}
}