|
@@ -137,3 +137,202 @@ async fn build_summary_excludes_down_clusters_and_sorts_nodes() {
|
|
|
);
|
|
);
|
|
|
assert!(!summary.by_recipe["ds4"].contains_key("down"));
|
|
assert!(!summary.by_recipe["ds4"].contains_key("down"));
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/// Renderer tests for [`super::render`] — plain, markdown, and HTML —
|
|
|
|
|
+/// reusing the collection-model fixtures above.
|
|
|
|
|
+mod render {
|
|
|
|
|
+ use std::collections::HashMap;
|
|
|
|
|
+
|
|
|
|
|
+ use super::super::FleetHealth;
|
|
|
|
|
+ use super::super::render::render_fleet;
|
|
|
|
|
+ use super::{HealthReport, RecipesSummary, node, report};
|
|
|
|
|
+ use crate::cli::OutputFormat;
|
|
|
|
|
+ use crate::tmux::discover::TmuxHealthEntry;
|
|
|
|
|
+
|
|
|
|
|
+ fn fleet(clusters: Vec<HealthReport>, recipes: Option<RecipesSummary>) -> FleetHealth {
|
|
|
|
|
+ FleetHealth { clusters, recipes }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn container(name: &str, status: &str, image: &str) -> String {
|
|
|
|
|
+ format!("{name}\t{status}\t{image}")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn up_report() -> HealthReport {
|
|
|
|
|
+ let mut r = report("c1", false);
|
|
|
|
|
+ r.nodes = vec![node(
|
|
|
|
|
+ "n0",
|
|
|
|
|
+ true,
|
|
|
|
|
+ &[&container("app", "Up 2 hours", "img:tag")],
|
|
|
|
|
+ )];
|
|
|
|
|
+ r
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn down_report() -> HealthReport {
|
|
|
|
|
+ let mut r = report("down", false);
|
|
|
|
|
+ r.verdict.up = false;
|
|
|
|
|
+ r.verdict.free = 0;
|
|
|
|
|
+ r.nodes = vec![
|
|
|
|
|
+ node("alive", true, &[&container("secret", "Up", "img")]),
|
|
|
|
|
+ node("dead", false, &[]),
|
|
|
|
|
+ ];
|
|
|
|
|
+ r
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn tmux_entry(recipe: &str, node_name: &str, session: bool, running: bool) -> TmuxHealthEntry {
|
|
|
|
|
+ TmuxHealthEntry {
|
|
|
|
|
+ recipe_display: recipe.into(),
|
|
|
|
|
+ node_name: node_name.into(),
|
|
|
|
|
+ session_exists: session,
|
|
|
|
|
+ process_running: running,
|
|
|
|
|
+ api_ok: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn tmux_report(entries: Vec<TmuxHealthEntry>) -> HealthReport {
|
|
|
|
|
+ let mut r = report("c1", false);
|
|
|
|
|
+ r.tmux = entries;
|
|
|
|
|
+ r
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn render_fleet_handles_each_format() {
|
|
|
|
|
+ let f = fleet(vec![up_report()], None);
|
|
|
|
|
+ let plain = render_fleet(&f, OutputFormat::Plain);
|
|
|
|
|
+ let md = render_fleet(&f, OutputFormat::Md);
|
|
|
|
|
+ let html = render_fleet(&f, OutputFormat::Html);
|
|
|
|
|
+ assert!(plain.contains("=== c1 ==="));
|
|
|
|
|
+ // Container columns parsed out of the tab-separated lines.
|
|
|
|
|
+ assert!(plain.contains("app Up 2 hours img:tag"));
|
|
|
|
|
+ assert!(!plain.contains('\t'), "plain must not emit raw tab blobs");
|
|
|
|
|
+ assert!(md.contains("## c1"));
|
|
|
|
|
+ assert!(html.contains("<h2>c1</h2>"));
|
|
|
|
|
+ assert_ne!(plain, md);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn html_escapes_hostile_names() {
|
|
|
|
|
+ let mut r = report("a<b>&\"c'", false);
|
|
|
|
|
+ r.nodes = vec![node(
|
|
|
|
|
+ "<script>",
|
|
|
|
|
+ true,
|
|
|
|
|
+ &[&container("evil&img", "Up", "<x>")],
|
|
|
|
|
+ )];
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![r], None), OutputFormat::Html);
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ out.contains("<script>"),
|
|
|
|
|
+ "node name must be escaped: {out}"
|
|
|
|
|
+ );
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ out.contains("evil&img"),
|
|
|
|
|
+ "container name must be escaped: {out}"
|
|
|
|
|
+ );
|
|
|
|
|
+ assert!(!out.contains("<script>"));
|
|
|
|
|
+ assert!(!out.contains("evil&img"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn md_renders_headings_and_tables() {
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![up_report()], None), OutputFormat::Md);
|
|
|
|
|
+ assert!(out.contains("## c1"));
|
|
|
|
|
+ assert!(out.contains("| Node | Address | Status | Containers |"));
|
|
|
|
|
+ assert!(out.contains("| --- | --- | --- | --- |"));
|
|
|
|
|
+ assert!(out.contains("| app | n0 | Up 2 hours | img:tag |"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn md_neutralizes_pipes_in_cells() {
|
|
|
|
|
+ let mut r = report("c1", false);
|
|
|
|
|
+ r.nodes = vec![node("n0", true, &[&container("bad|name", "Up", "img")])];
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![r], None), OutputFormat::Md);
|
|
|
|
|
+ assert!(out.contains("bad\\|name"), "pipe must be escaped: {out}");
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ out.contains("| bad\\|name | n0 | Up | img |"),
|
|
|
|
|
+ "table structure must stay intact: {out}"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn down_cluster_suppresses_container_data() {
|
|
|
|
|
+ for fmt in [OutputFormat::Plain, OutputFormat::Md, OutputFormat::Html] {
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![down_report()], None), fmt);
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ !out.contains("secret"),
|
|
|
|
|
+ "{fmt:?} must suppress containers: {out}"
|
|
|
|
|
+ );
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ out.contains("offline"),
|
|
|
|
|
+ "{fmt:?} must show the offline node: {out}"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn recipes_none_renders_no_recipes_section() {
|
|
|
|
|
+ for fmt in [OutputFormat::Plain, OutputFormat::Md, OutputFormat::Html] {
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![up_report()], None), fmt);
|
|
|
|
|
+ assert!(!out.contains("Running recipes:"), "{fmt:?}");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn tmux_all_stopped_shows_summary_line() {
|
|
|
|
|
+ for fmt in [OutputFormat::Plain, OutputFormat::Md, OutputFormat::Html] {
|
|
|
|
|
+ let r = tmux_report(vec![tmux_entry("rec", "dgx1", false, false)]);
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![r], Some(RecipesSummary::default())), fmt);
|
|
|
|
|
+ assert!(out.contains("all stopped"), "{fmt:?}: {out}");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn tmux_running_entry_rendered_stopped_skipped() {
|
|
|
|
|
+ for fmt in [OutputFormat::Plain, OutputFormat::Md, OutputFormat::Html] {
|
|
|
|
|
+ let r = tmux_report(vec![
|
|
|
|
|
+ tmux_entry("rec", "dgx1", true, true),
|
|
|
|
|
+ tmux_entry("rec", "dgx2", false, false),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ let out = render_fleet(&fleet(vec![r], None), fmt);
|
|
|
|
|
+ assert!(out.contains("rec \u{2014} dgx1"), "{fmt:?}: {out}");
|
|
|
|
|
+ assert!(!out.contains("dgx2"), "{fmt:?} must skip stopped: {out}");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn recipes_summary_is_sorted_and_deterministic() {
|
|
|
|
|
+ let build = || {
|
|
|
|
|
+ let mut summary = RecipesSummary::default();
|
|
|
|
|
+ let mut z = HashMap::new();
|
|
|
|
|
+ z.insert("zcluster".to_string(), vec!["n1".to_string()]);
|
|
|
|
|
+ summary.by_recipe.insert("zebra".to_string(), z);
|
|
|
|
|
+ let mut a = HashMap::new();
|
|
|
|
|
+ a.insert("c".to_string(), vec!["n2".to_string()]);
|
|
|
|
|
+ summary.by_recipe.insert("alpha".to_string(), a);
|
|
|
|
|
+ FleetHealth {
|
|
|
|
|
+ clusters: vec![],
|
|
|
|
|
+ recipes: Some(summary),
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ let first = render_fleet(&build(), OutputFormat::Plain);
|
|
|
|
|
+ let second = render_fleet(&build(), OutputFormat::Plain);
|
|
|
|
|
+ assert_eq!(first, second, "rendering must be deterministic");
|
|
|
|
|
+ let alpha = first.find("alpha").expect("alpha rendered");
|
|
|
|
|
+ let zebra = first.find("zebra").expect("zebra rendered");
|
|
|
|
|
+ assert!(alpha < zebra, "recipes must be sorted");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[tokio::test]
|
|
|
|
|
+ async fn renders_in_active_locale() {
|
|
|
|
|
+ crate::locale::set_global_locale(Some("ru-RU".to_string()));
|
|
|
|
|
+ let out = crate::locale::spawn_with_locale(async {
|
|
|
|
|
+ render_fleet(
|
|
|
|
|
+ &fleet(vec![], Some(RecipesSummary::default())),
|
|
|
|
|
+ OutputFormat::Plain,
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ out.contains("Нет запущенных"),
|
|
|
|
|
+ "expected Russian no-recipes line: {out}"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|