1use crate::realm::{get_all_instances, Instance};
6use anyhow::Result;
7use fidl_fuchsia_sys2 as fsys;
8use std::collections::HashSet;
9use std::fmt::Write;
10use std::str::FromStr;
11use url::Url;
12
13static GRAPHVIZ_START: &str = r##"digraph {
15 graph [ pad = 0.2 ]
16 node [ shape = "box" color = "#2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 target = "_parent" margin = 0.22, ordering = out ];
17 edge [ color = "#37474f" penwidth = 1 arrowhead = none target = "_parent" fontname = "roboto mono" fontsize = 10 ]
18 splines = "ortho"
19"##;
20
21static GRAPHVIZ_END: &str = "}";
24
25#[derive(Debug, PartialEq)]
27pub enum GraphFilter {
28 Ancestor(String),
31 Descendant(String),
34 Relative(String),
37}
38
39impl FromStr for GraphFilter {
40 type Err = &'static str;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 match s.split_once(":") {
44 Some((function, arg)) => match function {
45 "ancestor" | "ancestors" => Ok(Self::Ancestor(arg.to_string())),
46 "descendant" | "descendants" => Ok(Self::Descendant(arg.to_string())),
47 "relative" | "relatives" => Ok(Self::Relative(arg.to_string())),
48 _ => Err("unknown function for list filter."),
49 },
50 None => Err("list filter should be 'ancestors:<component_name>', 'descendants:<component_name>', or 'relatives:<component_name>'."),
51 }
52 }
53}
54
55#[derive(Debug, PartialEq)]
57pub enum GraphOrientation {
58 TopToBottom,
60 LeftToRight,
62}
63
64impl FromStr for GraphOrientation {
65 type Err = &'static str;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 match s.to_lowercase().replace("_", "").replace("-", "").as_str() {
69 "tb" | "toptobottom" => Ok(GraphOrientation::TopToBottom),
70 "lr" | "lefttoright" => Ok(GraphOrientation::LeftToRight),
71 _ => Err("graph orientation should be 'toptobottom' or 'lefttoright'."),
72 }
73 }
74}
75
76pub async fn graph_cmd<W: std::io::Write>(
77 filter: Option<GraphFilter>,
78 orientation: GraphOrientation,
79 realm_query: fsys::RealmQueryProxy,
80 mut writer: W,
81) -> Result<()> {
82 let mut instances = get_all_instances(&realm_query).await?;
83
84 instances = match filter {
85 Some(GraphFilter::Ancestor(m)) => filter_ancestors(instances, m),
86 Some(GraphFilter::Descendant(m)) => filter_descendants(instances, m),
87 Some(GraphFilter::Relative(m)) => filter_relatives(instances, m),
88 _ => instances,
89 };
90
91 let output = create_dot_graph(instances, orientation);
92 writeln!(writer, "{}", output)?;
93
94 Ok(())
95}
96
97fn filter_ancestors(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
98 let mut ancestors = HashSet::new();
99
100 for instance in &instances {
102 if let Some(child) = instance.moniker.leaf() {
103 if child.to_string() == child_str {
104 let mut cur_moniker = instance.moniker.clone();
106 ancestors.insert(cur_moniker.clone());
107
108 while let Some(parent) = cur_moniker.parent() {
110 ancestors.insert(parent.clone());
111 cur_moniker = parent;
112 }
113 }
114 }
115 }
116
117 instances.into_iter().filter(|i| ancestors.contains(&i.moniker)).collect()
118}
119
120fn filter_descendants(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
121 let mut descendants = HashSet::new();
122
123 for instance in &instances {
125 if let Some(child) = instance.moniker.leaf() {
126 if child.to_string() == child_str {
127 for possible_child_instance in &instances {
129 if possible_child_instance.moniker.has_prefix(&instance.moniker) {
130 descendants.insert(possible_child_instance.moniker.clone());
131 }
132 }
133 }
134 }
135 }
136
137 instances.into_iter().filter(|i| descendants.contains(&i.moniker)).collect()
138}
139
140fn filter_relatives(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
141 let mut relatives = HashSet::new();
142
143 for instance in &instances {
145 if let Some(child) = instance.moniker.leaf() {
146 if child.to_string() == child_str {
147 let mut cur_moniker = instance.moniker.clone();
149 while let Some(parent) = cur_moniker.parent() {
150 relatives.insert(parent.clone());
151 cur_moniker = parent;
152 }
153
154 for possible_child_instance in &instances {
156 if possible_child_instance.moniker.has_prefix(&instance.moniker) {
157 relatives.insert(possible_child_instance.moniker.clone());
158 }
159 }
160 }
161 }
162 }
163
164 instances.into_iter().filter(|i| relatives.contains(&i.moniker)).collect()
165}
166
167fn construct_codesearch_url(component_url: &str) -> String {
168 let mut name_with_filetype = match component_url.rsplit_once("/") {
170 Some(parts) => parts.1.to_string(),
171 None => component_url.to_string(),
174 };
175 if name_with_filetype.ends_with(".cm") {
176 name_with_filetype.push('l');
177 }
178
179 let name_with_underscores = name_with_filetype.replace("-", "_");
182 let name_with_dashes = name_with_filetype.replace("_", "-");
183
184 let query = if name_with_underscores == name_with_dashes {
185 format!("f:{}", &name_with_underscores)
186 } else {
187 format!("f:{}|{}", &name_with_underscores, &name_with_dashes)
188 };
189
190 let mut code_search_url = Url::parse("https://cs.opensource.google/search").unwrap();
191 code_search_url.query_pairs_mut().append_pair("q", &query).append_pair("ss", "fuchsia/fuchsia");
192
193 code_search_url.into()
194}
195
196pub fn create_dot_graph(instances: Vec<Instance>, orientation: GraphOrientation) -> String {
198 let mut output = GRAPHVIZ_START.to_string();
199
200 match orientation {
202 GraphOrientation::TopToBottom => writeln!(output, r#" rankdir = "TB""#).unwrap(),
203 GraphOrientation::LeftToRight => writeln!(output, r#" rankdir = "LR""#).unwrap(),
204 };
205
206 for instance in &instances {
207 let moniker = instance.moniker.to_string();
208 let label = if let Some(leaf) = instance.moniker.leaf() {
209 leaf.to_string()
210 } else {
211 ".".to_string()
212 };
213
214 let running_attrs =
216 if instance.resolved_info.as_ref().map_or(false, |r| r.execution_info.is_some()) {
217 r##"style = "filled" fontcolor = "#ffffff""##
218 } else {
219 ""
220 };
221
222 let url_attrs = if !instance.url.is_empty() {
224 let code_search_url = construct_codesearch_url(&instance.url);
225 format!(r#"href = "{}""#, code_search_url.as_str())
226 } else {
227 String::new()
228 };
229
230 writeln!(
232 output,
233 r#" "{}" [ label = "{}" {} {} ]"#,
234 &moniker, &label, &running_attrs, &url_attrs
235 )
236 .unwrap();
237
238 if let Some(parent_moniker) = instance.moniker.parent() {
240 if let Some(parent) = instances.iter().find(|i| i.moniker == parent_moniker) {
241 writeln!(output, r#" "{}" -> "{}""#, &parent.moniker.to_string(), &moniker)
243 .unwrap();
244 }
245 }
246 }
247
248 writeln!(output, "{}", GRAPHVIZ_END).unwrap();
249 output
250}
251
252#[cfg(test)]
253mod test {
254 use super::*;
255 use crate::realm::{ExecutionInfo, ResolvedInfo};
256 use moniker::Moniker;
257
258 fn instances_for_test() -> Vec<Instance> {
259 vec![
260 Instance {
261 moniker: Moniker::root(),
262 url: "fuchsia-boot:///#meta/root.cm".to_owned(),
263 environment: None,
264 instance_id: None,
265 resolved_info: Some(ResolvedInfo {
266 resolved_url: "fuchsia-boot:///#meta/root.cm".to_owned(),
267 execution_info: None,
268 }),
269 },
270 Instance {
271 moniker: Moniker::parse_str("appmgr").unwrap(),
272 url: "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_owned(),
273 environment: None,
274 instance_id: None,
275 resolved_info: Some(ResolvedInfo {
276 resolved_url: "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_owned(),
277 execution_info: Some(ExecutionInfo {
278 start_reason: "Debugging Workflow".to_owned(),
279 }),
280 }),
281 },
282 Instance {
283 moniker: Moniker::parse_str("sys").unwrap(),
284 url: "fuchsia-pkg://fuchsia.com/sys#meta/sys.cm".to_owned(),
285 environment: None,
286 instance_id: None,
287 resolved_info: Some(ResolvedInfo {
288 resolved_url: "fuchsia-pkg://fuchsia.com/sys#meta/sys.cm".to_owned(),
289 execution_info: None,
290 }),
291 },
292 Instance {
293 moniker: Moniker::parse_str("sys/baz").unwrap(),
294 url: "fuchsia-pkg://fuchsia.com/baz#meta/baz.cm".to_owned(),
295 environment: None,
296 instance_id: None,
297 resolved_info: Some(ResolvedInfo {
298 resolved_url: "fuchsia-pkg://fuchsia.com/baz#meta/baz.cm".to_owned(),
299 execution_info: Some(ExecutionInfo {
300 start_reason: "Debugging Workflow".to_owned(),
301 }),
302 }),
303 },
304 Instance {
305 moniker: Moniker::parse_str("sys/fuzz").unwrap(),
306 url: "fuchsia-pkg://fuchsia.com/fuzz#meta/fuzz.cm".to_owned(),
307 environment: None,
308 instance_id: None,
309 resolved_info: Some(ResolvedInfo {
310 resolved_url: "fuchsia-pkg://fuchsia.com/fuzz#meta/fuzz.cm".to_owned(),
311 execution_info: None,
312 }),
313 },
314 Instance {
315 moniker: Moniker::parse_str("sys/fuzz/hello").unwrap(),
316 url: "fuchsia-pkg://fuchsia.com/hello#meta/hello.cm".to_owned(),
317 environment: None,
318 instance_id: None,
319 resolved_info: Some(ResolvedInfo {
320 resolved_url: "fuchsia-pkg://fuchsia.com/hello#meta/hello.cm".to_owned(),
321 execution_info: None,
322 }),
323 },
324 ]
325 }
326
327 async fn test_graph_orientation(orientation: GraphOrientation, expected_rankdir: &str) {
331 let instances = instances_for_test();
332
333 let graph = create_dot_graph(instances, orientation);
334 pretty_assertions::assert_eq!(
335 graph,
336 format!(
337 r##"digraph {{
338 graph [ pad = 0.2 ]
339 node [ shape = "box" color = "#2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 target = "_parent" margin = 0.22, ordering = out ];
340 edge [ color = "#37474f" penwidth = 1 arrowhead = none target = "_parent" fontname = "roboto mono" fontsize = 10 ]
341 splines = "ortho"
342 rankdir = "{}"
343 "." [ label = "." href = "https://cs.opensource.google/search?q=f%3Aroot.cml&ss=fuchsia%2Ffuchsia" ]
344 "appmgr" [ label = "appmgr" style = "filled" fontcolor = "#ffffff" href = "https://cs.opensource.google/search?q=f%3Aappmgr.cml&ss=fuchsia%2Ffuchsia" ]
345 "." -> "appmgr"
346 "sys" [ label = "sys" href = "https://cs.opensource.google/search?q=f%3Asys.cml&ss=fuchsia%2Ffuchsia" ]
347 "." -> "sys"
348 "sys/baz" [ label = "baz" style = "filled" fontcolor = "#ffffff" href = "https://cs.opensource.google/search?q=f%3Abaz.cml&ss=fuchsia%2Ffuchsia" ]
349 "sys" -> "sys/baz"
350 "sys/fuzz" [ label = "fuzz" href = "https://cs.opensource.google/search?q=f%3Afuzz.cml&ss=fuchsia%2Ffuchsia" ]
351 "sys" -> "sys/fuzz"
352 "sys/fuzz/hello" [ label = "hello" href = "https://cs.opensource.google/search?q=f%3Ahello.cml&ss=fuchsia%2Ffuchsia" ]
353 "sys/fuzz" -> "sys/fuzz/hello"
354}}
355"##,
356 expected_rankdir
357 )
358 );
359 }
360
361 #[fuchsia_async::run_singlethreaded(test)]
362 async fn test_graph_top_to_bottom_orientation() {
363 test_graph_orientation(GraphOrientation::TopToBottom, "TB").await;
364 }
365
366 #[fuchsia_async::run_singlethreaded(test)]
367 async fn test_graph_left_to_right_orientation() {
368 test_graph_orientation(GraphOrientation::LeftToRight, "LR").await;
369 }
370}