diff --git a/Cargo.lock b/Cargo.lock index 1cdb7cf617e..cae61be062a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,12 +167,14 @@ dependencies = [ "camino", "cfg-if", "clap", + "clap_mangen", "color-eyre", "dialoguer", "duct", "enable-ansi-support", "env_logger", "guppy", + "home", "itertools", "log", "miette", @@ -292,6 +294,16 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_mangen" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16658e2f46d5269f95e4ec0f16594524cfc1e51637af40b2e5118c7c71a9fe1" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "color-eyre" version = "0.6.1" @@ -1653,6 +1665,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/cargo-nextest/Cargo.toml b/cargo-nextest/Cargo.toml index a01e9d7683f..08e6ae1940a 100644 --- a/cargo-nextest/Cargo.toml +++ b/cargo-nextest/Cargo.toml @@ -15,6 +15,7 @@ rust-version = "1.59" camino = "1.0.9" cfg-if = "1.0.0" clap = { version = "3.2.6", features = ["derive", "env"] } +clap_mangen = "0.1.9" # we don't use the tracing support color-eyre = { version = "0.6.1", default-features = false } dialoguer = "0.10.1" @@ -23,6 +24,7 @@ enable-ansi-support = "0.1.2" # we don't use the default formatter so we don't need default features env_logger = { version = "0.9.0", default-features = false } guppy = "0.14.2" +home = "0.5.3" log = "0.4.17" itertools = "0.10.3" miette = { version = "4.7.1", features = ["fancy"] } diff --git a/cargo-nextest/src/dispatch.rs b/cargo-nextest/src/dispatch.rs index 425547f81b6..9c8ed6f6d69 100644 --- a/cargo-nextest/src/dispatch.rs +++ b/cargo-nextest/src/dispatch.rs @@ -3,6 +3,7 @@ use crate::{ cargo_cli::{CargoCli, CargoOptions}, + mangen::install_man, output::{OutputContext, OutputOpts, OutputWriter}, reuse_build::{make_path_mapper, ArchiveFormatOpt, ReuseBuildOpts}, ExpectedError, Result, ReuseBuildKind, @@ -55,14 +56,20 @@ impl CargoNextestApp { } #[derive(Debug, Subcommand)] -enum NextestSubcommand { +pub(crate) enum NextestSubcommand { /// A next-generation test runner for Rust. Nextest(AppOpts), } -#[derive(Debug, Args)] +/// cargo-nextest is a next-generation test runner for Rust projects. +/// +/// Nextest runs tests in parallel and provides a rich set of features, such as partitioning test +/// runs, JUnit output, and archiving and reusing builds. +/// +/// For the full documentation, see the nextest site at . +#[derive(Debug, Parser)] #[clap(version)] -struct AppOpts { +pub(crate) struct AppOpts { /// Path to Cargo.toml #[clap(long, global = true, value_name = "PATH")] manifest_path: Option, @@ -1018,6 +1025,12 @@ impl App { #[derive(Debug, Subcommand)] enum SelfCommand { + /// Install man pages for nextest. + InstallMan { + /// The output directory [default: /../man] + output_dir: Option, + }, + #[cfg_attr( not(feature = "self-update"), doc = "This version of nextest does not have self-update enabled\n\ @@ -1064,6 +1077,10 @@ impl SelfCommand { let output = output.init(); match self { + Self::InstallMan { output_dir } => { + install_man(output_dir)?; + Ok(0) + } Self::Update { version, check, diff --git a/cargo-nextest/src/errors.rs b/cargo-nextest/src/errors.rs index 4b425948b0a..63c3093ffb0 100644 --- a/cargo-nextest/src/errors.rs +++ b/cargo-nextest/src/errors.rs @@ -172,6 +172,11 @@ pub enum ExpectedError { reason: &'static str, args: Vec, }, + #[error(transparent)] + InstallManError { + #[from] + error: InstallManError, + }, } impl ExpectedError { @@ -311,6 +316,7 @@ impl ExpectedError { NextestExitCode::EXPERIMENTAL_FEATURE_NOT_ENABLED } Self::FilterExpressionParseError { .. } => NextestExitCode::INVALID_FILTER_EXPRESSION, + Self::InstallManError { .. } => NextestExitCode::INSTALL_MAN_ERROR, } } @@ -529,6 +535,11 @@ impl ExpectedError { ); None } + Self::InstallManError { error } => { + // This is a transparent error. + log::error!("{}", error); + error.source() + } }; while let Some(err) = next_error { @@ -537,3 +548,25 @@ impl ExpectedError { } } } + +#[derive(Debug, Error)] +#[doc(hidden)] +pub enum InstallManError { + #[error("could not determine current executable path")] + CurrentExe { + #[source] + error: std::io::Error, + }, + #[error("error creating output directory `{path}`")] + CreateOutputDir { + path: Utf8PathBuf, + #[source] + error: std::io::Error, + }, + #[error("error writing to `{path}`")] + WriteToFile { + path: Utf8PathBuf, + #[source] + error: std::io::Error, + }, +} diff --git a/cargo-nextest/src/lib.rs b/cargo-nextest/src/lib.rs index f95d7fdfe84..d61d88e9839 100644 --- a/cargo-nextest/src/lib.rs +++ b/cargo-nextest/src/lib.rs @@ -16,6 +16,7 @@ mod cargo_cli; mod dispatch; mod errors; +mod mangen; mod output; mod reuse_build; #[cfg(feature = "self-update")] diff --git a/cargo-nextest/src/mangen.rs b/cargo-nextest/src/mangen.rs new file mode 100644 index 00000000000..b11554faba0 --- /dev/null +++ b/cargo-nextest/src/mangen.rs @@ -0,0 +1,58 @@ +// Copyright (c) The nextest Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::{AppOpts, InstallManError}; +use camino::{Utf8Path, Utf8PathBuf}; +use clap::CommandFactory; +use clap_mangen::Man; + +pub(crate) fn install_man(output_dir: Option) -> Result<(), InstallManError> { + let mut output_dir = match output_dir { + Some(d) => d, + None => { + let mut current_exe = std::env::current_exe() + .and_then(|home| { + Utf8PathBuf::try_from(home).map_err(|error| { + std::io::Error::new(std::io::ErrorKind::InvalidData, error) + }) + }) + .map_err(|error| InstallManError::CurrentExe { error })?; + // If the current exe is foo/bar/bin/cargo-nextest, the man directory is foo/bar/man. + current_exe.pop(); + current_exe.pop(); + current_exe.push("man"); + current_exe + } + }; + + // All of nextest's commands go in man1. + output_dir.push("man1"); + + std::fs::create_dir_all(&output_dir).map_err(|error| InstallManError::CreateOutputDir { + path: output_dir.clone(), + error, + })?; + + let command = AppOpts::command(); + + let man = Man::new(command.clone()).manual("Nextest Manual"); + let path = output_dir.join("cargo-nextest.1"); + render_to_file(&man, &path).map_err(|error| InstallManError::WriteToFile { path, error })?; + + for subcommand in command.get_subcommands() { + let name = subcommand.get_name(); + // XXX this line crashes with "Command list: Argument or group 'manifest-path' specified in + // 'conflicts_with*' for 'cargo-metadata' does not exist". + let man = Man::new(subcommand.clone()).manual("Nextest Manual"); + let path = output_dir.join(format!("cargo-nextest-{}.1", name)); + render_to_file(&man, &path) + .map_err(|error| InstallManError::WriteToFile { path, error })?; + } + + Ok(()) +} + +fn render_to_file(man: &Man, path: &Utf8Path) -> Result<(), std::io::Error> { + let mut writer = std::fs::File::create(&path)?; + man.render(&mut writer) +} diff --git a/nextest-metadata/src/exit_codes.rs b/nextest-metadata/src/exit_codes.rs index e7bb0863416..b13980d0ea0 100644 --- a/nextest-metadata/src/exit_codes.rs +++ b/nextest-metadata/src/exit_codes.rs @@ -28,6 +28,9 @@ impl NextestExitCode { /// Writing data to stdout or stderr produced an error. pub const WRITE_OUTPUT_ERROR: i32 = 110; + /// Installing man pages produced an error. + pub const INSTALL_MAN_ERROR: i32 = 120; + /// Downloading an update resulted in an error. pub const UPDATE_ERROR: i32 = 90;