1#![allow(clippy::let_unit_value)]
6
7use anyhow::{Context, Error};
8use async_trait::async_trait;
9use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, Proxy, ServerEnd};
10use fidl_fuchsia_io as fio;
11use fidl_fuchsia_paver::PaverRequestStream;
12use fidl_fuchsia_pkg_ext::RepositoryConfigs;
13use fuchsia_async as fasync;
14use fuchsia_component::server::ServiceFs;
15use fuchsia_component_test::LocalComponentHandles;
16use fuchsia_merkle::Hash;
17use fuchsia_pkg_testing::serve::ServedRepository;
18use fuchsia_pkg_testing::{Package, RepositoryBuilder};
19use futures::prelude::*;
20use isolated_ota::{OmahaConfig, UpdateUrlSource};
21use mock_omaha_server::{
22 OmahaResponse, OmahaServer, OmahaServerBuilder, ResponseAndMetadata, ResponseMap,
23};
24use mock_omaha_server_fuchsia::OmahaServerExt as _;
25use mock_paver::{MockPaverService, MockPaverServiceBuilder};
26use std::collections::{BTreeMap, BTreeSet};
27use std::io::Write;
28use std::str::FromStr;
29use std::sync::{Arc, Mutex};
30use tempfile::TempDir;
31
32const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
33const TEST_REPO_URL: &str = "fuchsia-pkg://integration.test.fuchsia.com";
34
35pub enum OmahaState {
36 Disabled(Option<http::Uri>),
38 Auto(OmahaResponse),
40 Manual(OmahaConfig),
42}
43
44pub struct TestParams {
45 pub blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
46 pub board: String,
47 pub channel: String,
48 pub expected_blobfs_contents: BTreeSet<Hash>,
49 pub paver: Arc<MockPaverService>,
50 pub repo_config_dir: TempDir,
51 pub update_merkle: Hash,
52 pub version: String,
53 pub update_url_source: UpdateUrlSource,
54 pub paver_connector: ClientEnd<fio::DirectoryMarker>,
55 pub system_image_hash: Option<Hash>,
56}
57
58pub async fn expose_mock_paver(
64 handles: LocalComponentHandles,
65 paver_dir_proxy: fio::DirectoryProxy,
66) -> Result<(), Error> {
67 let mut fs = ServiceFs::new();
68
69 fs.dir("svc").add_service_connector(
70 move |server_end: ServerEnd<fidl_fuchsia_paver::PaverMarker>| {
71 fdio::service_connect_at(
72 paver_dir_proxy.as_channel().as_ref(),
73 &format!("/{}", fidl_fuchsia_paver::PaverMarker::PROTOCOL_NAME),
74 server_end.into_channel(),
75 )
76 .expect("failed to connect to paver service node");
77 },
78 );
79
80 fs.serve_connection(handles.outgoing_dir).expect("failed to serve paver fs connection");
81 fs.collect::<()>().await;
82 Ok(())
83}
84
85#[async_trait(?Send)]
86pub trait TestExecutor<R> {
87 async fn run(&self, params: TestParams) -> R;
88}
89
90pub struct TestEnvBuilder<R> {
91 blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
92 board: String,
93 channel: String,
94 omaha: OmahaState,
95 packages: Vec<Package>,
96 paver: MockPaverServiceBuilder,
97 repo_config: Option<RepositoryConfigs>,
98 version: String,
99 test_executor: Option<Box<dyn TestExecutor<R>>>,
100 fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
102 recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
104 firmware_images: BTreeMap<String, Vec<u8>>,
105 system_image_hash: Option<Hash>,
106}
107
108impl<R> TestEnvBuilder<R> {
109 #[allow(clippy::new_without_default)]
110 pub fn new() -> Self {
111 TestEnvBuilder {
112 blobfs: None,
113 board: "test-board".to_owned(),
114 channel: "test".to_owned(),
115 omaha: OmahaState::Disabled(Some(format!("{TEST_REPO_URL}/update").parse().unwrap())),
116 packages: vec![],
117 paver: MockPaverServiceBuilder::new(),
118 repo_config: None,
119 version: "0.1.2.3".to_owned(),
120 test_executor: None,
121 fuchsia_image: None,
122 recovery_image: None,
123 firmware_images: BTreeMap::new(),
124 system_image_hash: None,
125 }
126 }
127
128 pub fn add_package(mut self, pkg: Package) -> Self {
132 self.packages.push(pkg);
133 self
134 }
135
136 pub fn blobfs(mut self, client: ClientEnd<fio::DirectoryMarker>) -> Self {
137 self.blobfs = Some(client);
138 self
139 }
140
141 pub fn repo_config(mut self, repo: RepositoryConfigs) -> Self {
144 self.repo_config = Some(repo);
145 self
146 }
147
148 pub fn system_image_hash(mut self, hash: Hash) -> Self {
149 self.system_image_hash = Some(hash);
150 self
151 }
152
153 pub fn omaha_state(mut self, state: OmahaState) -> Self {
156 self.omaha = state;
157 self
158 }
159
160 pub fn paver<F>(mut self, func: F) -> Self
162 where
163 F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
164 {
165 self.paver = func(self.paver);
166 self
167 }
168
169 pub fn test_executor(mut self, executor: Box<dyn TestExecutor<R>>) -> Self {
170 self.test_executor = Some(executor);
171 self
172 }
173
174 pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
176 assert_eq!(self.fuchsia_image, None);
177 self.fuchsia_image = Some((zbi, vbmeta));
178 self
179 }
180
181 pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
183 assert_eq!(self.recovery_image, None);
184 self.recovery_image = Some((zbi, vbmeta));
185 self
186 }
187
188 pub fn firmware_image(mut self, type_: String, content: Vec<u8>) -> Self {
190 assert_eq!(self.firmware_images.insert(type_, content), None);
191 self
192 }
193
194 pub async fn build(mut self) -> Result<TestEnv<R>, Error> {
196 let (repo_config, served_repo, expected_blobfs_contents, merkle) =
197 if let Some(repo_config) = self.repo_config {
198 (
200 repo_config,
201 None,
202 BTreeSet::new(),
203 Hash::from_str(
204 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
205 )
206 .expect("make merkle"),
207 )
208 } else {
209 let mut update =
212 fuchsia_pkg_testing::UpdatePackageBuilder::new(TEST_REPO_URL.parse().unwrap())
213 .packages(
214 self.packages
215 .iter()
216 .map(|p| {
217 fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
218 TEST_REPO_URL.parse().unwrap(),
219 p.name().clone(),
220 None,
221 *p.hash(),
222 )
223 })
224 .collect::<Vec<_>>(),
225 )
226 .firmware_images(self.firmware_images);
227 if let Some((zbi, vbmeta)) = self.fuchsia_image {
228 update = update.fuchsia_image(zbi, vbmeta);
229 }
230 if let Some((zbi, vbmeta)) = self.recovery_image {
231 update = update.recovery_image(zbi, vbmeta);
232 }
233 let (update, images) = update.build().await;
234
235 let expected_blobfs_contents = self
237 .packages
238 .iter()
239 .chain([update.as_package()])
240 .flat_map(|p| p.list_blobs())
241 .collect();
242
243 let repo = Arc::new(
244 self.packages
245 .iter()
246 .chain([update.as_package(), &images])
247 .fold(
248 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
249 .add_package(update.as_package()),
250 |repo, package| repo.add_package(package),
251 )
252 .build()
253 .await
254 .expect("build repo"),
255 );
256
257 let served_repo = Arc::clone(&repo).server().start().expect("serve repo");
258 let config = RepositoryConfigs::Version1(vec![
259 served_repo.make_repo_config(TEST_REPO_URL.parse().expect("make repo config")),
260 ]);
261
262 let update_merkle = *update.as_package().hash();
263 let mut packages = vec![update.into_package()];
266 packages.append(&mut self.packages);
267 (config, Some(served_repo), expected_blobfs_contents, update_merkle)
268 };
269
270 let dir = tempfile::tempdir()?;
271 let mut path = dir.path().to_owned();
272 path.push("repo_config.json");
273 let path = path.as_path();
274 let mut file =
275 std::io::BufWriter::new(std::fs::File::create(path).context("creating file")?);
276 serde_json::to_writer(&mut file, &repo_config).unwrap();
277 file.flush().unwrap();
278
279 Ok(TestEnv {
280 blobfs: self.blobfs,
281 board: self.board,
282 channel: self.channel,
283 omaha: self.omaha,
284 expected_blobfs_contents,
285 paver: Arc::new(self.paver.build()),
286 _repo: served_repo,
287 repo_config_dir: dir,
288 update_merkle: merkle,
289 version: self.version,
290 test_executor: self.test_executor.expect("test executor must be set"),
291 system_image_hash: self.system_image_hash,
292 })
293 }
294}
295
296pub struct TestEnv<R> {
297 blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
298 channel: String,
299 omaha: OmahaState,
300 expected_blobfs_contents: BTreeSet<Hash>,
301 paver: Arc<MockPaverService>,
302 _repo: Option<ServedRepository>,
303 repo_config_dir: tempfile::TempDir,
304 update_merkle: Hash,
305 board: String,
306 version: String,
307 test_executor: Box<dyn TestExecutor<R>>,
308 system_image_hash: Option<Hash>,
309}
310
311impl<R> TestEnv<R> {
312 async fn start_omaha(omaha: OmahaState, merkle: Hash) -> Result<UpdateUrlSource, Error> {
313 match omaha {
314 OmahaState::Disabled(url) => Ok(match url {
315 Some(url) => UpdateUrlSource::UpdateUrl(url),
316 None => UpdateUrlSource::UseDefault,
317 }),
318 OmahaState::Manual(cfg) => Ok(UpdateUrlSource::OmahaConfig(cfg)),
319 OmahaState::Auto(response) => {
320 let mut response = ResponseAndMetadata { response, ..Default::default() };
322 let p: Vec<_> = response.package_name.split("?hash=").collect();
323 assert_eq!(p.len(), 2);
324 response.package_name = format!("{}?hash={}", p[0], merkle);
325 let server = OmahaServerBuilder::default()
326 .responses_by_appid(
327 vec![("integration-test-appid".to_string(), response)]
328 .into_iter()
329 .collect::<ResponseMap>(),
330 )
331 .build()
332 .unwrap();
333 let addr = OmahaServer::start_and_detach(Arc::new(Mutex::new(server)), None)
334 .await
335 .context("Starting omaha server")?;
336 let config =
337 OmahaConfig { app_id: "integration-test-appid".to_owned(), server_url: addr };
338
339 Ok(UpdateUrlSource::OmahaConfig(config))
340 }
341 }
342 }
343
344 pub async fn run(self) -> R {
346 let update_url_source = TestEnv::<R>::start_omaha(self.omaha, self.update_merkle)
347 .await
348 .expect("Starting Omaha server");
349
350 let mut service_fs = ServiceFs::new();
351 let paver_clone = Arc::clone(&self.paver);
352 service_fs.add_fidl_service(move |stream: PaverRequestStream| {
353 fasync::Task::spawn(
354 Arc::clone(&paver_clone)
355 .run_paver_service(stream)
356 .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
357 )
358 .detach();
359 });
360
361 let (client, server) =
362 fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>();
363 service_fs
364 .serve_connection(server.into_channel().into())
365 .expect("Failed to serve connection");
366 fasync::Task::spawn(service_fs.collect()).detach();
367
368 let params = TestParams {
369 blobfs: self.blobfs,
370 board: self.board,
371 channel: self.channel,
372 expected_blobfs_contents: self.expected_blobfs_contents,
373 paver: self.paver,
374 repo_config_dir: self.repo_config_dir,
375 update_merkle: self.update_merkle,
376 version: self.version,
377 update_url_source,
378 paver_connector: client,
379 system_image_hash: self.system_image_hash,
380 };
381
382 self.test_executor.run(params).await
383 }
384}