commit 0a7fb27cbcc4c2114f63423d777737d7119d6196
parent f33a34767075f76b41a9ea78bcf431e3b785ca28
Author: Zack Newman <zack@philomathiclife.com>
Date: Mon, 19 Aug 2024 16:55:04 -0600
make color and working dir configurable
Diffstat:
M | Cargo.toml | | | 4 | ++-- |
M | README.md | | | 12 | +++++++----- |
M | src/args.rs | | | 180 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- |
M | src/main.rs | | | 106 | ++++++++++++++++++++++++++++++++++++++++++------------------------------------- |
4 files changed, 198 insertions(+), 104 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,14 +9,14 @@ license = "MIT OR Apache-2.0"
name = "ci"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/ci/"
-rust-version = "1.74.0"
+rust-version = "1.77.0"
version = "0.1.0"
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
-serde = { version = "1.0.207", default-features = false }
+serde = { version = "1.0.208", default-features = false }
toml = { version = "0.8.19", default-features = false, features = ["parse"] }
[profile.release]
diff --git a/README.md b/README.md
@@ -1,7 +1,7 @@
# `ci`
-`ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with `clippy -q --color always`,
-`t -q --color always --doc`, and `t -q --color always --tests` with all possible combinations of features defined in `Cargo.toml`.
+`ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with
+`clippy -q`, `t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`.
`ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables
feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains
@@ -23,11 +23,13 @@ invoking `cargo` with each possible combination of features, this handles it aut
* `clippy`: `cargo clippy -q` is invoked for each combination of features.
* `doc_tests`: `cargo t -q --doc` is invoked for each combination of features.
* `tests`: `cargo t -q --tests` is invoked for each combination of features.
+* `--color`: `--color always` is passed to the above commands; otherwise without this option, `--color never` is
+ passed.
+* `--dir <path to directory Cargo.toml is in>`: `ci` changes the working directory to the passed path (after
+ canonicalizing it) before executing. Without this, the current directory is used.
-When no options are passed, then all three options above are invoked.
+When `clippy`, `doc_tests`, and `tests` are not passed; then all three are invoked.
## Limitations
Any use of `compile_error` _not_ related to incompatible features will be silently ignored.
-
-`ci` must be run in the same directory as the `Cargo.toml` file that contains the features.
diff --git a/src/args.rs b/src/args.rs
@@ -4,6 +4,8 @@ use std::{
collections::HashSet,
env,
error::Error,
+ fs,
+ path::PathBuf,
process::{Command, Stdio},
};
/// Error returned when parsing arguments passed to the application.
@@ -12,16 +14,21 @@ use std::{
pub enum ArgsErr {
/// Error when no arguments exist.
NoArgs,
- /// Error when an invalid option is passed. The contained [`String`]
- /// is the value of the invalid option.
+ /// Error when an invalid option is passed. The contained [`String`] is the value of the invalid option.
InvalidOption(String),
+ /// Error when an option is passed more than once. The contained [`String`] is the duplicate option.
+ DuplicateOption(String),
+ /// Error when `--dir` is passed with no file path to the directory `ci` should run in.
+ MissingPath,
}
impl Display for ArgsErr {
#[allow(clippy::ref_patterns)]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::NoArgs => write!(f, "no arguments exist including the name of the process itself"),
- Self::InvalidOption(ref arg) => write!(f, "{arg} is an invalid option. No arguments or exactly one of 'clippy', 'doc_tests', or 'tests' is allowed"),
+ 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"),
+ Self::DuplicateOption(ref arg) => write!(f, "{arg} was passed more than once"),
+ Self::MissingPath => f.write_str("--dir was passed without a path to the directory ci should run in"),
}
}
}
@@ -31,15 +38,21 @@ impl Error for ArgsErr {}
pub enum Opts {
/// Variant when no arguments were passed.
///
- /// The contained `bool` is `true` iff
- /// `Self::DocTests` should be ignored.
- None(bool),
+ /// The first contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used.
+ /// The second contained `bool` is `true` iff `Self::DocTests` should be ignored.
+ None(bool, bool),
/// Variant when `clippy` is passed.
- Clippy,
+ ///
+ /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used.
+ Clippy(bool),
/// Variant when `doc_tests` is passed.
- DocTests,
+ ///
+ /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used.
+ DocTests(bool),
/// Variant when `tests` is passed.
- Tests,
+ ///
+ /// The contained `bool` is `true` iff `--color always` should be used; otherwise `--color never` is used.
+ Tests(bool),
}
/// Kind of successful completion of a command.
pub enum Success {
@@ -47,29 +60,52 @@ pub enum Success {
Normal,
/// Erred due to [`compile_error`].
CompileError,
- /// `cargo t -q --color always --doc --no-default-features` erred since
- /// there was no library target.
+ /// `cargo t -q --doc` erred since there was no library target.
NoLibraryTargets,
}
impl Opts {
+ /// Returns `true` iff color should be outputted.
+ const fn contains_color(self) -> bool {
+ match self {
+ Self::None(color, _)
+ | Self::Clippy(color)
+ | Self::DocTests(color)
+ | Self::Tests(color) => color,
+ }
+ }
+ /// Changes `self` such that it should contain color.
+ fn set_color(&mut self) {
+ match *self {
+ Self::None(ref mut color, _)
+ | Self::Clippy(ref mut color)
+ | Self::DocTests(ref mut color)
+ | Self::Tests(ref mut color) => *color = true,
+ }
+ }
/// Returns the arguments to pass to the command based on `self`.
fn args(self) -> Vec<&'static str> {
match self {
- Self::None(_) => Vec::new(),
- Self::Clippy => vec!["clippy", "-q", "--color", "always", "--no-default-features"],
- Self::DocTests => vec![
+ Self::None(_, _) => Vec::new(),
+ Self::Clippy(color) => vec![
+ "clippy",
+ "-q",
+ "--color",
+ if color { "always" } else { "never" },
+ "--no-default-features",
+ ],
+ Self::DocTests(color) => vec![
"t",
"-q",
"--color",
- "always",
+ if color { "always" } else { "never" },
"--doc",
"--no-default-features",
],
- Self::Tests => vec![
+ Self::Tests(color) => vec![
"t",
"-q",
"--color",
- "always",
+ if color { "always" } else { "never" },
"--tests",
"--no-default-features",
],
@@ -78,8 +114,8 @@ impl Opts {
/// Returns the appropriate [`Stdio`] to be used to capture `stdout` based on `self`.
fn stdout(self) -> Stdio {
match self {
- Self::None(_) | Self::Clippy => Stdio::null(),
- Self::DocTests | Self::Tests => Stdio::piped(),
+ Self::None(_, _) | Self::Clippy(_) => Stdio::null(),
+ Self::DocTests(_) | Self::Tests(_) => Stdio::piped(),
}
}
/// Returns the entire command based on `self`.
@@ -103,8 +139,8 @@ impl Opts {
/// `Ok(Success::NoLibraryTargets)` iff `Self::DocTests` erred due to there not being any library targets.
/// `err_msgs` is used to collect unique output written to `stderr` when the command successfully completes.
///
- /// `self` is mutated iff `Self::None(false)` and `cargo t -q --color always --doc` errors
- /// due to a lack of library target; in which case, the contained `bool` becomes `true`.
+ /// `self` is mutated iff `Self::None(_, false)` and `cargo t -q --doc` errors
+ /// due to a lack of library target; in which case, the second `bool` becomes `true`.
#[allow(clippy::too_many_lines)]
pub fn run_cmd(
&mut self,
@@ -112,8 +148,8 @@ impl Opts {
err_msgs: &mut HashSet<String>,
) -> Result<Success, E> {
match *self {
- Self::None(ref mut skip_doc) => {
- Self::Clippy
+ Self::None(color, ref mut skip_doc) => {
+ Self::Clippy(color)
.run_cmd(features, err_msgs)
.and_then(|success| {
// We don't want to run the other commands since they will also have a `compile_error`.
@@ -121,27 +157,27 @@ impl Opts {
Ok(success)
} else {
if *skip_doc {
- Ok(Success::Normal)
+ Ok(Success::NoLibraryTargets)
} else {
- Self::DocTests.run_cmd(features, err_msgs)
+ Self::DocTests(color).run_cmd(features, err_msgs)
}
.and_then(|success_2| {
*skip_doc = matches!(success_2, Success::NoLibraryTargets);
- Self::Tests.run_cmd(features, err_msgs)
+ Self::Tests(color).run_cmd(features, err_msgs)
})
}
})
}
- Self::Clippy | Self::DocTests | Self::Tests => {
+ Self::Clippy(color) | Self::DocTests(color) | Self::Tests(color) => {
let mut args = self.args();
if !features.is_empty() {
args.push("--features");
args.push(features);
}
- if matches!(*self, Self::DocTests | Self::Tests) {
+ if matches!(*self, Self::DocTests(_) | Self::Tests(_)) {
args.push("--");
args.push("--color");
- args.push("always");
+ args.push(if color { "always" } else { "never" });
}
let output = Command::new("cargo")
.stderr(Stdio::piped())
@@ -175,7 +211,7 @@ impl Opts {
.position(|window| window == COMPILE_ERROR)
.map_or_else(
|| {
- if matches!(*self, Self::DocTests)
+ if matches!(*self, Self::DocTests(_))
&& output
.stderr
.windows(NO_LIB_TARG.len())
@@ -211,8 +247,12 @@ impl Opts {
msg.push_str(" --features ");
msg.push_str(features);
}
- if matches!(*self, Self::DocTests | Self::Tests) {
- msg.push_str(" -- --color always");
+ if matches!(*self, Self::DocTests(_) | Self::Tests(_)) {
+ msg.push_str(if color {
+ " -- --color always"
+ } else {
+ " -- --color never"
+ });
}
Err(E::Cmd(msg))
},
@@ -222,22 +262,68 @@ impl Opts {
}
}
}
- /// Returns `Opts` based on arguments passed to the application.
- pub fn from_args() -> Result<Self, ArgsErr> {
+ /// Returns `Opts` and the directory `ci` should run in based on arguments passed to the application.
+ #[allow(clippy::redundant_else)]
+ pub fn from_args() -> Result<(Self, Option<PathBuf>), E> {
let mut args = env::args();
- args.next().ok_or(ArgsErr::NoArgs).and_then(|_| {
- args.next().map_or(Ok(Self::None(false)), |arg| {
- match arg.as_str() {
- "clippy" => Ok(Self::Clippy),
- "doc_tests" => Ok(Self::DocTests),
- "tests" => Ok(Self::Tests),
- _ => Err(ArgsErr::InvalidOption(arg)),
+ if args.next().is_none() {
+ return Err(E::Args(ArgsErr::NoArgs));
+ }
+ let mut opt = Self::None(false, false);
+ let mut path = None;
+ while let Some(arg) = args.next() {
+ match arg.as_str() {
+ "clippy" => {
+ if !matches!(opt, Self::None(_, _)) {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("clippy"))));
+ } else if opt.contains_color() {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("--color"))));
+ } else if path.is_some() {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir"))));
+ } else {
+ opt = Self::Clippy(false);
+ }
}
- .and_then(|opts| {
- args.next()
- .map_or(Ok(opts), |opt| Err(ArgsErr::InvalidOption(opt)))
- })
- })
- })
+ "doc_tests" => {
+ if !matches!(opt, Self::None(_, _)) {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("doc_tests"))));
+ } else if opt.contains_color() {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("--color"))));
+ } else if path.is_some() {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir"))));
+ } else {
+ opt = Self::DocTests(false);
+ }
+ }
+ "tests" => {
+ if !matches!(opt, Self::None(_, _)) {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("tests"))));
+ } else if opt.contains_color() {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("--color"))));
+ } else if path.is_some() {
+ return Err(E::Args(ArgsErr::InvalidOption(String::from("--dir"))));
+ } else {
+ opt = Self::Tests(false);
+ }
+ }
+ "--color" => {
+ if opt.contains_color() {
+ return Err(E::Args(ArgsErr::DuplicateOption(arg)));
+ }
+ opt.set_color();
+ }
+ "--dir" => {
+ if path.is_some() {
+ return Err(E::Args(ArgsErr::DuplicateOption(arg)));
+ } else if let Some(p) = args.next() {
+ path = Some(fs::canonicalize(p).map_err(E::Io)?);
+ } else {
+ return Err(E::Args(ArgsErr::MissingPath));
+ }
+ }
+ _ => return Err(E::Args(ArgsErr::InvalidOption(arg))),
+ }
+ }
+ Ok((opt, path))
}
}
diff --git a/src/main.rs b/src/main.rs
@@ -1,7 +1,7 @@
//! # `ci`
//!
-//! `ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with `clippy -q --color always`,
-//! `t -q --color always --doc`, and `t -q --color always --tests` with all possible combinations of features defined in `Cargo.toml`.
+//! `ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with
+//! `clippy -q`, `t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`.
//!
//! `ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables
//! feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains
@@ -23,15 +23,18 @@
//! * `clippy`: `cargo clippy -q` is invoked for each combination of features.
//! * `doc_tests`: `cargo t -q --doc` is invoked for each combination of features.
//! * `tests`: `cargo t -q --tests` is invoked for each combination of features.
+//! * `--color`: `--color always` is passed to the above commands; otherwise without this option, `--color never` is
+//! passed.
+//! * `--dir <path to directory Cargo.toml is in>`: `ci` changes the working directory to the passed path (after
+//! canonicalizing it) before executing. Without this, the current directory is used.
//!
-//! When no options are passed, then all three options above are invoked.
+//! When `clippy`, `doc_tests`, and `tests` are not passed; then all three are invoked.
//!
//! ## Limitations
//!
//! Any use of [`compile_error`] _not_ related to incompatible features will be silently ignored.
-//!
-//! `ci` must be run in the same directory as the `Cargo.toml` file that contains the features.
#![deny(
+ unknown_lints,
future_incompatible,
let_underscore,
missing_docs,
@@ -39,6 +42,7 @@
rust_2018_compatibility,
rust_2018_idioms,
rust_2021_compatibility,
+ rust_2024_compatibility,
unsafe_code,
unused,
warnings,
@@ -73,6 +77,7 @@ use args::{ArgsErr, Opts, Success};
use core::fmt::{self, Display, Formatter};
use std::{
collections::HashSet,
+ env,
error::Error,
fs,
io::{self, Write},
@@ -115,51 +120,52 @@ impl fmt::Debug for E {
}
impl Error for E {}
fn main() -> Result<(), E> {
- Opts::from_args().map_err(E::Args).and_then(|mut opt| {
- fs::read_to_string("Cargo.toml")
- .map_err(E::Io)
- .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
- .and_then(|features_powerset| {
- features_powerset
- .into_iter()
- .try_fold(HashSet::new(), |mut msgs, features| {
- opt.run_cmd(features.as_str(), &mut msgs)
- .and_then(|success| {
- if matches!(success, Success::NoLibraryTargets) {
- // We don't want to bother continuing to call
- // `cargo t -q --color always --doc` once we know
- // there is no library target.
- assert!(matches!(opt, Opts::DocTests), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
- Err(E::NoLibraryTargets)
- } else {
- Ok(msgs)
- }
- })
- })
- .or_else(|e| {
- if matches!(e, E::NoLibraryTargets) {
- Ok(HashSet::new())
- } else {
- Err(e)
- }
- })
- .and_then(|msgs| {
- if msgs.is_empty() {
- Ok(())
- } else {
- io::stderr()
- .lock()
- .write_all(
- msgs.into_iter()
- .fold(String::new(), |mut buffer, msg| {
- buffer.push_str(msg.as_str());
- buffer
- })
- .as_bytes(),
- )
- .map_err(E::Io)
- }
- })
+ Opts::from_args().and_then(|(mut opt, path)| {
+ path.map_or(Ok(()), |p| env::set_current_dir(p).map_err(E::Io)).and_then(|()| {
+ fs::read_to_string("Cargo.toml")
+ .map_err(E::Io)
+ .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
+ .and_then(|features_powerset| {
+ features_powerset
+ .into_iter()
+ .try_fold(HashSet::new(), |mut msgs, features| {
+ opt.run_cmd(features.as_str(), &mut msgs)
+ .and_then(|success| {
+ if matches!(success, Success::NoLibraryTargets) {
+ // We don't want to bother continuing to call `cargo t -q --doc` once we
+ // know there is no library target.
+ assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
+ Err(E::NoLibraryTargets)
+ } else {
+ Ok(msgs)
+ }
+ })
+ })
+ .or_else(|e| {
+ if matches!(e, E::NoLibraryTargets) {
+ Ok(HashSet::new())
+ } else {
+ Err(e)
+ }
+ })
+ .and_then(|msgs| {
+ if msgs.is_empty() {
+ Ok(())
+ } else {
+ io::stderr()
+ .lock()
+ .write_all(
+ msgs.into_iter()
+ .fold(String::new(), |mut buffer, msg| {
+ buffer.push_str(msg.as_str());
+ buffer
+ })
+ .as_bytes(),
+ )
+ .map_err(E::Io)
+ }
+ })
+ })
})
})
}