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