iquery_test_support/
test_support.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use byteorder::{LittleEndian, WriteBytesExt};
6use fidl::endpoints::{create_endpoints, create_proxy, ControlHandle, RequestStream, ServerEnd};
7use fidl_fuchsia_component_decl::{
8    Capability, Component, Dictionary, Expose, ExposeDictionary, ExposeProtocol, ParentRef,
9    Protocol, Ref, SelfRef,
10};
11use fidl_fuchsia_io::{self as fio, DirectoryMarker};
12use fidl_fuchsia_sys2 as fsys2;
13use futures::{StreamExt, TryStreamExt};
14use moniker::Moniker;
15use std::collections::HashMap;
16use std::io::Write;
17use std::path::Path;
18use std::rc::Rc;
19use std::str::FromStr;
20use std::sync::atomic::{AtomicBool, Ordering};
21use zx_status::Status;
22
23/// Builder struct for `RealmQueryResult`/
24/// This is an builder interface meant to simplify building of test fixtures.
25/// Example usage:
26/// ```
27///   MockRealmQuery.add()
28///   .when("other/component") // when client queries for this string ("other/component").
29///   .moniker("./other/component") // Returns the following.
30///   .exposes(vec![Expose::Protocol(ExposeProtocol {
31///       source: Some(Ref::Self_(SelfRef)),
32///       target: Some(Ref::Self_(SelfRef)),
33///       source_name: Some("src".to_owned()),
34///       target_name: Some("fuchsia.io.SomeOtherThing".to_owned()),
35///       ..Default::default()
36///   })])
37///   .add() // Finish building the result.
38///   .when("some/thing") // Start another build.
39///   ...
40/// ```
41#[derive(Default)]
42pub struct MockRealmQueryBuilder {
43    mapping: HashMap<String, Box<MockRealmQueryBuilderInner>>,
44}
45
46/// Inner struct of `MockRealmQueryBuilder` to provide a builder interface for
47/// RealmQuery protocol responses.
48pub struct MockRealmQueryBuilderInner {
49    when: Moniker,
50    moniker: Moniker,
51    exposes: Vec<Expose>,
52    parent: Option<Box<MockRealmQueryBuilder>>,
53}
54
55impl MockRealmQueryBuilderInner {
56    /// Sets the result moniker.
57    pub fn moniker(mut self, moniker: &str) -> Self {
58        self.moniker = moniker.try_into().unwrap();
59        self
60    }
61
62    /// Sets the result vector of `Expose`s.
63    pub fn exposes(mut self, exposes: Vec<Expose>) -> Self {
64        self.exposes = exposes;
65        self
66    }
67
68    /// Completes the build and returns a `MockRealmQueryBuilder`.
69    pub fn add(mut self) -> MockRealmQueryBuilder {
70        let mut parent = *self.parent.unwrap();
71        self.parent = None;
72
73        parent.mapping.insert(self.when.to_string(), Box::new(self));
74        parent
75    }
76
77    pub fn serve_exposed_dir(&self, server_end: ServerEnd<DirectoryMarker>, path: &str) {
78        let mut mock_dir_top = MockDir::new("expose".to_owned());
79        let mut mock_accessors = MockDir::new("diagnostics-accessors".to_owned());
80        for expose in &self.exposes {
81            let Expose::Protocol(ExposeProtocol {
82                source_name: Some(name), source_dictionary, ..
83            }) = expose
84            else {
85                continue;
86            };
87            if matches!(source_dictionary, Some(d) if d == "diagnostics-accessors") {
88                mock_accessors = mock_accessors.add_entry(MockFile::new_arc(name.to_owned()));
89            }
90        }
91
92        match path {
93            "diagnostics-accessors" => {
94                fuchsia_async::Task::local(async move {
95                    Rc::new(mock_accessors).serve(server_end).await
96                })
97                .detach();
98            }
99            _ => {
100                mock_dir_top = mock_dir_top.add_entry(Rc::new(mock_accessors));
101                fuchsia_async::Task::local(
102                    async move { Rc::new(mock_dir_top).serve(server_end).await },
103                )
104                .detach();
105            }
106        }
107    }
108
109    fn to_instance(&self) -> fsys2::Instance {
110        fsys2::Instance {
111            moniker: Some(self.moniker.to_string()),
112            url: Some("".to_owned()),
113            instance_id: None,
114            resolved_info: Some(fsys2::ResolvedInfo {
115                resolved_url: Some("".to_owned()),
116                ..Default::default()
117            }),
118            ..Default::default()
119        }
120    }
121
122    fn make_manifest(&self) -> Component {
123        let capabilities = self
124            .exposes
125            .iter()
126            .map(|expose| match expose {
127                Expose::Protocol(ExposeProtocol {
128                    source_name: Some(name),
129                    source: Some(Ref::Self_(SelfRef)),
130                    ..
131                }) => Capability::Protocol(Protocol {
132                    name: Some(name.clone()),
133                    source_path: Some(format!("/svc/{}", name)),
134                    ..Protocol::default()
135                }),
136                Expose::Dictionary(ExposeDictionary {
137                    source_name: Some(name),
138                    source: Some(Ref::Self_(SelfRef)),
139                    ..
140                }) => Capability::Dictionary(Dictionary {
141                    name: Some(name.clone()),
142                    source: Some(Ref::Self_(SelfRef)),
143                    ..Dictionary::default()
144                }),
145                _ => unreachable!("we just add protocols for the test purposes"),
146            })
147            .collect::<Vec<_>>();
148        Component {
149            capabilities: Some(capabilities),
150            exposes: Some(self.exposes.clone()),
151            ..Default::default()
152        }
153    }
154}
155
156impl MockRealmQueryBuilder {
157    /// Create a new empty `MockRealmQueryBuilder`.
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// Start a build of `RealmQueryResult` by specifying the
163    /// expected query string.
164    pub fn when(self, at: &str) -> MockRealmQueryBuilderInner {
165        MockRealmQueryBuilderInner {
166            when: at.try_into().unwrap(),
167            moniker: Moniker::root(),
168            exposes: vec![],
169            parent: Some(Box::new(self)),
170        }
171    }
172
173    /// Finish the build and return servable `MockRealmQuery`.
174    pub fn build(self) -> MockRealmQuery {
175        MockRealmQuery { mapping: self.mapping }
176    }
177
178    pub fn prefilled() -> Self {
179        Self::new()
180            .when("example/component")
181            .moniker("./example/component")
182            .exposes(vec![
183                Expose::Protocol(ExposeProtocol {
184                    source: Some(Ref::Self_(SelfRef)),
185                    target: Some(Ref::Parent(ParentRef)),
186                    source_name: Some("fuchsia.diagnostics.ArchiveAccessor".to_owned()),
187                    target_name: Some("fuchsia.diagnostics.ArchiveAccessor".to_owned()),
188                    source_dictionary: Some("diagnostics-accessors".to_owned()),
189                    ..Default::default()
190                }),
191                Expose::Dictionary(ExposeDictionary {
192                    source_name: Some("diagnostics-accessors".into()),
193                    target_name: Some("diagnostics-accessors".into()),
194                    source: Some(Ref::Self_(SelfRef)),
195                    target: Some(Ref::Parent(ParentRef)),
196                    ..Default::default()
197                }),
198            ])
199            .add()
200            .when("other/component")
201            .moniker("./other/component")
202            .exposes(vec![Expose::Protocol(ExposeProtocol {
203                source: Some(Ref::Self_(SelfRef)),
204                target: Some(Ref::Parent(ParentRef)),
205                source_name: Some("src".to_owned()),
206                target_name: Some("fuchsia.io.SomeOtherThing".to_owned()),
207                ..Default::default()
208            })])
209            .add()
210            .when("other/component")
211            .moniker("./other/component")
212            .exposes(vec![Expose::Protocol(ExposeProtocol {
213                source: Some(Ref::Self_(SelfRef)),
214                target: Some(Ref::Parent(ParentRef)),
215                source_name: Some("src".to_owned()),
216                target_name: Some("fuchsia.io.MagicStuff".to_owned()),
217                ..Default::default()
218            })])
219            .add()
220            .when("foo/component")
221            .moniker("./foo/component")
222            .exposes(vec![
223                Expose::Protocol(ExposeProtocol {
224                    source: Some(Ref::Self_(SelfRef)),
225                    target: Some(Ref::Parent(ParentRef)),
226                    source_name: Some("fuchsia.diagnostics.ArchiveAccessor.feedback".to_owned()),
227                    target_name: Some("fuchsia.diagnostics.ArchiveAccessor.feedback".to_owned()),
228                    source_dictionary: Some("diagnostics-accessors".to_owned()),
229                    ..Default::default()
230                }),
231                Expose::Dictionary(ExposeDictionary {
232                    source_name: Some("diagnostics-accessors".into()),
233                    target_name: Some("diagnostics-accessors".into()),
234                    source: Some(Ref::Self_(SelfRef)),
235                    target: Some(Ref::Parent(ParentRef)),
236                    ..Default::default()
237                }),
238            ])
239            .add()
240            .when("foo/bar/thing:instance")
241            .moniker("./foo/bar/thing:instance")
242            .exposes(vec![
243                Expose::Protocol(ExposeProtocol {
244                    source: Some(Ref::Self_(SelfRef)),
245                    target: Some(Ref::Parent(ParentRef)),
246                    source_name: Some("fuchsia.diagnostics.ArchiveAccessor.feedback".to_owned()),
247                    target_name: Some("fuchsia.diagnostics.ArchiveAccessor.feedback".to_owned()),
248                    source_dictionary: Some("diagnostics-accessors".to_owned()),
249                    ..Default::default()
250                }),
251                Expose::Dictionary(ExposeDictionary {
252                    source_name: Some("diagnostics-accessors".into()),
253                    target_name: Some("diagnostics-accessors".into()),
254                    source: Some(Ref::Self_(SelfRef)),
255                    target: Some(Ref::Parent(ParentRef)),
256                    ..Default::default()
257                }),
258            ])
259            .add()
260    }
261}
262
263/// Provides a mock `RealmQuery` interface.
264pub struct MockRealmQuery {
265    /// Mapping from Moniker -> Expose.
266    mapping: HashMap<String, Box<MockRealmQueryBuilderInner>>,
267}
268
269/// Creates the default test fixures for `MockRealmQuery`.
270impl Default for MockRealmQuery {
271    fn default() -> Self {
272        MockRealmQueryBuilder::prefilled().build()
273    }
274}
275
276impl MockRealmQuery {
277    /// Serves the `RealmQuery` interface asynchronously and runs until the program terminates.
278    pub async fn serve(self: Rc<Self>, object: ServerEnd<fsys2::RealmQueryMarker>) {
279        let mut stream = object.into_stream();
280        while let Ok(Some(request)) = stream.try_next().await {
281            match request {
282                fsys2::RealmQueryRequest::GetInstance { moniker, responder } => {
283                    let query_moniker = Moniker::from_str(moniker.as_str()).unwrap();
284                    let res = self.mapping.get(&query_moniker.to_string()).unwrap();
285                    responder.send(Ok(&res.to_instance())).unwrap();
286                }
287                fsys2::RealmQueryRequest::DeprecatedOpen {
288                    moniker,
289                    dir_type,
290                    object,
291                    responder,
292                    path,
293                    ..
294                } => {
295                    let query_moniker = Moniker::from_str(moniker.as_str()).unwrap();
296                    if let Some(res) = self.mapping.get(&query_moniker.to_string()) {
297                        if dir_type == fsys2::OpenDirType::ExposedDir {
298                            // Serve the out dir, everything else doesn't get served.
299                            res.serve_exposed_dir(object.into_channel().into(), &path);
300                        }
301                        responder.send(Ok(())).unwrap();
302                    } else {
303                        responder.send(Err(fsys2::OpenError::InstanceNotFound)).unwrap();
304                    }
305                }
306                fsys2::RealmQueryRequest::OpenDirectory {
307                    moniker,
308                    dir_type,
309                    object,
310                    responder,
311                    ..
312                } => {
313                    let query_moniker = Moniker::from_str(moniker.as_str()).unwrap();
314                    if let Some(res) = self.mapping.get(&query_moniker.to_string()) {
315                        if dir_type == fsys2::OpenDirType::OutgoingDir {
316                            // Serve the out dir, everything else doesn't get served.
317                            res.serve_exposed_dir(object, "");
318                        }
319                        responder.send(Ok(())).unwrap();
320                    } else {
321                        responder.send(Err(fsys2::OpenError::InstanceNotFound)).unwrap();
322                    }
323                }
324                fsys2::RealmQueryRequest::GetManifest { moniker, responder, .. } => {
325                    let query_moniker = Moniker::from_str(moniker.as_str()).unwrap();
326                    let res = self.mapping.get(&query_moniker.to_string()).unwrap();
327                    let manifest = res.make_manifest();
328                    let manifest = fidl::persist(&manifest).unwrap();
329                    let (client_end, server_end) =
330                        create_endpoints::<fsys2::ManifestBytesIteratorMarker>();
331
332                    fuchsia_async::Task::spawn(async move {
333                        let mut stream = server_end.into_stream();
334                        let fsys2::ManifestBytesIteratorRequest::Next { responder } =
335                            stream.next().await.unwrap().unwrap();
336                        responder.send(manifest.as_slice()).unwrap();
337                        let fsys2::ManifestBytesIteratorRequest::Next { responder } =
338                            stream.next().await.unwrap().unwrap();
339                        responder.send(&[]).unwrap();
340                    })
341                    .detach();
342
343                    responder.send(Ok(client_end)).unwrap();
344                }
345                fsys2::RealmQueryRequest::GetResolvedDeclaration { moniker, responder, .. } => {
346                    let query_moniker = Moniker::from_str(moniker.as_str()).unwrap();
347                    let res = self.mapping.get(&query_moniker.to_string()).unwrap();
348                    let manifest = res.make_manifest();
349                    let manifest = fidl::persist(&manifest).unwrap();
350                    let (client_end, server_end) =
351                        create_endpoints::<fsys2::ManifestBytesIteratorMarker>();
352
353                    fuchsia_async::Task::spawn(async move {
354                        let mut stream = server_end.into_stream();
355                        let fsys2::ManifestBytesIteratorRequest::Next { responder } =
356                            stream.next().await.unwrap().unwrap();
357                        responder.send(manifest.as_slice()).unwrap();
358                        let fsys2::ManifestBytesIteratorRequest::Next { responder } =
359                            stream.next().await.unwrap().unwrap();
360                        responder.send(&[]).unwrap();
361                    })
362                    .detach();
363
364                    responder.send(Ok(client_end)).unwrap();
365                }
366                fsys2::RealmQueryRequest::GetAllInstances { responder } => {
367                    let instances: Vec<fsys2::Instance> =
368                        self.mapping.values().map(|m| m.to_instance()).collect();
369
370                    let (client_end, server_end) =
371                        create_endpoints::<fsys2::InstanceIteratorMarker>();
372
373                    fuchsia_async::Task::spawn(async move {
374                        let mut stream = server_end.into_stream();
375                        let fsys2::InstanceIteratorRequest::Next { responder } =
376                            stream.next().await.unwrap().unwrap();
377                        responder.send(&instances).unwrap();
378                        let fsys2::InstanceIteratorRequest::Next { responder } =
379                            stream.next().await.unwrap().unwrap();
380                        responder.send(&[]).unwrap();
381                    })
382                    .detach();
383
384                    responder.send(Ok(client_end)).unwrap();
385                }
386                _ => unreachable!("request {:?}", request),
387            }
388        }
389    }
390
391    /// Serves the `RealmQuery` interface asynchronously and runs until the program terminates.
392    /// Then, instead of needing the client to discover the protocol, return the proxy for futher
393    /// test use.
394    pub async fn get_proxy(self: Rc<Self>) -> fsys2::RealmQueryProxy {
395        let (proxy, server_end) = create_proxy::<fsys2::RealmQueryMarker>();
396        fuchsia_async::Task::local(async move { self.serve(server_end).await }).detach();
397        proxy
398    }
399}
400
401// Mock directory structure.
402pub trait Entry {
403    fn open(self: Rc<Self>, flags: fio::OpenFlags, path: &str, object: ServerEnd<fio::NodeMarker>);
404    fn encode(&self, buf: &mut Vec<u8>);
405    fn name(&self) -> String;
406}
407
408pub struct MockDir {
409    subdirs: HashMap<String, Rc<dyn Entry>>,
410    name: String,
411    at_end: AtomicBool,
412}
413
414impl MockDir {
415    pub fn new(name: String) -> Self {
416        MockDir { name, subdirs: HashMap::new(), at_end: AtomicBool::new(false) }
417    }
418
419    pub fn new_arc(name: String) -> Rc<Self> {
420        Rc::new(Self::new(name))
421    }
422
423    pub fn add_entry(mut self, entry: Rc<dyn Entry>) -> Self {
424        self.subdirs.insert(entry.name(), entry);
425        self
426    }
427
428    async fn serve(self: Rc<Self>, object: ServerEnd<fio::DirectoryMarker>) {
429        let mut stream = object.into_stream();
430        let _ = stream.control_handle().send_on_open_(
431            Status::OK.into_raw(),
432            Some(fio::NodeInfoDeprecated::Directory(fio::DirectoryObject {})),
433        );
434        while let Ok(Some(request)) = stream.try_next().await {
435            match request {
436                fio::DirectoryRequest::DeprecatedOpen { flags, mode: _, path, object, .. } => {
437                    self.clone().open(flags, &path, object);
438                }
439                fio::DirectoryRequest::Rewind { responder, .. } => {
440                    self.at_end.store(false, Ordering::Relaxed);
441                    responder.send(Status::OK.into_raw()).unwrap();
442                }
443                fio::DirectoryRequest::ReadDirents { max_bytes: _, responder, .. } => {
444                    let entries = match self.at_end.compare_exchange(
445                        false,
446                        true,
447                        Ordering::Relaxed,
448                        Ordering::Relaxed,
449                    ) {
450                        Ok(false) => encode_entries(&self.subdirs),
451                        Err(true) => Vec::new(),
452                        _ => unreachable!(),
453                    };
454                    responder.send(Status::OK.into_raw(), &entries).unwrap();
455                }
456                x => panic!("unsupported request: {:?}", x),
457            }
458        }
459    }
460}
461
462fn encode_entries(subdirs: &HashMap<String, Rc<dyn Entry>>) -> Vec<u8> {
463    let mut buf = Vec::new();
464    let mut data = subdirs.iter().collect::<Vec<(_, _)>>();
465    data.sort_by(|a, b| a.0.cmp(b.0));
466    for (_, entry) in data.iter() {
467        entry.encode(&mut buf);
468    }
469    buf
470}
471
472impl Entry for MockDir {
473    fn open(self: Rc<Self>, flags: fio::OpenFlags, path: &str, object: ServerEnd<fio::NodeMarker>) {
474        let path = Path::new(path);
475        let mut path_iter = path.iter();
476        let segment = if let Some(segment) = path_iter.next() {
477            if let Some(segment) = segment.to_str() {
478                segment
479            } else {
480                send_error(object, Status::NOT_FOUND);
481                return;
482            }
483        } else {
484            "."
485        };
486        if segment == "." {
487            fuchsia_async::Task::local(self.serve(ServerEnd::new(object.into_channel()))).detach();
488            return;
489        }
490        if let Some(entry) = self.subdirs.get(segment) {
491            entry.clone().open(flags, path_iter.as_path().to_str().unwrap(), object);
492        } else {
493            send_error(object, Status::NOT_FOUND);
494        }
495    }
496
497    fn encode(&self, buf: &mut Vec<u8>) {
498        buf.write_u64::<LittleEndian>(fio::INO_UNKNOWN).expect("writing mockdir ino to work");
499        buf.write_u8(self.name.len() as u8).expect("writing mockdir size to work");
500        buf.write_u8(fio::DirentType::Directory.into_primitive())
501            .expect("writing mockdir type to work");
502        buf.write_all(self.name.as_ref()).expect("writing mockdir name to work");
503    }
504
505    fn name(&self) -> String {
506        self.name.clone()
507    }
508}
509
510impl Entry for fio::DirectoryProxy {
511    fn open(self: Rc<Self>, flags: fio::OpenFlags, path: &str, object: ServerEnd<fio::NodeMarker>) {
512        let _ = fio::DirectoryProxy::deprecated_open(
513            &self,
514            flags,
515            fio::ModeType::empty(),
516            path,
517            object,
518        );
519    }
520
521    fn encode(&self, _buf: &mut Vec<u8>) {
522        unimplemented!();
523    }
524
525    fn name(&self) -> String {
526        unimplemented!();
527    }
528}
529
530struct MockFile {
531    name: String,
532}
533
534impl MockFile {
535    pub fn new(name: String) -> Self {
536        MockFile { name }
537    }
538    pub fn new_arc(name: String) -> Rc<Self> {
539        Rc::new(Self::new(name))
540    }
541}
542
543impl Entry for MockFile {
544    fn open(
545        self: Rc<Self>,
546        _flags: fio::OpenFlags,
547        _path: &str,
548        _object: ServerEnd<fio::NodeMarker>,
549    ) {
550        unimplemented!();
551    }
552
553    fn encode(&self, buf: &mut Vec<u8>) {
554        buf.write_u64::<LittleEndian>(fio::INO_UNKNOWN).expect("writing mockdir ino to work");
555        buf.write_u8(self.name.len() as u8).expect("writing mockdir size to work");
556        buf.write_u8(fio::DirentType::Service.into_primitive())
557            .expect("writing mockdir type to work");
558        buf.write_all(self.name.as_ref()).expect("writing mockdir name to work");
559    }
560
561    fn name(&self) -> String {
562        self.name.clone()
563    }
564}
565
566fn send_error(object: ServerEnd<fio::NodeMarker>, status: Status) {
567    let stream = object.into_stream();
568    let control_handle = stream.control_handle();
569    let _ = control_handle.send_on_open_(status.into_raw(), None);
570    control_handle.shutdown_with_epitaph(status);
571}