args.rs (17989B)
1 use super::E; 2 use core::{ 3 error::Error, 4 fmt::{self, Display, Formatter}, 5 }; 6 use std::{ 7 collections::HashSet, 8 env, fs, 9 path::PathBuf, 10 process::{Command, Stdio}, 11 }; 12 /// Error returned when parsing arguments passed to the application. 13 #[derive(Clone, Debug)] 14 pub(crate) enum ArgsErr { 15 /// Error when no arguments exist. 16 NoArgs, 17 /// Error when an invalid option is passed. The contained [`String`] is the value of the invalid option. 18 InvalidOption(String), 19 /// Error when an option is passed more than once. The contained [`String`] is the duplicate option. 20 DuplicateOption(String), 21 /// Error when `--dir` is passed with no file path to the directory `ci` should run in. 22 MissingPath, 23 } 24 impl Display for ArgsErr { 25 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 26 match *self { 27 Self::NoArgs => write!( 28 f, 29 "no arguments exist including the name of the process itself" 30 ), 31 Self::InvalidOption(ref arg) => write!( 32 f, 33 "{arg} is an invalid option. No arguments or exactly one of 'clippy', 'doc_tests', 'tests', 'ignored', or 'include-ignored' is allowed followed by nothing, '--color', or '--dir' and the path to the directory ci should run in" 34 ), 35 Self::DuplicateOption(ref arg) => write!(f, "{arg} was passed more than once"), 36 Self::MissingPath => { 37 f.write_str("--dir was passed without a path to the directory ci should run in") 38 } 39 } 40 } 41 } 42 impl Error for ArgsErr {} 43 /// The options passed to the application. 44 #[derive(Clone, Copy, Debug)] 45 pub(crate) enum Opts { 46 /// Variant when no arguments were passed. 47 /// 48 /// The first contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used. 49 /// The second contained `bool` is `true` iff `Self::DocTests` should be ignored. 50 None(bool, bool), 51 /// Variant when `clippy` is passed. 52 /// 53 /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used. 54 Clippy(bool), 55 /// Variant when `doc_tests` is passed. 56 /// 57 /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used. 58 DocTests(bool), 59 /// Variant when `tests` is passed. 60 /// 61 /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used. 62 Tests(bool), 63 /// Variant when `ignored` is passed. 64 /// 65 /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used. 66 IgnoredTests(bool), 67 /// Variant when `include-ignored` is passed. 68 /// 69 /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used. 70 IncludeIgnoredTests(bool), 71 } 72 /// Kind of successful completion of a command. 73 pub(crate) enum Success { 74 /// Ran normally without errors. 75 Normal, 76 /// Erred due to [`compile_error`]. 77 CompileError, 78 /// `cargo t -q --doc` erred since there was no library target. 79 NoLibraryTargets, 80 } 81 impl Opts { 82 /// Returns `true` iff color should be outputted. 83 const fn contains_color(self) -> bool { 84 match self { 85 Self::None(color, _) 86 | Self::Clippy(color) 87 | Self::DocTests(color) 88 | Self::Tests(color) 89 | Self::IgnoredTests(color) 90 | Self::IncludeIgnoredTests(color) => color, 91 } 92 } 93 /// Changes `self` such that it should contain color. 94 const fn set_color(&mut self) { 95 match *self { 96 Self::None(ref mut color, _) 97 | Self::Clippy(ref mut color) 98 | Self::DocTests(ref mut color) 99 | Self::Tests(ref mut color) 100 | Self::IgnoredTests(ref mut color) 101 | Self::IncludeIgnoredTests(ref mut color) => *color = true, 102 } 103 } 104 /// Returns the arguments to pass to the command based on `self`. 105 fn args(self) -> Vec<&'static str> { 106 match self { 107 Self::None(_, _) => Vec::new(), 108 Self::Clippy(color) => vec![ 109 "clippy", 110 "-q", 111 "--color", 112 if color { "always" } else { "never" }, 113 "--no-default-features", 114 ], 115 Self::DocTests(color) => vec![ 116 "t", 117 "-q", 118 "--color", 119 if color { "always" } else { "never" }, 120 "--doc", 121 "--no-default-features", 122 ], 123 Self::Tests(color) | Self::IgnoredTests(color) | Self::IncludeIgnoredTests(color) => { 124 vec![ 125 "t", 126 "-q", 127 "--color", 128 if color { "always" } else { "never" }, 129 "--tests", 130 "--no-default-features", 131 ] 132 } 133 } 134 } 135 /// Returns the appropriate [`Stdio`] to be used to capture `stdout` based on `self`. 136 fn stdout(self) -> Stdio { 137 match self { 138 Self::None(_, _) | Self::Clippy(_) => Stdio::null(), 139 Self::DocTests(_) 140 | Self::Tests(_) 141 | Self::IgnoredTests(_) 142 | Self::IncludeIgnoredTests(_) => Stdio::piped(), 143 } 144 } 145 /// Returns the entire command based on `self`. 146 /// 147 /// Note this is the same as [`Opts::args`] except a `String` containing the space-separated values is returned 148 /// with `"\ncargo "` beginning the `String`. 149 fn cmd_str(self) -> String { 150 let mut cmd = self 151 .args() 152 .into_iter() 153 .fold(String::from("\ncargo "), |mut cmd, val| { 154 cmd.push_str(val); 155 cmd.push(' '); 156 cmd 157 }); 158 _ = cmd.pop(); 159 cmd 160 } 161 /// Runs `cargo` with argument based on `self` and features of `features` returning `Ok(Success::Normal)` iff 162 /// the command ran successfully, `Ok(Success::CompileError)` iff a [`compile_error`] occurred, and 163 /// `Ok(Success::NoLibraryTargets)` iff `Self::DocTests` erred due to there not being any library targets. 164 /// `err_msgs` is used to collect unique output written to `stderr` when the command successfully completes. 165 /// 166 /// `self` is mutated iff `Self::None(_, false)` and `cargo t -q --doc` errors 167 /// due to a lack of library target; in which case, the second `bool` becomes `true`. 168 #[expect(clippy::else_if_without_else, reason = "don't want it")] 169 #[expect(clippy::too_many_lines, reason = "not too many")] 170 pub(crate) fn run_cmd( 171 &mut self, 172 features: &str, 173 err_msgs: &mut HashSet<String>, 174 ) -> Result<Success, E> { 175 match *self { 176 Self::None(color, ref mut skip_doc) => { 177 Self::Clippy(color) 178 .run_cmd(features, err_msgs) 179 .and_then(|success| { 180 // We don't want to run the other commands since they will also have a `compile_error`. 181 if matches!(success, Success::CompileError) { 182 Ok(success) 183 } else { 184 if *skip_doc { 185 Ok(Success::NoLibraryTargets) 186 } else { 187 Self::DocTests(color).run_cmd(features, err_msgs) 188 } 189 .and_then(|success_2| { 190 *skip_doc = matches!(success_2, Success::NoLibraryTargets); 191 Self::Tests(color).run_cmd(features, err_msgs) 192 }) 193 } 194 }) 195 } 196 Self::Clippy(color) 197 | Self::DocTests(color) 198 | Self::Tests(color) 199 | Self::IgnoredTests(color) 200 | Self::IncludeIgnoredTests(color) => { 201 let mut args = self.args(); 202 if !features.is_empty() { 203 args.push("--features"); 204 args.push(features); 205 } 206 if matches!( 207 *self, 208 Self::DocTests(_) 209 | Self::Tests(_) 210 | Self::IgnoredTests(_) 211 | Self::IncludeIgnoredTests(_) 212 ) { 213 args.push("--"); 214 args.push("--color"); 215 args.push(if color { "always" } else { "never" }); 216 } 217 if matches!(*self, Self::IgnoredTests(_)) { 218 args.push("--ignored"); 219 } else if matches!(*self, Self::IncludeIgnoredTests(_)) { 220 args.push("--include-ignored"); 221 } 222 let output = Command::new("cargo") 223 .stderr(Stdio::piped()) 224 .stdin(Stdio::null()) 225 .stdout(self.stdout()) 226 .args(args) 227 .output() 228 .map_err(E::Io)?; 229 if let Some(code) = output.status.code() { 230 match code { 231 0i32 => { 232 if output.stderr.is_empty() { 233 Ok(Success::Normal) 234 } else { 235 return String::from_utf8(output.stderr).map_err(E::Utf8).map( 236 |msg| { 237 _ = err_msgs.insert(msg); 238 Success::Normal 239 }, 240 ); 241 } 242 } 243 101i32 => { 244 /// `"compile_error!"` as a byte string. 245 const COMPILE_ERROR: &[u8; 14] = b"compile_error!"; 246 /// `"no library targets found in package"` as a byte string. 247 const NO_LIB_TARG: &[u8; 35] = b"no library targets found in package"; 248 output 249 .stderr 250 .windows(COMPILE_ERROR.len()) 251 .position(|window| window == COMPILE_ERROR) 252 .map_or_else( 253 || { 254 if matches!(*self, Self::DocTests(_)) 255 && output 256 .stderr 257 .windows(NO_LIB_TARG.len()) 258 .any(|window| window == NO_LIB_TARG) 259 { 260 Ok(Success::NoLibraryTargets) 261 } else { 262 Err(()) 263 } 264 }, 265 |_| Ok(Success::CompileError), 266 ) 267 } 268 _ => Err(()), 269 } 270 } else { 271 Err(()) 272 } 273 .or_else(|()| { 274 String::from_utf8(output.stderr) 275 .map_err(E::Utf8) 276 .and_then(|err| { 277 String::from_utf8(output.stdout).map_err(E::Utf8).and_then( 278 |mut stdout| { 279 let mut msg = if stdout.is_empty() { 280 err 281 } else { 282 stdout.push_str(err.as_str()); 283 stdout 284 }; 285 msg.push_str(self.cmd_str().as_str()); 286 if !features.is_empty() { 287 msg.push_str(" --features "); 288 msg.push_str(features); 289 } 290 if matches!( 291 *self, 292 Self::DocTests(_) 293 | Self::Tests(_) 294 | Self::IgnoredTests(_) 295 | Self::IncludeIgnoredTests(_) 296 ) { 297 msg.push_str(if color { 298 " -- --color always" 299 } else { 300 " -- --color never" 301 }); 302 } 303 if matches!(*self, Self::IgnoredTests(_)) { 304 msg.push_str(" --ignored"); 305 } else if matches!(*self, Self::IncludeIgnoredTests(_)) { 306 msg.push_str(" --include-ignored"); 307 } 308 Err(E::Cmd(msg)) 309 }, 310 ) 311 }) 312 }) 313 } 314 } 315 } 316 /// Returns `Opts` and the directory `ci` should run in based on arguments passed to the application. 317 #[expect(clippy::redundant_else, reason = "when else-if is used, prefer else")] 318 pub(crate) fn from_args() -> Result<(Self, Option<PathBuf>), E> { 319 let mut args = env::args(); 320 if args.next().is_none() { 321 return Err(E::Args(ArgsErr::NoArgs)); 322 } 323 let mut opt = Self::None(false, false); 324 let mut path = None; 325 while let Some(arg) = args.next() { 326 match arg.as_str() { 327 "clippy" => { 328 if !matches!(opt, Self::None(_, _)) { 329 return Err(E::Args(ArgsErr::InvalidOption(String::from("clippy")))); 330 } else if opt.contains_color() { 331 return Err(E::Args(ArgsErr::InvalidOption(String::from("--color")))); 332 } else if path.is_some() { 333 return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir")))); 334 } else { 335 opt = Self::Clippy(false); 336 } 337 } 338 "doc_tests" => { 339 if !matches!(opt, Self::None(_, _)) { 340 return Err(E::Args(ArgsErr::InvalidOption(String::from("doc_tests")))); 341 } else if opt.contains_color() { 342 return Err(E::Args(ArgsErr::InvalidOption(String::from("--color")))); 343 } else if path.is_some() { 344 return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir")))); 345 } else { 346 opt = Self::DocTests(false); 347 } 348 } 349 "tests" => { 350 if !matches!(opt, Self::None(_, _)) { 351 return Err(E::Args(ArgsErr::InvalidOption(String::from("tests")))); 352 } else if opt.contains_color() { 353 return Err(E::Args(ArgsErr::InvalidOption(String::from("--color")))); 354 } else if path.is_some() { 355 return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir")))); 356 } else { 357 opt = Self::Tests(false); 358 } 359 } 360 "ignored" => { 361 if !matches!(opt, Self::None(_, _)) { 362 return Err(E::Args(ArgsErr::InvalidOption(String::from("ignored")))); 363 } else if opt.contains_color() { 364 return Err(E::Args(ArgsErr::InvalidOption(String::from("--color")))); 365 } else if path.is_some() { 366 return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir")))); 367 } else { 368 opt = Self::IgnoredTests(false); 369 } 370 } 371 "include-ignored" => { 372 if !matches!(opt, Self::None(_, _)) { 373 return Err(E::Args(ArgsErr::InvalidOption(String::from( 374 "include-ignored", 375 )))); 376 } else if opt.contains_color() { 377 return Err(E::Args(ArgsErr::InvalidOption(String::from("--color")))); 378 } else if path.is_some() { 379 return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir")))); 380 } else { 381 opt = Self::IncludeIgnoredTests(false); 382 } 383 } 384 "--color" => { 385 if opt.contains_color() { 386 return Err(E::Args(ArgsErr::DuplicateOption(arg))); 387 } 388 opt.set_color(); 389 } 390 "--dir" => { 391 if path.is_some() { 392 return Err(E::Args(ArgsErr::DuplicateOption(arg))); 393 } else if let Some(p) = args.next() { 394 path = Some(fs::canonicalize(p).map_err(E::Io)?); 395 } else { 396 return Err(E::Args(ArgsErr::MissingPath)); 397 } 398 } 399 _ => return Err(E::Args(ArgsErr::InvalidOption(arg))), 400 } 401 } 402 Ok((opt, path)) 403 } 404 }