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