| // 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 |
| } |
| } |