Skip to main content

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