use crate::realm::{get_all_instances, Instance};
use anyhow::Result;
use fidl_fuchsia_sys2 as fsys;
use std::collections::HashSet;
use std::fmt::Write;
use std::str::FromStr;
use url::Url;
static GRAPHVIZ_START: &str = r##"digraph {
graph [ pad = 0.2 ]
node [ shape = "box" color = "#2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 target = "_parent" margin = 0.22, ordering = out ];
edge [ color = "#37474f" penwidth = 1 arrowhead = none target = "_parent" fontname = "roboto mono" fontsize = 10 ]
splines = "ortho"
"##;
static GRAPHVIZ_END: &str = "}";
#[derive(Debug, PartialEq)]
pub enum GraphFilter {
Ancestor(String),
Descendant(String),
Relative(String),
}
impl FromStr for GraphFilter {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(":") {
Some((function, arg)) => match function {
"ancestor" | "ancestors" => Ok(Self::Ancestor(arg.to_string())),
"descendant" | "descendants" => Ok(Self::Descendant(arg.to_string())),
"relative" | "relatives" => Ok(Self::Relative(arg.to_string())),
_ => Err("unknown function for list filter."),
},
None => Err("list filter should be 'ancestors:<component_name>', 'descendants:<component_name>', or 'relatives:<component_name>'."),
}
}
}
#[derive(Debug, PartialEq)]
pub enum GraphOrientation {
TopToBottom,
LeftToRight,
}
impl FromStr for GraphOrientation {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().replace("_", "").replace("-", "").as_str() {
"tb" | "toptobottom" => Ok(GraphOrientation::TopToBottom),
"lr" | "lefttoright" => Ok(GraphOrientation::LeftToRight),
_ => Err("graph orientation should be 'toptobottom' or 'lefttoright'."),
}
}
}
pub async fn graph_cmd<W: std::io::Write>(
filter: Option<GraphFilter>,
orientation: GraphOrientation,
realm_query: fsys::RealmQueryProxy,
mut writer: W,
) -> Result<()> {
let mut instances = get_all_instances(&realm_query).await?;
instances = match filter {
Some(GraphFilter::Ancestor(m)) => filter_ancestors(instances, m),
Some(GraphFilter::Descendant(m)) => filter_descendants(instances, m),
Some(GraphFilter::Relative(m)) => filter_relatives(instances, m),
_ => instances,
};
let output = create_dot_graph(instances, orientation);
writeln!(writer, "{}", output)?;
Ok(())
}
fn filter_ancestors(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
let mut ancestors = HashSet::new();
for instance in &instances {
if let Some(child) = instance.moniker.leaf() {
if child.to_string() == child_str {
let mut cur_moniker = instance.moniker.clone();
ancestors.insert(cur_moniker.clone());
while let Some(parent) = cur_moniker.parent() {
ancestors.insert(parent.clone());
cur_moniker = parent;
}
}
}
}
instances.into_iter().filter(|i| ancestors.contains(&i.moniker)).collect()
}
fn filter_descendants(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
let mut descendants = HashSet::new();
for instance in &instances {
if let Some(child) = instance.moniker.leaf() {
if child.to_string() == child_str {
for possible_child_instance in &instances {
if possible_child_instance.moniker.has_prefix(&instance.moniker) {
descendants.insert(possible_child_instance.moniker.clone());
}
}
}
}
}
instances.into_iter().filter(|i| descendants.contains(&i.moniker)).collect()
}
fn filter_relatives(instances: Vec<Instance>, child_str: String) -> Vec<Instance> {
let mut relatives = HashSet::new();
for instance in &instances {
if let Some(child) = instance.moniker.leaf() {
if child.to_string() == child_str {
let mut cur_moniker = instance.moniker.clone();
while let Some(parent) = cur_moniker.parent() {
relatives.insert(parent.clone());
cur_moniker = parent;
}
for possible_child_instance in &instances {
if possible_child_instance.moniker.has_prefix(&instance.moniker) {
relatives.insert(possible_child_instance.moniker.clone());
}
}
}
}
}
instances.into_iter().filter(|i| relatives.contains(&i.moniker)).collect()
}
fn construct_codesearch_url(component_url: &str) -> String {
let mut name_with_filetype = match component_url.rsplit_once("/") {
Some(parts) => parts.1.to_string(),
None => component_url.to_string(),
};
if name_with_filetype.ends_with(".cm") {
name_with_filetype.push('l');
}
let name_with_underscores = name_with_filetype.replace("-", "_");
let name_with_dashes = name_with_filetype.replace("_", "-");
let query = if name_with_underscores == name_with_dashes {
format!("f:{}", &name_with_underscores)
} else {
format!("f:{}|{}", &name_with_underscores, &name_with_dashes)
};
let mut code_search_url = Url::parse("https://cs.opensource.google/search").unwrap();
code_search_url.query_pairs_mut().append_pair("q", &query).append_pair("ss", "fuchsia/fuchsia");
code_search_url.into()
}
pub fn create_dot_graph(instances: Vec<Instance>, orientation: GraphOrientation) -> String {
let mut output = GRAPHVIZ_START.to_string();
match orientation {
GraphOrientation::TopToBottom => writeln!(output, r#" rankdir = "TB""#).unwrap(),
GraphOrientation::LeftToRight => writeln!(output, r#" rankdir = "LR""#).unwrap(),
};
for instance in &instances {
let moniker = instance.moniker.to_string();
let label = if let Some(leaf) = instance.moniker.leaf() {
leaf.to_string()
} else {
".".to_string()
};
let running_attrs =
if instance.resolved_info.as_ref().map_or(false, |r| r.execution_info.is_some()) {
r##"style = "filled" fontcolor = "#ffffff""##
} else {
""
};
let url_attrs = if !instance.url.is_empty() {
let code_search_url = construct_codesearch_url(&instance.url);
format!(r#"href = "{}""#, code_search_url.as_str())
} else {
String::new()
};
writeln!(
output,
r#" "{}" [ label = "{}" {} {} ]"#,
&moniker, &label, &running_attrs, &url_attrs
)
.unwrap();
if let Some(parent_moniker) = instance.moniker.parent() {
if let Some(parent) = instances.iter().find(|i| i.moniker == parent_moniker) {
writeln!(output, r#" "{}" -> "{}""#, &parent.moniker.to_string(), &moniker)
.unwrap();
}
}
}
writeln!(output, "{}", GRAPHVIZ_END).unwrap();
output
}
#[cfg(test)]
mod test {
use super::*;
use crate::realm::{ExecutionInfo, ResolvedInfo};
use moniker::Moniker;
fn instances_for_test() -> Vec<Instance> {
vec![
Instance {
moniker: Moniker::root(),
url: "fuchsia-boot:///#meta/root.cm".to_owned(),
environment: None,
instance_id: None,
resolved_info: Some(ResolvedInfo {
resolved_url: "fuchsia-boot:///#meta/root.cm".to_owned(),
execution_info: None,
}),
},
Instance {
moniker: Moniker::parse_str("appmgr").unwrap(),
url: "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_owned(),
environment: None,
instance_id: None,
resolved_info: Some(ResolvedInfo {
resolved_url: "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_owned(),
execution_info: Some(ExecutionInfo {
start_reason: "Debugging Workflow".to_owned(),
}),
}),
},
Instance {
moniker: Moniker::parse_str("sys").unwrap(),
url: "fuchsia-pkg://fuchsia.com/sys#meta/sys.cm".to_owned(),
environment: None,
instance_id: None,
resolved_info: Some(ResolvedInfo {
resolved_url: "fuchsia-pkg://fuchsia.com/sys#meta/sys.cm".to_owned(),
execution_info: None,
}),
},
Instance {
moniker: Moniker::parse_str("sys/baz").unwrap(),
url: "fuchsia-pkg://fuchsia.com/baz#meta/baz.cm".to_owned(),
environment: None,
instance_id: None,
resolved_info: Some(ResolvedInfo {
resolved_url: "fuchsia-pkg://fuchsia.com/baz#meta/baz.cm".to_owned(),
execution_info: Some(ExecutionInfo {
start_reason: "Debugging Workflow".to_owned(),
}),
}),
},
Instance {
moniker: Moniker::parse_str("sys/fuzz").unwrap(),
url: "fuchsia-pkg://fuchsia.com/fuzz#meta/fuzz.cm".to_owned(),
environment: None,
instance_id: None,
resolved_info: Some(ResolvedInfo {
resolved_url: "fuchsia-pkg://fuchsia.com/fuzz#meta/fuzz.cm".to_owned(),
execution_info: None,
}),
},
Instance {
moniker: Moniker::parse_str("sys/fuzz/hello").unwrap(),
url: "fuchsia-pkg://fuchsia.com/hello#meta/hello.cm".to_owned(),
environment: None,
instance_id: None,
resolved_info: Some(ResolvedInfo {
resolved_url: "fuchsia-pkg://fuchsia.com/hello#meta/hello.cm".to_owned(),
execution_info: None,
}),
},
]
}
async fn test_graph_orientation(orientation: GraphOrientation, expected_rankdir: &str) {
let instances = instances_for_test();
let graph = create_dot_graph(instances, orientation);
pretty_assertions::assert_eq!(
graph,
format!(
r##"digraph {{
graph [ pad = 0.2 ]
node [ shape = "box" color = "#2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 target = "_parent" margin = 0.22, ordering = out ];
edge [ color = "#37474f" penwidth = 1 arrowhead = none target = "_parent" fontname = "roboto mono" fontsize = 10 ]
splines = "ortho"
rankdir = "{}"
"." [ label = "." href = "https://cs.opensource.google/search?q=f%3Aroot.cml&ss=fuchsia%2Ffuchsia" ]
"appmgr" [ label = "appmgr" style = "filled" fontcolor = "#ffffff" href = "https://cs.opensource.google/search?q=f%3Aappmgr.cml&ss=fuchsia%2Ffuchsia" ]
"." -> "appmgr"
"sys" [ label = "sys" href = "https://cs.opensource.google/search?q=f%3Asys.cml&ss=fuchsia%2Ffuchsia" ]
"." -> "sys"
"sys/baz" [ label = "baz" style = "filled" fontcolor = "#ffffff" href = "https://cs.opensource.google/search?q=f%3Abaz.cml&ss=fuchsia%2Ffuchsia" ]
"sys" -> "sys/baz"
"sys/fuzz" [ label = "fuzz" href = "https://cs.opensource.google/search?q=f%3Afuzz.cml&ss=fuchsia%2Ffuchsia" ]
"sys" -> "sys/fuzz"
"sys/fuzz/hello" [ label = "hello" href = "https://cs.opensource.google/search?q=f%3Ahello.cml&ss=fuchsia%2Ffuchsia" ]
"sys/fuzz" -> "sys/fuzz/hello"
}}
"##,
expected_rankdir
)
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_graph_top_to_bottom_orientation() {
test_graph_orientation(GraphOrientation::TopToBottom, "TB").await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_graph_left_to_right_orientation() {
test_graph_orientation(GraphOrientation::LeftToRight, "LR").await;
}
}