wumpus_hunter/
report.rs

1//! Summary report of all test runs so far, as HTML.
2
3use std::{collections::HashMap, path::Path, time::SystemTime};
4
5use html_page::{Element, HtmlPage, Tag};
6use log::trace;
7
8use crate::{
9    run::commit_dir,
10    {runlog::RunLog, spec::Spec, timestamp},
11};
12
13const REFRESH_INTERVAL: &str = "300";
14
15/// A summary report of all test runs.
16pub struct Report {
17    description: String,
18    commits: Vec<(String, bool)>,
19}
20
21impl Report {
22    /// Create a new [`Report`].
23    pub fn new(description: &str, stats_txt: &Path) -> anyhow::Result<Self> {
24        let text = std::fs::read(stats_txt)?;
25        let text = String::from_utf8_lossy(&text);
26        let mut commits = vec![];
27        for line in text.lines() {
28            let mut words = line.split_whitespace();
29            let commit = words.next().unwrap();
30            let result = words.next().unwrap();
31            commits.push((commit.to_string(), result == "SUCCESS"));
32        }
33        trace!("loaded {} commits", commits.len());
34        Ok(Self {
35            description: description.into(),
36            commits,
37        })
38    }
39
40    fn count(&self) -> HashMap<String, (usize, usize)> {
41        let mut counts = HashMap::new();
42        for (commit, success) in self.commits.iter() {
43            let (mut s, mut f) = counts.get(commit).copied().unwrap_or((0, 0));
44            if *success {
45                s += 1;
46            } else {
47                f += 1;
48            }
49            counts.insert(commit.into(), (s, f));
50        }
51        counts
52    }
53
54    fn counts_with_date(
55        spec: &Spec,
56        working_dir: &Path,
57        counts: HashMap<String, (usize, usize)>,
58    ) -> HashMap<String, (String, usize, usize)> {
59        let mut map = HashMap::new();
60        let mut run_log = RunLog::default();
61        for (commit, (successes, failures)) in counts.iter() {
62            let date = spec.git_commit_date(working_dir, commit, &mut run_log);
63            map.insert(commit.to_string(), (date, *successes, *failures));
64        }
65        map
66    }
67
68    /// Produce HTML report.
69    pub fn as_html(&self, spec: &Spec, working_dir: &Path) -> HtmlPage {
70        let counts = self.count();
71        let counts = Self::counts_with_date(spec, working_dir, counts);
72
73        let now = timestamp(&SystemTime::now());
74
75        let mut table = Element::new(Tag::Table).with_child(
76            Element::new(Tag::Tr)
77                .with_child(Element::new(Tag::Th).with_text("date"))
78                .with_child(Element::new(Tag::Th).with_text("commit"))
79                .with_child(
80                    Element::new(Tag::Th)
81                        .with_class("numeric")
82                        .with_text("successes"),
83                )
84                .with_child(
85                    Element::new(Tag::Th)
86                        .with_class("numeric")
87                        .with_text("failures"),
88                ),
89        );
90
91        let mut counts: Vec<_> = counts.iter().collect();
92        counts.sort_by_cached_key(|(_, (date, _, _))| date);
93        counts.reverse();
94
95        for (commit, (date, successes, failures)) in counts.iter() {
96            let successes = format!("{successes}");
97            let failures = format!("{failures}");
98
99            table.push_child(
100                Element::new(Tag::Tr)
101                    .with_child(Element::new(Tag::Td).with_class("date").with_text(date))
102                    .with_child(
103                        Element::new(Tag::Td).with_child(
104                            Element::new(Tag::A)
105                                .with_attribute("href", &commit_dir(commit))
106                                .with_class("commit")
107                                .with_text(commit),
108                        ),
109                    )
110                    .with_child(
111                        Element::new(Tag::Td)
112                            .with_class("numeric")
113                            .with_text(&successes),
114                    )
115                    .with_child(
116                        Element::new(Tag::Td)
117                            .with_class("numeric")
118                            .with_text(&failures),
119                    ),
120            );
121        }
122
123        let total = self.commits.len();
124        HtmlPage::default()
125            .with_head_element(Element::new(Tag::Title).with_text("Wumpus hunter"))
126            .with_head_element(Element::new(Tag::Style).with_text(crate::CSS))
127            .with_head_element(
128                Element::new(Tag::Meta)
129                    .with_attribute("http-equiv", "refresh")
130                    .with_attribute("content", REFRESH_INTERVAL),
131            )
132            .with_body_element(Element::new(Tag::H1).with_text("Wumpus hunter"))
133            .with_body_element(Element::new(Tag::P).with_text(&self.description))
134            .with_body_element(
135                Element::new(Tag::P)
136                    .with_text(&format!("Total of {total} test runs. Last updated "))
137                    .with_text(&now),
138            )
139            .with_body_element(table)
140    }
141}