inspect_validator/
puppet.rs

1// Copyright 2019 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 super::PUPPET_MONIKER;
6use super::data::{self, Data, LazyNode};
7use super::metrics::Metrics;
8use anyhow::{Error, format_err};
9use fuchsia_component::client as fclient;
10use serde::Serialize;
11use zx::{self as zx, Vmo};
12use {fidl_diagnostics_validate as validate, fidl_fuchsia_inspect as fidl_inspect};
13
14pub const VMO_SIZE: u64 = 4096;
15
16#[derive(Debug)]
17pub struct Config {
18    pub diff_type: DiffType,
19    pub printable_name: String,
20    pub has_runner_node: bool,
21    pub test_archive: bool,
22}
23
24/// When reporting a discrepancy between local and remote Data trees, should the output include:
25/// - The full rendering of both trees?
26/// - The condensed diff between the trees? (This may still be quite large.)
27/// - Both full and condensed renderings?
28#[derive(Clone, Copy, Debug, Default, Serialize)]
29pub enum DiffType {
30    #[default]
31    Full,
32    Diff,
33    Both,
34}
35
36impl From<Option<validate::DiffType>> for DiffType {
37    fn from(original: Option<validate::DiffType>) -> Self {
38        match original {
39            Some(validate::DiffType::Diff) => Self::Diff,
40            Some(validate::DiffType::Both) => Self::Both,
41            _ => Self::Full,
42        }
43    }
44}
45
46pub struct Puppet {
47    pub vmo: Vmo,
48    // Need to remember the connection to avoid dropping the VMO
49    connection: Connection,
50    // A printable name for output to the user.
51    pub config: Config,
52}
53
54impl Puppet {
55    pub async fn apply(
56        &mut self,
57        action: &mut validate::Action,
58    ) -> Result<validate::TestResult, Error> {
59        Ok(self.connection.fidl.act(action).await?)
60    }
61
62    pub async fn apply_lazy(
63        &mut self,
64        lazy_action: &mut validate::LazyAction,
65    ) -> Result<validate::TestResult, Error> {
66        match &self.connection.root_link_channel {
67            Some(_) => Ok(self.connection.fidl.act_lazy(lazy_action).await?),
68            None => Ok(validate::TestResult::Unimplemented),
69        }
70    }
71
72    pub async fn act_lazy_thread_local(
73        &mut self,
74        lazy_action: &mut validate::LazyAction,
75    ) -> Result<validate::TestResult, Error> {
76        Ok(self.connection.fidl.act_lazy_thread_local(lazy_action).await?)
77    }
78
79    pub async fn publish(&mut self) -> Result<validate::TestResult, Error> {
80        Ok(self.connection.fidl.publish().await?)
81    }
82
83    pub async fn connect() -> Result<Self, Error> {
84        Puppet::initialize_with_connection(Connection::connect().await?).await
85    }
86
87    pub(crate) async fn shutdown(self) {
88        let lifecycle_controller =
89            fclient::connect_to_protocol::<fidl_fuchsia_sys2::LifecycleControllerMarker>().unwrap();
90        lifecycle_controller.stop_instance(&format!("./{PUPPET_MONIKER}")).await.unwrap().unwrap();
91    }
92
93    /// Get the printable name associated with this puppet/test
94    pub fn printable_name(&self) -> &str {
95        &self.config.printable_name
96    }
97
98    #[cfg(test)]
99    pub async fn connect_local(local_fidl: validate::InspectPuppetProxy) -> Result<Puppet, Error> {
100        let mut puppet = Puppet::initialize_with_connection(Connection::new(local_fidl)).await?;
101        puppet.config.test_archive = false;
102        Ok(puppet)
103    }
104
105    async fn initialize_with_connection(mut connection: Connection) -> Result<Puppet, Error> {
106        Ok(Puppet {
107            vmo: connection.initialize_vmo().await?,
108            config: connection.get_config().await?,
109            connection,
110        })
111    }
112
113    pub async fn read_data(&self) -> Result<Data, Error> {
114        Ok(match &self.connection.root_link_channel {
115            None => data::Scanner::try_from(&self.vmo)?.data(),
116            Some(root_link_channel) => {
117                let vmo_tree = LazyNode::new(root_link_channel.clone()).await?;
118                data::Scanner::try_from(vmo_tree)?.data()
119            }
120        })
121    }
122
123    pub fn metrics(&self) -> Result<Metrics, Error> {
124        Ok(data::Scanner::try_from(&self.vmo)?.metrics())
125    }
126}
127
128struct Connection {
129    fidl: validate::InspectPuppetProxy,
130    // Connection to Tree FIDL if Puppet supports it.
131    // Puppets can add support by implementing InitializeTree method.
132    root_link_channel: Option<fidl_inspect::TreeProxy>,
133}
134
135impl Connection {
136    async fn connect() -> Result<Self, Error> {
137        let puppet_fidl = fclient::connect_to_protocol::<validate::InspectPuppetMarker>().unwrap();
138        Ok(Self::new(puppet_fidl))
139    }
140
141    async fn get_config(&self) -> Result<Config, Error> {
142        let (printable_name, opts) = self.fidl.get_config().await?;
143        Ok(Config {
144            diff_type: opts.diff_type.into(),
145            printable_name,
146            has_runner_node: opts.has_runner_node.unwrap_or(false),
147            test_archive: true,
148        })
149    }
150
151    async fn fetch_link_channel(
152        fidl: &validate::InspectPuppetProxy,
153    ) -> Option<fidl_inspect::TreeProxy> {
154        let params =
155            validate::InitializationParams { vmo_size: Some(VMO_SIZE), ..Default::default() };
156        let response = fidl.initialize_tree(&params).await;
157        if let Ok((Some(tree_client_end), validate::TestResult::Ok)) = response {
158            Some(tree_client_end.into_proxy())
159        } else {
160            None
161        }
162    }
163
164    async fn get_vmo_handle(channel: &fidl_inspect::TreeProxy) -> Result<Vmo, Error> {
165        let tree_content = channel.get_content().await?;
166        let buffer = tree_content.buffer.ok_or(format_err!("Buffer doesn't contain VMO"))?;
167        Ok(buffer.vmo)
168    }
169
170    fn new(fidl: validate::InspectPuppetProxy) -> Self {
171        Self { fidl, root_link_channel: None }
172    }
173
174    async fn initialize_vmo(&mut self) -> Result<Vmo, Error> {
175        self.root_link_channel = Self::fetch_link_channel(&self.fidl).await;
176        match &self.root_link_channel {
177            Some(root_link_channel) => Self::get_vmo_handle(root_link_channel).await,
178            None => {
179                let params = validate::InitializationParams {
180                    vmo_size: Some(VMO_SIZE),
181                    ..Default::default()
182                };
183                let handle: Option<zx::Handle>;
184                let out = self.fidl.initialize(&params).await?;
185                if let (Some(out_handle), _) = out {
186                    handle = Some(out_handle);
187                } else {
188                    return Err(format_err!("Didn't get a VMO handle"));
189                }
190                match handle {
191                    Some(unwrapped_handle) => Ok(Vmo::from(unwrapped_handle)),
192                    None => Err(format_err!("Failed to unwrap handle")),
193                }
194            }
195        }
196    }
197}
198
199#[cfg(test)]
200pub(crate) mod tests {
201    use super::*;
202    use crate::create_node;
203    use anyhow::Context as _;
204    use fidl::endpoints::{RequestStream, ServerEnd, create_proxy};
205    use fidl_diagnostics_validate::{
206        Action, CreateNode, CreateNumericProperty, InspectPuppetMarker, InspectPuppetRequest,
207        InspectPuppetRequestStream, Options, ROOT_ID, TestResult, Value,
208    };
209    use fuchsia_async as fasync;
210    use fuchsia_inspect::{Inspector, InspectorConfig, IntProperty, Node};
211    use futures::prelude::*;
212    use log::info;
213    use std::collections::HashMap;
214    use zx::HandleBased;
215
216    #[fuchsia::test]
217    async fn test_fidl_loopback() -> Result<(), Error> {
218        let mut puppet = local_incomplete_puppet().await?;
219        assert_eq!(puppet.vmo.get_size().unwrap(), VMO_SIZE);
220        let tree = puppet.read_data().await?;
221        assert_eq!(tree.to_string(), "root ->".to_string());
222        let mut data = Data::new();
223        tree.compare(&data, DiffType::Full)?;
224        let mut action = create_node!(parent: ROOT_ID, id: 1, name: "child");
225        puppet.apply(&mut action).await?;
226        data.apply(&action)?;
227        let tree = data::Scanner::try_from(&puppet.vmo)?.data();
228        assert_eq!(tree.to_string(), "root ->\n> child ->".to_string());
229        tree.compare(&data, DiffType::Full)?;
230        Ok(())
231    }
232
233    // This is a partial implementation.
234    // All it can do is initialize, and then create nodes and int properties (which it
235    // will hold forever). Trying to create a uint property will return Unimplemented.
236    // Other actions will give various kinds of incorrect results.
237    pub(crate) async fn local_incomplete_puppet() -> Result<Puppet, Error> {
238        let (client_end, server_end) = create_proxy();
239        spawn_local_puppet(server_end).await;
240        Puppet::connect_local(client_end).await
241    }
242
243    async fn spawn_local_puppet(server_end: ServerEnd<InspectPuppetMarker>) {
244        fasync::Task::spawn(
245            async move {
246                // Inspector must be remembered so its VMO persists
247                let mut inspector_maybe: Option<Inspector> = None;
248                let mut nodes: HashMap<u32, Node> = HashMap::new();
249                let mut properties: HashMap<u32, IntProperty> = HashMap::new();
250                let server_chan = fasync::Channel::from_channel(server_end.into_channel());
251                let mut stream = InspectPuppetRequestStream::from_channel(server_chan);
252                while let Some(event) = stream.try_next().await? {
253                    match event {
254                        InspectPuppetRequest::GetConfig { responder } => {
255                            responder.send("*Local*", Options::default()).ok();
256                        }
257                        InspectPuppetRequest::Initialize { params, responder } => {
258                            let inspector = match params.vmo_size {
259                                Some(size) => {
260                                    Inspector::new(InspectorConfig::default().size(size as usize))
261                                }
262                                None => Inspector::default(),
263                            };
264                            responder
265                                .send(
266                                    inspector.duplicate_vmo().map(|v| v.into_handle()),
267                                    TestResult::Ok,
268                                )
269                                .context("responding to initialize")?;
270                            inspector_maybe = Some(inspector);
271                        }
272                        InspectPuppetRequest::Act { action, responder } => match action {
273                            Action::CreateNode(CreateNode { parent, id, name }) => {
274                                if let Some(ref inspector) = inspector_maybe {
275                                    let parent_node = if parent == ROOT_ID {
276                                        inspector.root()
277                                    } else {
278                                        nodes.get(&parent).unwrap()
279                                    };
280                                    let new_child = parent_node.create_child(name);
281                                    nodes.insert(id, new_child);
282                                }
283                                responder.send(TestResult::Ok)?;
284                            }
285                            Action::CreateNumericProperty(CreateNumericProperty {
286                                parent,
287                                id,
288                                name,
289                                value: Value::IntT(value),
290                            }) => {
291                                inspector_maybe.as_ref().map(|i| {
292                                    let parent_node = if parent == 0 {
293                                        i.root()
294                                    } else {
295                                        nodes.get(&parent).unwrap()
296                                    };
297                                    properties.insert(id, parent_node.create_int(name, value))
298                                });
299                                responder.send(TestResult::Ok)?;
300                            }
301                            Action::CreateNumericProperty(CreateNumericProperty {
302                                value: Value::UintT(_),
303                                ..
304                            }) => {
305                                responder.send(TestResult::Unimplemented)?;
306                            }
307
308                            _ => responder.send(TestResult::Illegal)?,
309                        },
310                        InspectPuppetRequest::InitializeTree { params: _, responder } => {
311                            responder.send(None, TestResult::Unimplemented)?;
312                        }
313                        InspectPuppetRequest::ActLazy { lazy_action: _, responder } => {
314                            responder.send(TestResult::Unimplemented)?;
315                        }
316                        InspectPuppetRequest::Publish { responder } => {
317                            responder.send(TestResult::Unimplemented)?;
318                        }
319                        InspectPuppetRequest::ActLazyThreadLocal { responder, .. } => {
320                            responder.send(TestResult::Unimplemented)?;
321                        }
322                        InspectPuppetRequest::_UnknownMethod { .. } => {}
323                    }
324                }
325                Ok(())
326            }
327            .unwrap_or_else(|e: anyhow::Error| info!("error running validate interface: {:?}", e)),
328        )
329        .detach();
330    }
331}