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