wumpus_hunter/
run.rs

1//! Sub-command to repeatedly run test suite for the specified
2//! project.
3
4use std::{
5    fs::{create_dir, remove_dir_all, write, OpenOptions},
6    io::Write,
7    path::{Path, PathBuf},
8    time::SystemTime,
9};
10
11use clap::Parser;
12use log::{debug, info, trace};
13use tempfile::tempdir;
14use walkdir::WalkDir;
15
16use radicle::git::Oid;
17
18use crate::{
19    report::Report, runlog::RunLog, spec::Spec, timestamp, Args, FAILURE, STATS_TXT, SUCCESS,
20};
21
22/// Run tests for the project specified in the SPEC file.
23#[derive(Debug, Parser)]
24pub struct Run {
25    /// How many repetitions? Default is no limit.
26    #[clap(long)]
27    repeats: Option<usize>,
28
29    /// Directory where to put HTML log files and report.
30    #[clap(long)]
31    logs: PathBuf,
32
33    /// Maximum time a run of the test suite may take.
34    #[clap(long, default_value = "600")]
35    timeout: usize,
36
37    /// Check if the test suite leaves temporary files behind.
38    #[clap(long)]
39    check_tempdir: bool,
40
41    spec: PathBuf,
42}
43
44impl Run {
45    /// Execute the `run` sub-command.
46    pub fn run(&self, args: &Args) -> anyhow::Result<()> {
47        let spec = Spec::from_file(&self.spec)?;
48
49        debug!("{args:#?}");
50        debug!("{spec:#?}");
51
52        assert!(self.logs.exists());
53        let stats_txt = self.logs.join(STATS_TXT);
54
55        let tmp = tempdir()?;
56        let working_dir = tmp.path().join("srcdir");
57
58        let report_html = self.logs.join("counts.html");
59        let mut i = 0;
60        loop {
61            i += 1;
62            if let Some(repeats) = &self.repeats {
63                if i > *repeats {
64                    break;
65                }
66            }
67            info!("repetition {i}");
68
69            let run_id = timestamp(&SystemTime::now());
70            info!("run {run_id}");
71
72            let mut run_log = RunLog::default();
73            run_log.url(&spec.repository_url);
74            run_log.git_ref(&spec.git_ref);
75
76            spec.versions(&mut run_log)?;
77
78            if working_dir.exists() {
79                spec.git_remote_update(&working_dir, &mut run_log)?;
80                debug!("git pulled");
81            } else {
82                spec.git_clone(&working_dir, &mut run_log)?;
83                debug!("git cloned");
84            }
85            spec.git_checkout(&working_dir, &spec.git_ref, &mut run_log)?;
86            debug!("git checked out");
87
88            let commit = spec.git_head(&working_dir, &mut run_log)?;
89            if let Ok(oid) = Oid::try_from(commit.as_str()) {
90                run_log.git_commit(oid);
91            }
92
93            let test_tmpdir = tmp.path().join("tmp");
94            create_dir(&test_tmpdir)?;
95            debug!("created temporary directory {}", test_tmpdir.display());
96
97            let (mut log, mut success) =
98                spec.run_test_suite(&working_dir, self.timeout, &test_tmpdir, &mut run_log)?;
99            info!("ran test suite: {success}");
100
101            if self.check_tempdir && !dir_is_empty(&test_tmpdir) {
102                log.push_str("\n\n\n# Temporary files left behind\n");
103                success = false;
104                info!("test failed to clean up its temporary files: test failed");
105            }
106
107            remove_dir_all(&test_tmpdir)?;
108
109            if success {
110                record(&stats_txt, &commit, SUCCESS)?;
111            } else {
112                record(&stats_txt, &commit, FAILURE)?;
113            }
114            assert!(stats_txt.exists());
115
116            let log_subdir = commit_dir(&commit);
117            let log_dir = self.logs.join(&log_subdir);
118            if !log_dir.exists() {
119                create_dir(&log_dir)?;
120            }
121            let log_filename = log_dir.join(format!(
122                "log-{run_id}.{i}.{}.html",
123                if success { "success" } else { "fail" }
124            ));
125            write(&log_filename, run_log.as_html().to_string())?;
126
127            let report = Report::new(&spec.description, &stats_txt)?;
128            write(
129                &report_html,
130                report.as_html(&spec, &working_dir).to_string(),
131            )?;
132        }
133
134        Ok(())
135    }
136}
137
138/// Return sub-directory for log files for a commit.
139pub fn commit_dir(commit: &str) -> String {
140    format!("log-{commit}")
141}
142
143fn dir_is_empty(dirname: &Path) -> bool {
144    WalkDir::new(dirname)
145        .min_depth(1)
146        .into_iter()
147        .next()
148        .is_none()
149}
150
151fn record(filename: &Path, commit: &str, result: &str) -> anyhow::Result<()> {
152    trace!("writing {result} on {commit} to {}", filename.display());
153    let mut file = OpenOptions::new()
154        .create(true)
155        .append(true)
156        .open(filename)?;
157    file.write_all(format!("{commit} {result}\n").as_bytes())?;
158    Ok(())
159}