use serde::Serialize;
use crate::safety_lock::{ResolvedRoot, RootInventory};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SafetyPromptCount {
pub category: String,
pub files: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SafetyPromptView {
pub root: String,
pub provenance: String,
pub counts: Vec<SafetyPromptCount>,
pub paths: Vec<String>,
pub omitted: usize,
pub total: usize,
}
impl SafetyPromptView {
pub fn new(root: &ResolvedRoot, inventory: &RootInventory) -> Self {
Self {
root: root.identity().spelling(),
provenance: root.source().label().to_string(),
counts: inventory
.counts
.iter()
.map(|count| SafetyPromptCount {
category: count.category.label().to_string(),
files: count.files,
})
.collect(),
paths: inventory
.sample
.iter()
.map(|entry| entry.relative_path.display().to_string())
.collect(),
omitted: inventory.omitted,
total: inventory.total_files(),
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::safety_lock::{
CategoryCount, InventoryCategory, InventoryEntry, RootIdentity, RootSource,
};
use super::*;
fn root() -> ResolvedRoot {
ResolvedRoot::new(RootIdentity::new("/srv/dots").unwrap(), RootSource::Git)
}
fn inventory() -> RootInventory {
RootInventory {
counts: vec![
CategoryCount {
category: InventoryCategory::Shell,
files: 3,
},
CategoryCount {
category: InventoryCategory::Link,
files: 9,
},
],
sample: vec![
InventoryEntry {
category: InventoryCategory::Shell,
relative_path: PathBuf::from("zsh/aliases.sh"),
},
InventoryEntry {
category: InventoryCategory::Link,
relative_path: PathBuf::from("git/gitconfig"),
},
],
omitted: 10,
}
}
#[test]
fn the_view_names_the_root_the_way_roots_list_will() {
let view = SafetyPromptView::new(&root(), &inventory());
assert_eq!(
view.root,
RootIdentity::new("/srv/dots").unwrap().spelling()
);
assert_eq!(view.provenance, "git top-level");
}
#[test]
fn the_rendered_prompt_names_the_root_its_counts_and_its_sample() {
let rendered = crate::render::render_safety_prompt(
&SafetyPromptView::new(&root(), &inventory()),
standout_render::OutputMode::Text,
)
.unwrap();
for expected in [
"/srv/dots",
"selected by git top-level",
"3 shell",
"9 link",
"zsh/aliases.sh",
"git/gitconfig",
"... and 10 more",
"dodot roots forget",
] {
assert!(
rendered.contains(expected),
"`{expected}` missing from:\n{rendered}"
);
}
assert!(
!rendered.contains("\n\n\n"),
"the prompt padded itself with blank lines:\n{rendered:?}"
);
}
#[test]
fn an_empty_root_says_so_instead_of_rendering_an_empty_inventory() {
let empty = RootInventory {
counts: Vec::new(),
sample: Vec::new(),
omitted: 0,
};
let rendered = crate::render::render_safety_prompt(
&SafetyPromptView::new(&root(), &empty),
standout_render::OutputMode::Text,
)
.unwrap();
assert!(rendered.contains("recognizes no files"), "{rendered}");
assert!(
!rendered.contains("What dodot recognizes there"),
"{rendered}"
);
}
#[test]
fn counts_survive_the_path_cap_that_omitted_their_examples() {
let view = SafetyPromptView::new(&root(), &inventory());
assert_eq!(view.total, 12);
assert_eq!(view.paths.len(), 2);
assert_eq!(view.omitted, 10);
assert_eq!(
view.counts,
vec![
SafetyPromptCount {
category: "shell".into(),
files: 3,
},
SafetyPromptCount {
category: "link".into(),
files: 9,
},
]
);
}
}