1use anyhow::anyhow;
2use anyhow::Result;
3use clap::{error::ErrorKind::ArgumentConflict, Args, Parser};
4use clap::{CommandFactory, Subcommand};
5use std::path::PathBuf;
6
7use crate::audit;
8use crate::basic_measure::measure;
9use crate::config::bump_epoch;
10use crate::data::ReductionFunc;
11use crate::git_interop::{prune, pull, push};
12use crate::measurement_storage::add;
13use crate::reporting::report;
14
15#[derive(Parser)]
16#[command(version)]
17struct Cli {
18 #[command(subcommand)]
19 command: Commands,
20}
21
22#[derive(Args)]
23struct CliMeasurement {
24 #[arg(short = 'm', long = "measurement", value_parser=parse_spaceless_string)]
26 name: String,
27
28 #[arg(short, long, value_parser=parse_key_value)]
30 key_value: Vec<(String, String)>,
31}
32
33#[derive(Args)]
34struct CliReportHistory {
35 #[arg(short = 'n', long, default_value = "40")]
38 max_count: usize,
39}
40
41#[derive(Subcommand)]
42enum Commands {
43 Measure {
45 #[arg(short = 'n', long, value_parser=clap::value_parser!(u16).range(1..), default_value = "1")]
47 repetitions: u16,
48
49 #[command(flatten)]
50 measurement: CliMeasurement,
51
52 #[arg(required(true), last(true))]
54 command: Vec<String>,
55 },
56
57 Add {
59 value: f64,
61
62 #[command(flatten)]
63 measurement: CliMeasurement,
64 },
65
66 Push {},
68
69 Pull {},
71
72 Report {
74 #[arg(short, long, default_value = "output.html")]
76 output: PathBuf,
77
78 #[command(flatten)]
79 report_history: CliReportHistory,
80
81 #[arg(short, long)]
83 measurement: Vec<String>,
84
85 #[arg(short, long, value_parser=parse_key_value)]
87 key_value: Vec<(String, String)>,
88
89 #[arg(short, long, value_parser=parse_spaceless_string)]
91 separate_by: Option<String>,
92
93 #[arg(short, long)]
95 aggregate_by: Option<ReductionFunc>,
96 },
97
98 Audit {
102 #[arg(short, long, value_parser=parse_spaceless_string)]
103 measurement: String,
104
105 #[command(flatten)]
106 report_history: CliReportHistory,
107
108 #[arg(short, long, value_parser=parse_key_value)]
110 selectors: Vec<(String, String)>,
111
112 #[arg(long, value_parser=clap::value_parser!(u16).range(1..), default_value="2")]
118 min_measurements: u16,
119
120 #[arg(short, long, default_value = "min")]
122 aggregate_by: ReductionFunc,
123
124 #[arg(short = 'd', long, default_value = "4.0")]
128 sigma: f64,
129 },
130
131 BumpEpoch {
138 #[arg(short = 'm', long = "measurement", value_parser=parse_spaceless_string)]
139 measurement: String,
140 },
141
142 Prune {},
145
146 #[command(hide = true)]
148 Manpage {},
149}
150
151fn parse_key_value(s: &str) -> Result<(String, String)> {
152 let pos = s
153 .find('=')
154 .ok_or_else(|| anyhow!("invalid key=value: no '=' found in '{}'", s))?;
155 let key = parse_spaceless_string(&s[..pos])?;
156 let value = parse_spaceless_string(&s[pos + 1..])?;
157 Ok((key, value))
158}
159
160fn parse_spaceless_string(s: &str) -> Result<String> {
161 if s.split_whitespace().count() > 1 {
162 Err(anyhow!("invalid string/key/value: found space in '{}'", s))
163 } else {
164 Ok(String::from(s))
165 }
166}
167
168pub fn handle_calls() -> Result<()> {
169 let cli = Cli::parse();
170
171 match cli.command {
172 Commands::Measure {
173 repetitions,
174 command,
175 measurement,
176 } => Ok(measure(
177 &measurement.name,
178 repetitions,
179 &command,
180 &measurement.key_value,
181 )?),
182 Commands::Add { value, measurement } => {
183 Ok(add(&measurement.name, value, &measurement.key_value)?)
184 }
185 Commands::Push {} => Ok(push(None)?),
186 Commands::Pull {} => Ok(pull(None)?),
187 Commands::Report {
188 output,
189 separate_by,
190 report_history,
191 measurement,
192 key_value,
193 aggregate_by,
194 } => Ok(report(
195 output,
196 separate_by,
197 report_history.max_count,
198 &measurement,
199 &key_value,
200 aggregate_by,
201 )?),
202 Commands::Audit {
203 measurement,
204 report_history,
205 selectors,
206 min_measurements,
207 aggregate_by,
208 sigma,
209 } => {
210 if report_history.max_count < min_measurements.into() {
211 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()
212 }
213 Ok(audit::audit(
214 &measurement,
215 report_history.max_count,
216 min_measurements,
217 &selectors,
218 aggregate_by,
219 sigma,
220 )?)
221 }
222 Commands::BumpEpoch { measurement } => Ok(bump_epoch(&measurement)?),
223 Commands::Prune {} => Ok(prune()?),
224 Commands::Manpage {} => {
225 generate_manpage().expect("Man page generation failed");
226 Ok(())
227 }
228 }
229}
230
231fn generate_manpage() -> Result<()> {
232 let man = clap_mangen::Man::new(Cli::command());
233 man.render(&mut std::io::stdout())?;
234
235 for command in Cli::command()
237 .get_subcommands()
238 .filter(|c| !c.is_hide_set())
239 {
240 let man = clap_mangen::Man::new(command.clone());
241 man.render(&mut std::io::stdout())?
242 }
243
244 Ok(())
245}
246
247#[cfg(test)]
248mod test {
249 use super::*;
250
251 #[test]
252 fn verify_cli() {
253 Cli::command().debug_assert()
254 }
255}