ci

CI for all possible combinations of features in Cargo.toml.
git clone https://git.philomathiclife.com/repos/ci
Log | Files | Refs | README

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 }