1use anyhow::{anyhow, bail, Result};
2use clap::{error::ErrorKind::ArgumentConflict, Args, Parser};
3use clap::{CommandFactory, Subcommand};
4use env_logger::Env;
5use log::Level;
6use std::path::PathBuf;
7
8use chrono::prelude::*;
9use chrono::Duration;
10
11use crate::audit;
12use crate::basic_measure::measure;
13use crate::config::bump_epoch;
14use crate::data::ReductionFunc;
15use crate::git_interop;
16use crate::git_interop::{prune, pull, push};
17use crate::measurement_storage::{add, remove_measurements_from_commits};
18use crate::reporting::report;
19
20#[derive(Parser)]
21#[command(version)]
22struct Cli {
23 #[arg(short, long, action = clap::ArgAction::Count)]
26 verbose: u8,
27
28 #[command(subcommand)]
29 command: Commands,
30}
31
32#[derive(Args)]
33struct CliMeasurement {
34 #[arg(short = 'm', long = "measurement", value_parser=parse_spaceless_string)]
36 name: String,
37
38 #[arg(short, long, value_parser=parse_key_value)]
40 key_value: Vec<(String, String)>,
41}
42
43#[derive(Args)]
44struct CliReportHistory {
45 #[arg(short = 'n', long, default_value = "40")]
48 max_count: usize,
49}
50
51#[derive(Subcommand)]
52enum Commands {
53 Measure {
55 #[arg(short = 'n', long, value_parser=clap::value_parser!(u16).range(1..), default_value = "1")]
57 repetitions: u16,
58
59 #[command(flatten)]
60 measurement: CliMeasurement,
61
62 #[arg(required(true), last(true))]
64 command: Vec<String>,
65 },
66
67 Add {
69 value: f64,
71
72 #[command(flatten)]
73 measurement: CliMeasurement,
74 },
75
76 Push {},
78
79 Pull {},
81
82 Report {
84 #[arg(short, long, default_value = "output.html")]
86 output: PathBuf,
87
88 #[command(flatten)]
89 report_history: CliReportHistory,
90
91 #[arg(short, long)]
93 measurement: Vec<String>,
94
95 #[arg(short, long, value_parser=parse_key_value)]
97 key_value: Vec<(String, String)>,
98
99 #[arg(short, long, value_parser=parse_spaceless_string)]
101 separate_by: Option<String>,
102
103 #[arg(short, long)]
105 aggregate_by: Option<ReductionFunc>,
106 },
107
108 Audit {
112 #[arg(short, long, value_parser=parse_spaceless_string)]
113 measurement: String,
114
115 #[command(flatten)]
116 report_history: CliReportHistory,
117
118 #[arg(short, long, value_parser=parse_key_value)]
120 selectors: Vec<(String, String)>,
121
122 #[arg(long, value_parser=clap::value_parser!(u16).range(1..), default_value="2")]
128 min_measurements: u16,
129
130 #[arg(short, long, default_value = "min")]
132 aggregate_by: ReductionFunc,
133
134 #[arg(short = 'd', long, default_value = "4.0")]
138 sigma: f64,
139 },
140
141 BumpEpoch {
148 #[arg(short = 'm', long = "measurement", value_parser=parse_spaceless_string)]
149 measurement: String,
150 },
151
152 Remove {
155 #[arg(long = "older-than", value_parser = parse_datetime_value_now)]
156 older_than: DateTime<Utc>,
157 },
158
159 Prune {},
162
163 #[command(hide = true)]
165 Manpage {},
166}
167
168fn parse_key_value(s: &str) -> Result<(String, String)> {
169 let pos = s
170 .find('=')
171 .ok_or_else(|| anyhow!("invalid key=value: no '=' found in '{}'", s))?;
172 let key = parse_spaceless_string(&s[..pos])?;
173 let value = parse_spaceless_string(&s[pos + 1..])?;
174 Ok((key, value))
175}
176
177fn parse_spaceless_string(s: &str) -> Result<String> {
178 if s.split_whitespace().count() > 1 {
179 Err(anyhow!("invalid string/key/value: found space in '{}'", s))
180 } else {
181 Ok(String::from(s))
182 }
183}
184
185fn parse_datetime_value_now(input: &str) -> Result<DateTime<Utc>> {
186 parse_datetime_value(&Utc::now(), input)
187}
188
189fn parse_datetime_value(now: &DateTime<Utc>, input: &str) -> Result<DateTime<Utc>> {
190 if input.len() < 2 {
191 bail!("Invalid datetime format");
192 }
193
194 let (num, unit) = input.split_at(input.len() - 1);
195 let num: i64 = num.parse()?;
196 let subtractor = match unit {
197 "w" => Duration::weeks(num),
198 "d" => Duration::days(num),
199 "h" => Duration::hours(num),
200 _ => bail!("Unsupported datetime format"),
201 };
202 Ok(*now - subtractor)
203}
204
205pub fn handle_calls() -> Result<()> {
206 let cli = Cli::parse();
207 let logger_level = match cli.verbose {
208 0 => Level::Warn,
209 1 => Level::Info,
210 2 => Level::Debug,
211 3 | _ => Level::Trace,
212 };
213 env_logger::Builder::from_env(Env::default().default_filter_or(logger_level.as_str())).init();
214
215 git_interop::check_git_version()?;
216
217 match cli.command {
218 Commands::Measure {
219 repetitions,
220 command,
221 measurement,
222 } => Ok(measure(
223 &measurement.name,
224 repetitions,
225 &command,
226 &measurement.key_value,
227 )?),
228 Commands::Add { value, measurement } => {
229 Ok(add(&measurement.name, value, &measurement.key_value)?)
230 }
231 Commands::Push {} => Ok(push(None)?),
232 Commands::Pull {} => Ok(pull(None)?),
233 Commands::Report {
234 output,
235 separate_by,
236 report_history,
237 measurement,
238 key_value,
239 aggregate_by,
240 } => Ok(report(
241 output,
242 separate_by,
243 report_history.max_count,
244 &measurement,
245 &key_value,
246 aggregate_by,
247 )?),
248 Commands::Audit {
249 measurement,
250 report_history,
251 selectors,
252 min_measurements,
253 aggregate_by,
254 sigma,
255 } => {
256 if report_history.max_count < min_measurements.into() {
257 Cli::command().error(ArgumentConflict, format!("The minimal number of measurements ({}) cannot be more than the maximum number of measurements ({})", min_measurements, report_history.max_count)).exit()
258 }
259 Ok(audit::audit(
260 &measurement,
261 report_history.max_count,
262 min_measurements,
263 &selectors,
264 aggregate_by,
265 sigma,
266 )?)
267 }
268 Commands::BumpEpoch { measurement } => Ok(bump_epoch(&measurement)?),
269 Commands::Prune {} => Ok(prune()?),
270 Commands::Manpage {} => {
271 generate_manpage().expect("Man page generation failed");
272 Ok(())
273 }
274 Commands::Remove { older_than } => remove_measurements_from_commits(older_than),
275 }
276}
277
278fn generate_manpage() -> Result<()> {
279 let man = clap_mangen::Man::new(Cli::command());
280 man.render(&mut std::io::stdout())?;
281
282 for command in Cli::command()
284 .get_subcommands()
285 .filter(|c| !c.is_hide_set())
286 {
287 let man = clap_mangen::Man::new(command.clone());
288 man.render(&mut std::io::stdout())?
289 }
290
291 Ok(())
292}
293
294#[cfg(test)]
295mod test {
296 use super::*;
297
298 #[test]
299 fn verify_cli() {
300 Cli::command().debug_assert()
301 }
302
303 #[test]
304 fn verify_date_parsing() {
305 let now = Utc::now();
306
307 assert_eq!(
308 now - Duration::weeks(2),
309 parse_datetime_value(&now, "2w").unwrap()
310 );
311
312 assert_eq!(
313 now - Duration::days(30),
314 parse_datetime_value(&now, "30d").unwrap()
315 );
316
317 assert_eq!(
318 now - Duration::hours(72),
319 parse_datetime_value(&now, "72h").unwrap()
320 );
321
322 assert!(parse_datetime_value(&now, " 2w ").is_err());
323
324 assert!(parse_datetime_value(&now, "").is_err());
325
326 assert!(parse_datetime_value(&now, "945kjfg").is_err());
327 }
328}