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