1use 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#[derive(Debug, Parser)]
24pub struct Run {
25 #[clap(long)]
27 repeats: Option<usize>,
28
29 #[clap(long)]
31 logs: PathBuf,
32
33 #[clap(long, default_value = "600")]
35 timeout: usize,
36
37 #[clap(long)]
39 check_tempdir: bool,
40
41 spec: PathBuf,
42}
43
44impl Run {
45 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
138pub 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}