pipecircuit-visualization 0.0.2

Code-derived architectural visualizations for Pipecircuit programs
Documentation
use std::{
    collections::{BTreeMap, BTreeSet},
    env,
    error::Error,
    fmt, io,
    path::{Path, PathBuf},
    process::{Command, Output},
    string::FromUtf8Error,
};

#[derive(Debug)]
pub enum BehaviorError {
    CurrentExecutable(io::Error),
    DiscoveryIo {
        executable: PathBuf,
        source: io::Error,
    },
    DiscoveryFailed {
        executable: PathBuf,
        output: Output,
    },
    ListingUtf8(FromUtf8Error),
    InvalidListing {
        line: String,
    },
    DuplicateCheck {
        name: String,
    },
    UnknownIgnoredCheck {
        name: String,
    },
    InvalidSnapshotModule {
        module: String,
    },
    EmptyScope {
        module: String,
    },
    RecursiveDiscovery,
}

impl fmt::Display for BehaviorError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::CurrentExecutable(source) => {
                write!(formatter, "could not locate the test executable: {source}")
            }
            Self::DiscoveryIo { executable, source } => write!(
                formatter,
                "could not list tests in `{}`: {source}",
                executable.display()
            ),
            Self::DiscoveryFailed { executable, output } => write!(
                formatter,
                "test listing in `{}` failed with {}",
                executable.display(),
                output.status
            ),
            Self::ListingUtf8(source) => write!(formatter, "test listing was not UTF-8: {source}"),
            Self::InvalidListing { line } => {
                write!(formatter, "unrecognized test listing line: `{line}`")
            }
            Self::DuplicateCheck { name } => {
                write!(formatter, "test `{name}` was listed more than once")
            }
            Self::UnknownIgnoredCheck { name } => write!(
                formatter,
                "ignored test `{name}` was absent from the complete listing"
            ),
            Self::InvalidSnapshotModule { module } => write!(
                formatter,
                "snapshot module `{module}` was not a fully qualified child module"
            ),
            Self::EmptyScope { module } => {
                write!(formatter, "no compiled checks were found beside `{module}`")
            }
            Self::RecursiveDiscovery => formatter.write_str(
                "behavior discovery must run inside a libtest test, not its listing process",
            ),
        }
    }
}

impl Error for BehaviorError {}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CheckDisposition {
    Runnable,
    Ignored,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BehaviorCheck {
    qualified_name: String,
    disposition: CheckDisposition,
}

impl BehaviorCheck {
    pub fn qualified_name(&self) -> &str {
        &self.qualified_name
    }

    pub const fn disposition(&self) -> CheckDisposition {
        self.disposition
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BehaviorInventory {
    checks: Vec<BehaviorCheck>,
}

impl BehaviorInventory {
    /// Call from a dedicated snapshot module beside the checks' owning module.
    /// Discovers this compiled libtest target, not other targets or Kani harnesses.
    pub fn for_snapshot(snapshot_module: &str) -> Result<Self, BehaviorError> {
        if env::args_os().any(|argument| argument == "--list") {
            return Err(BehaviorError::RecursiveDiscovery);
        }
        let executable = env::current_exe().map_err(BehaviorError::CurrentExecutable)?;
        Self::from_test_binary(executable)?.beside_snapshot(snapshot_module)
    }

    /// The executable must be a trusted Rust libtest binary supporting `--list`.
    pub fn from_test_binary(executable: impl AsRef<Path>) -> Result<Self, BehaviorError> {
        let executable = executable.as_ref();
        let list = |ignored| {
            let mut command = Command::new(executable);
            command.args(["--list", "--format", "terse"]);
            if ignored {
                command.arg("--ignored");
            }
            let output = command
                .output()
                .map_err(|source| BehaviorError::DiscoveryIo {
                    executable: executable.to_owned(),
                    source,
                })?;
            test_listing(executable, output)
        };
        Self::from_listings(&list(false)?, &list(true)?)
    }

    fn from_listings(all: &str, ignored: &str) -> Result<Self, BehaviorError> {
        let all = test_names(all)?;
        let ignored = test_names(ignored)?;
        if let Some(name) = ignored.difference(&all).next() {
            return Err(BehaviorError::UnknownIgnoredCheck { name: name.clone() });
        }
        Ok(Self {
            checks: all
                .into_iter()
                .map(|qualified_name| {
                    let disposition = if ignored.contains(&qualified_name) {
                        CheckDisposition::Ignored
                    } else {
                        CheckDisposition::Runnable
                    };
                    BehaviorCheck {
                        qualified_name,
                        disposition,
                    }
                })
                .collect(),
        })
    }

    fn beside_snapshot(mut self, snapshot_module: &str) -> Result<Self, BehaviorError> {
        let invalid = || BehaviorError::InvalidSnapshotModule {
            module: snapshot_module.to_owned(),
        };
        let (crate_name, relative_module) = snapshot_module.split_once("::").ok_or_else(invalid)?;
        if crate_name.is_empty() || relative_module.split("::").any(str::is_empty) {
            return Err(invalid());
        }
        let parent = relative_module
            .rsplit_once("::")
            .map_or("", |(parent, _)| parent);
        let owner_prefix = format!("{parent}::");
        let snapshot_prefix = format!("{relative_module}::");
        self.checks.retain(|check| {
            (parent.is_empty() || check.qualified_name.starts_with(&owner_prefix))
                && !check.qualified_name.starts_with(&snapshot_prefix)
        });
        if self.checks.is_empty() {
            return Err(BehaviorError::EmptyScope {
                module: snapshot_module.to_owned(),
            });
        }
        Ok(self)
    }

    pub fn checks(&self) -> &[BehaviorCheck] {
        &self.checks
    }

    pub fn render(&self) -> String {
        let mut sentences = BTreeMap::<String, Vec<&BehaviorCheck>>::new();
        for check in &self.checks {
            let name = check
                .qualified_name
                .rsplit("::")
                .next()
                .unwrap_or(&check.qualified_name);
            let words = name.replace('_', " ");
            let sentence = words
                .chars()
                .take(1)
                .flat_map(char::to_uppercase)
                .chain(words.chars().skip(1))
                .collect();
            sentences.entry(sentence).or_default().push(check);
        }
        let mut rendering = String::new();
        for (sentence, checks) in sentences {
            for check in &checks {
                rendering.push_str("- ");
                rendering.push_str(&sentence);
                if checks.len() > 1 {
                    rendering.push_str(" (");
                    rendering.push_str(&check.qualified_name);
                    rendering.push(')');
                }
                match check.disposition {
                    CheckDisposition::Runnable => {}
                    CheckDisposition::Ignored => rendering.push_str(" [ignored]"),
                }
                rendering.push('\n');
            }
        }
        rendering
    }
}

fn test_listing(executable: &Path, output: Output) -> Result<String, BehaviorError> {
    if !output.status.success() {
        return Err(BehaviorError::DiscoveryFailed {
            executable: executable.to_owned(),
            output,
        });
    }
    String::from_utf8(output.stdout).map_err(BehaviorError::ListingUtf8)
}

fn test_names(listing: &str) -> Result<BTreeSet<String>, BehaviorError> {
    let mut names = BTreeSet::new();
    for line in listing.lines().filter(|line| !line.is_empty()) {
        if let Some(name) = line.strip_suffix(": test") {
            if name.is_empty() || name.split("::").any(str::is_empty) {
                return Err(BehaviorError::InvalidListing {
                    line: line.to_owned(),
                });
            }
            if !names.insert(name.to_owned()) {
                return Err(BehaviorError::DuplicateCheck {
                    name: name.to_owned(),
                });
            }
        } else if !line.ends_with(": benchmark") {
            return Err(BehaviorError::InvalidListing {
                line: line.to_owned(),
            });
        }
    }
    Ok(names)
}

#[cfg(test)]
mod tests;

#[cfg(test)]
mod snapshot;