blob: 2f242bda0c63c45a8a5fa1f94b75d36386602f03 [file] [log] [blame]
// Copyright 2022 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.
#![allow(clippy::let_unit_value)]
use {
anyhow::{Context, Error},
async_trait::async_trait,
fidl::endpoints::{ClientEnd, Proxy},
fidl::endpoints::{DiscoverableProtocolMarker, ServerEnd},
fidl_fuchsia_io as fio,
fidl_fuchsia_io::DirectoryProxy,
fidl_fuchsia_paver::PaverRequestStream,
fidl_fuchsia_pkg_ext::RepositoryConfigs,
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_component_test::LocalComponentHandles,
fuchsia_merkle::Hash,
fuchsia_pkg_testing::{serve::ServedRepository, Package, RepositoryBuilder},
fuchsia_sync::Mutex,
futures::prelude::*,
isolated_ota::{OmahaConfig, UpdateUrlSource},
mock_omaha_server::{
OmahaResponse, OmahaServer, OmahaServerBuilder, ResponseAndMetadata, ResponseMap,
},
mock_paver::{MockPaverService, MockPaverServiceBuilder},
std::{
collections::{BTreeMap, BTreeSet},
io::Write,
str::FromStr,
sync::Arc,
},
tempfile::TempDir,
};
pub const GLOBAL_SSL_CERTS_PATH: &str = "/config/ssl";
const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
const TEST_CERTS_PATH: &str = "/pkg/data/ssl";
const TEST_REPO_URL: &str = "fuchsia-pkg://integration.test.fuchsia.com";
pub enum OmahaState {
/// Don't use Omaha for this update, instead use the provided, or default, update package URL.
Disabled(Option<fuchsia_url::AbsolutePackageUrl>),
/// Set up an Omaha server automatically.
Auto(OmahaResponse),
/// Pass the given OmahaConfig to Omaha.
Manual(OmahaConfig),
}
pub struct TestParams {
pub blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
pub board: String,
pub channel: String,
pub expected_blobfs_contents: BTreeSet<Hash>,
pub paver: Arc<MockPaverService>,
pub repo_config_dir: TempDir,
pub ssl_certs: DirectoryProxy,
pub update_merkle: Hash,
pub version: String,
pub update_url_source: UpdateUrlSource,
pub paver_connector: ClientEnd<fio::DirectoryMarker>,
}
/// Connects the local component to a mock paver.
///
/// Unlike other mocks, the `fuchsia.paver.Paver` is serviced by [`isolated_ota_env::TestEnv`], so
/// this function proxies to the given `paver_dir_proxy` which is expected to host a
/// file named "fuchsia.paver.Paver" which implements the `fuchsia.paver.Paver` FIDL protocol.
pub async fn expose_mock_paver(
handles: LocalComponentHandles,
paver_dir_proxy: fio::DirectoryProxy,
) -> Result<(), Error> {
let mut fs = ServiceFs::new();
fs.dir("svc").add_service_connector(
move |server_end: ServerEnd<fidl_fuchsia_paver::PaverMarker>| {
fdio::service_connect_at(
paver_dir_proxy.as_channel().as_ref(),
&format!("/{}", fidl_fuchsia_paver::PaverMarker::PROTOCOL_NAME),
server_end.into_channel(),
)
.expect("failed to connect to paver service node");
},
);
fs.serve_connection(handles.outgoing_dir).expect("failed to serve paver fs connection");
fs.collect::<()>().await;
Ok(())
}
#[async_trait(?Send)]
pub trait TestExecutor<R> {
async fn run(&self, params: TestParams) -> R;
}
pub struct TestEnvBuilder<R> {
blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
board: String,
channel: String,
omaha: OmahaState,
packages: Vec<Package>,
paver: MockPaverServiceBuilder,
repo_config: Option<RepositoryConfigs>,
version: String,
test_executor: Option<Box<dyn TestExecutor<R>>>,
// The zbi and optional vbmeta contents.
fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
// The zbi and optional vbmeta contents of the recovery partition.
recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
firmware_images: BTreeMap<String, Vec<u8>>,
}
impl<R> TestEnvBuilder<R> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
TestEnvBuilder {
blobfs: None,
board: "test-board".to_owned(),
channel: "test".to_owned(),
omaha: OmahaState::Disabled(Some(fuchsia_url::AbsolutePackageUrl::new(
TEST_REPO_URL.parse().unwrap(),
"update".parse().unwrap(),
None,
None,
))),
packages: vec![],
paver: MockPaverServiceBuilder::new(),
repo_config: None,
version: "0.1.2.3".to_owned(),
test_executor: None,
fuchsia_image: None,
recovery_image: None,
firmware_images: BTreeMap::new(),
}
}
/// Add a package to the repository generated by this TestEnvBuilder.
/// The package will also be listed in the generated update package
/// so that it will be downloaded as part of the OTA.
pub fn add_package(mut self, pkg: Package) -> Self {
self.packages.push(pkg);
self
}
pub fn blobfs(mut self, client: ClientEnd<fio::DirectoryMarker>) -> Self {
self.blobfs = Some(client);
self
}
/// Provide a TUF repository configuration to the package resolver.
/// This will override the repository that the builder would otherwise generate.
pub fn repo_config(mut self, repo: RepositoryConfigs) -> Self {
self.repo_config = Some(repo);
self
}
/// Enable/disable Omaha. OmahaState::Auto will automatically set up an Omaha server and tell
/// the updater to use it.
pub fn omaha_state(mut self, state: OmahaState) -> Self {
self.omaha = state;
self
}
/// Mutate the MockPaverServiecBuilder used by this TestEnvBuilder.
pub fn paver<F>(mut self, func: F) -> Self
where
F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
{
self.paver = func(self.paver);
self
}
pub fn test_executor(mut self, executor: Box<dyn TestExecutor<R>>) -> Self {
self.test_executor = Some(executor);
self
}
/// The zbi and optional vbmeta images to write.
pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
assert_eq!(self.fuchsia_image, None);
self.fuchsia_image = Some((zbi, vbmeta));
self
}
/// The zbi and optional vbmeta images to write to the recovery partition.
pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
assert_eq!(self.recovery_image, None);
self.recovery_image = Some((zbi, vbmeta));
self
}
/// A firmware image to write.
pub fn firmware_image(mut self, type_: String, content: Vec<u8>) -> Self {
assert_eq!(self.firmware_images.insert(type_, content), None);
self
}
/// Turn this |TestEnvBuilder| into a |TestEnv|
pub async fn build(mut self) -> Result<TestEnv<R>, Error> {
let (repo_config, served_repo, ssl_certs, expected_blobfs_contents, merkle) = if self
.repo_config
.is_none()
{
// If no repo config was specified, host a repo containing the provided packages,
// and an update package containing given images + all packages in the repo.
let mut update =
fuchsia_pkg_testing::UpdatePackageBuilder::new(TEST_REPO_URL.parse().unwrap())
.packages(
self.packages
.iter()
.map(|p| {
fuchsia_url::PinnedAbsolutePackageUrl::new(
TEST_REPO_URL.parse().unwrap(),
p.name().clone(),
None,
*p.hash(),
)
})
.collect::<Vec<_>>(),
)
.firmware_images(self.firmware_images);
if let Some((zbi, vbmeta)) = self.fuchsia_image {
update = update.fuchsia_image(zbi, vbmeta);
}
if let Some((zbi, vbmeta)) = self.recovery_image {
update = update.recovery_image(zbi, vbmeta);
}
let (update, images) = update.build().await;
// Do not include the images package, system-updater triggers GC after resolving it.
let expected_blobfs_contents = self
.packages
.iter()
.chain([update.as_package()])
.flat_map(|p| p.list_blobs())
.collect();
let repo = Arc::new(
self.packages
.iter()
.chain([update.as_package(), &images])
.fold(
RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
.add_package(update.as_package()),
|repo, package| repo.add_package(package),
)
.build()
.await
.expect("build repo"),
);
let served_repo = Arc::clone(&repo).server().start().expect("serve repo");
let config = RepositoryConfigs::Version1(vec![
served_repo.make_repo_config(TEST_REPO_URL.parse().expect("make repo config"))
]);
let update_merkle = *update.as_package().hash();
// Add the update package to the list of packages, so that TestResult::check_packages
// will expect to see the update package's blobs in blobfs.
let mut packages = vec![update.into_package()];
packages.append(&mut self.packages);
(
config,
Some(served_repo),
fuchsia_fs::directory::open_in_namespace(
TEST_CERTS_PATH,
fio::OpenFlags::RIGHT_READABLE,
)
.unwrap(),
expected_blobfs_contents,
update_merkle,
)
} else {
// Use the provided repo config. Assume that this means we'll actually want to use
// real SSL certificates, and that we don't need to host our own repository.
(
self.repo_config.unwrap(),
None,
fuchsia_fs::directory::open_in_namespace(
GLOBAL_SSL_CERTS_PATH,
fio::OpenFlags::RIGHT_READABLE,
)
.unwrap(),
BTreeSet::new(),
Hash::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
.expect("make merkle"),
)
};
let dir = tempfile::tempdir()?;
let mut path = dir.path().to_owned();
path.push("repo_config.json");
let path = path.as_path();
let mut file =
std::io::BufWriter::new(std::fs::File::create(path).context("creating file")?);
serde_json::to_writer(&mut file, &repo_config).unwrap();
file.flush().unwrap();
Ok(TestEnv {
blobfs: self.blobfs,
board: self.board,
channel: self.channel,
omaha: self.omaha,
expected_blobfs_contents,
paver: Arc::new(self.paver.build()),
_repo: served_repo,
repo_config_dir: dir,
ssl_certs,
update_merkle: merkle,
version: self.version,
test_executor: self.test_executor.expect("test executor must be set"),
})
}
}
pub struct TestEnv<R> {
blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
channel: String,
omaha: OmahaState,
expected_blobfs_contents: BTreeSet<Hash>,
paver: Arc<MockPaverService>,
_repo: Option<ServedRepository>,
repo_config_dir: tempfile::TempDir,
ssl_certs: DirectoryProxy,
update_merkle: Hash,
board: String,
version: String,
test_executor: Box<dyn TestExecutor<R>>,
}
impl<R> TestEnv<R> {
fn start_omaha(omaha: OmahaState, merkle: Hash) -> Result<UpdateUrlSource, Error> {
match omaha {
OmahaState::Disabled(url) => Ok(match url {
Some(url) => UpdateUrlSource::UpdateUrl(url),
None => UpdateUrlSource::UseDefault,
}),
OmahaState::Manual(cfg) => Ok(UpdateUrlSource::OmahaConfig(cfg)),
OmahaState::Auto(response) => {
let server = OmahaServerBuilder::default()
.responses_by_appid(
vec![(
"integration-test-appid".to_string(),
ResponseAndMetadata { response, merkle, ..Default::default() },
)]
.into_iter()
.collect::<ResponseMap>(),
)
.build()
.unwrap();
let addr = OmahaServer::start(Arc::new(Mutex::new(server)))
.context("Starting omaha server")?;
let config =
OmahaConfig { app_id: "integration-test-appid".to_owned(), server_url: addr };
Ok(UpdateUrlSource::OmahaConfig(config))
}
}
}
/// Run the update, consuming this |TestEnv| and returning a |TestResult|.
pub async fn run(self) -> R {
let update_url_source = TestEnv::<R>::start_omaha(self.omaha, self.update_merkle)
.expect("Starting Omaha server");
let mut service_fs = ServiceFs::new();
let paver_clone = Arc::clone(&self.paver);
service_fs.add_fidl_service(move |stream: PaverRequestStream| {
fasync::Task::spawn(
Arc::clone(&paver_clone)
.run_paver_service(stream)
.unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
)
.detach();
});
let (client, server) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>();
service_fs
.serve_connection(server.into_channel().into())
.expect("Failed to serve connection");
fasync::Task::spawn(service_fs.collect()).detach();
let params = TestParams {
blobfs: self.blobfs,
board: self.board,
channel: self.channel,
expected_blobfs_contents: self.expected_blobfs_contents,
paver: self.paver,
repo_config_dir: self.repo_config_dir,
ssl_certs: self.ssl_certs,
update_merkle: self.update_merkle,
version: self.version,
update_url_source,
paver_connector: client,
};
self.test_executor.run(params).await
}
}