wumpus_hunter/
spec.rs

1//! A wumpus hunter project specification.
2
3use std::{
4    ffi::OsString,
5    fs::File,
6    os::unix::ffi::OsStringExt,
7    path::{Path, PathBuf},
8    process::Command,
9};
10
11use log::{error, trace};
12use serde::Deserialize;
13
14use crate::runlog::RunLog;
15
16/// A project specification.
17#[derive(Debug, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct Spec {
20    /// The project description.
21    ///
22    /// This gets put on the HTML report front page.
23    pub description: String,
24
25    /// The project git repository URL.
26    ///
27    /// This is where the project source is cloned from.
28    pub repository_url: String,
29
30    /// The git ref to use.
31    ///
32    /// This can be anything `git checkout` can check out. Usually it
33    /// is a branch name. If it's a tag or commit id or something else
34    /// that doesn't ever change, the wumpus hunter doesn't get
35    /// changes to the repository and always runs tests for the same
36    /// commit.
37    pub git_ref: String,
38
39    /// The shell command to run to run the test suite.
40    ///
41    /// This will be passed onto the `bash` shell.
42    pub command: String,
43}
44
45impl Spec {
46    /// Load a [`Spec`] from a file.
47    pub fn from_file(filename: &Path) -> anyhow::Result<Self> {
48        let file = File::open(filename)?;
49        let spec: Self = serde_yaml::from_reader(&file)?;
50        Ok(spec)
51    }
52
53    /// Query versions of important tools.
54    pub fn versions(&self, run_log: &mut RunLog) -> anyhow::Result<()> {
55        RunCmd::new(".", run_log)
56            .arg("rustc")
57            .arg("--version")
58            .run()?;
59        RunCmd::new(".", run_log)
60            .arg("cargo")
61            .arg("--version")
62            .run()?;
63        Ok(())
64    }
65
66    /// Clone the specified repository to the desired directory.
67    pub fn git_clone(&self, working_dir: &Path, run_log: &mut RunLog) -> anyhow::Result<()> {
68        RunCmd::new(".", run_log)
69            .arg("git")
70            .arg("clone")
71            .arg(&self.repository_url)
72            .path(working_dir)
73            .run()?;
74        Ok(())
75    }
76
77    /// Update the information about git remotes checked out
78    /// repository.
79    pub fn git_remote_update(
80        &self,
81        working_dir: &Path,
82        run_log: &mut RunLog,
83    ) -> anyhow::Result<()> {
84        RunCmd::new(working_dir, run_log)
85            .arg("git")
86            .arg("remote")
87            .arg("update")
88            .run()?;
89        Ok(())
90    }
91
92    /// Check out the desired ref.
93    pub fn git_checkout(
94        &self,
95        working_dir: &Path,
96        committish: &str,
97        run_log: &mut RunLog,
98    ) -> anyhow::Result<()> {
99        RunCmd::new(working_dir, run_log)
100            .arg("git")
101            .arg("checkout")
102            .arg(committish)
103            .run()?;
104        Ok(())
105    }
106
107    /// Return the commit id currently checked out.
108    pub fn git_head(&self, working_dir: &Path, run_log: &mut RunLog) -> anyhow::Result<String> {
109        let (stdout, _) = RunCmd::new(working_dir, run_log)
110            .arg("git")
111            .arg("rev-parse")
112            .arg("HEAD")
113            .run()?;
114        Ok(stdout.trim().into())
115    }
116
117    /// Return the date for a specified commit.
118    pub fn git_commit_date(
119        &self,
120        working_dir: &Path,
121        commit: &str,
122        run_log: &mut RunLog,
123    ) -> String {
124        if let Ok((stdout, _)) = RunCmd::new(working_dir, run_log)
125            .arg("git")
126            .arg("show")
127            .arg("--pretty=fuller")
128            .arg("--date=iso")
129            .arg(commit)
130            .run()
131        {
132            const PREFIX: &str = "CommitDate: ";
133            let x: Vec<String> = stdout
134                .lines()
135                .filter_map(|line| line.strip_prefix(PREFIX).map(|s| s.to_string()))
136                .collect();
137            if x.len() == 1 {
138                return x[0].clone();
139            }
140        }
141        "(unknown date)".into()
142    }
143
144    /// Run the test suite once.
145    pub fn run_test_suite(
146        &self,
147        working_dir: &Path,
148        timeout: usize,
149        tmpdir: &Path,
150        run_log: &mut RunLog,
151    ) -> anyhow::Result<(String, bool)> {
152        RunCmd::new(working_dir, run_log)
153            .tmpdir(tmpdir)
154            .arg("timeout")
155            .arg(&format!("{}s", timeout))
156            .arg("bash")
157            .arg("-c")
158            .arg(&self.command)
159            .run()
160    }
161}
162
163#[derive(Debug)]
164struct RunCmd<'a> {
165    argv: Vec<OsString>,
166    cwd: PathBuf,
167    tmpdir: Option<PathBuf>,
168    run_log: &'a mut RunLog,
169}
170
171impl<'a> RunCmd<'a> {
172    fn new<P: AsRef<Path>>(cwd: P, run_log: &'a mut RunLog) -> Self {
173        let cwd = cwd.as_ref();
174        if !cwd.exists() {
175            error!("ERROR: directory {} does not exist", cwd.display());
176        }
177        assert!(cwd.exists());
178        Self {
179            argv: vec![],
180            cwd: cwd.into(),
181            tmpdir: None,
182            run_log,
183        }
184    }
185
186    fn tmpdir(mut self, dirname: &Path) -> Self {
187        self.tmpdir = Some(dirname.into());
188        self
189    }
190
191    fn arg(mut self, arg: &str) -> Self {
192        self.argv.push(OsString::from_vec(arg.as_bytes().to_vec()));
193        self
194    }
195
196    fn path(mut self, arg: &Path) -> Self {
197        self.argv.push(arg.as_os_str().into());
198        self
199    }
200
201    fn run(self) -> anyhow::Result<(String, bool)> {
202        trace!("runcmd: {self:#?}");
203        let tmpdir = self.tmpdir.unwrap_or(PathBuf::from("/tmp"));
204        let output = Command::new("bash")
205            .arg("-c")
206            .arg(r#""$@" 2>&1"#)
207            .arg("--")
208            .args(&self.argv)
209            .current_dir(&self.cwd)
210            .env("TMPDIR", &tmpdir)
211            .output()?;
212        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
213        trace!("runcmd: stdout:\n{stdout}");
214        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
215        trace!("runcmd: stderr:\n{stderr}");
216        trace!("runcmd: success? {}", output.status.success());
217
218        let argv: Vec<&str> = self.argv.iter().map(|os| os.to_str().unwrap()).collect();
219
220        self.run_log
221            .runcmd(&argv, output.status.code().unwrap(), &stdout, &stderr);
222        Ok((stdout, output.status.success()))
223    }
224}