use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressMode {
Dots,
Bar,
None,
Verbose,
}
impl std::str::FromStr for ProgressMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"dots" => Ok(Self::Dots),
"bar" => Ok(Self::Bar),
"none" => Ok(Self::None),
_ => Ok(Self::Dots),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
Console,
Json,
JUnit,
Allure,
}
#[derive(Parser, Debug)]
#[command(name = "grpctestify")]
#[command(author = "grpctestify team")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Test gRPC services with simple .gctf files", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[command(flatten)]
pub run_args: RunArgs,
#[arg(short = 'v', long, global = true, default_value_t = false)]
pub verbose: bool,
#[arg(short = 'c', long, global = true, default_value_t = false)]
pub no_color: bool,
#[arg(long, value_name = "SHELL_TYPE", value_parser = ["bash", "zsh", "fish", "elvish", "powershell"])]
pub completion: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Run(Box<RunArgs>),
Reflect(ReflectArgs),
Fmt(FmtArgs),
Check(CheckArgs),
Inspect(InspectArgs),
Explain(ExplainArgs),
Grpcurl(GrpcurlArgs),
List(ListArgs),
Lsp(LspArgs),
}
#[derive(Args, Debug, Clone)]
pub struct GrpcurlArgs {
#[arg(required = true)]
pub file: PathBuf,
#[arg(long)]
pub doc_index: Option<usize>,
#[arg(long, default_value = "text")]
pub format: String,
}
#[derive(Args, Debug, Clone)]
pub struct LspArgs {
#[arg(long, default_value_t = true)]
pub stdio: bool,
}
#[derive(Args, Debug, Clone)]
pub struct ListArgs {
#[arg(required = false)]
pub path: Option<PathBuf>,
#[arg(long, default_value = "json")]
pub format: String,
#[arg(long, default_value_t = false)]
pub with_range: bool,
}
#[derive(Args, Debug, Clone)]
pub struct InspectArgs {
#[arg(required = true)]
pub file: PathBuf,
#[arg(long, default_value = "text")]
pub format: String,
}
#[derive(Args, Debug, Clone)]
pub struct ExplainArgs {
#[arg(required = true)]
pub file: PathBuf,
#[arg(long, default_value = "text")]
pub format: String,
}
#[derive(Args, Debug, Clone)]
pub struct CheckArgs {
#[arg(required = true)]
pub files: Vec<PathBuf>,
#[arg(long, default_value = "text")]
pub format: String,
}
#[derive(Args, Debug, Clone)]
pub struct RunArgs {
#[arg(required = false)]
pub test_paths: Vec<PathBuf>,
#[arg(long = "exclude", value_name = "PATTERN")]
pub exclude: Vec<String>,
#[arg(long = "tags", value_name = "TAGS")]
pub tags: Vec<String>,
#[arg(long = "skip-tags", value_name = "TAGS")]
pub skip_tags: Vec<String>,
#[arg(short = 'p', long, default_value = "auto")]
pub parallel: String,
#[arg(short = 'd', long, default_value_t = false)]
pub dry_run: bool,
#[arg(short = 's', long, default_value = "path")]
pub sort: String,
#[arg(long, value_name = "FORMAT")]
pub log_format: Option<String>,
#[arg(long, value_name = "OUTPUT_FILE")]
pub log_output: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub stream: bool,
#[arg(short = 't', long, default_value_t = 30)]
pub timeout: u64,
#[arg(short = 'r', long, default_value_t = 0)]
pub retry: u32,
#[arg(long, default_value_t = 1.0)]
pub retry_delay: f64,
#[arg(long, default_value_t = false)]
pub no_retry: bool,
#[arg(long, default_value = "auto")]
pub progress: String,
#[arg(long, default_value_t = false)]
pub no_assert: bool,
#[arg(long, default_value_t = false)]
pub coverage: bool,
#[arg(long, default_value = "text")]
pub coverage_format: String,
#[arg(short = 'w', long, default_value_t = false)]
pub write: bool,
}
#[derive(Args, Debug, Clone)]
pub struct ReflectArgs {
pub symbol: Option<String>,
#[arg(long)]
pub address: Option<String>,
#[arg(long, default_value_t = false)]
pub plaintext: bool,
}
#[derive(Args, Debug, Clone)]
pub struct FmtArgs {
#[arg(required = true)]
pub files: Vec<PathBuf>,
#[arg(short = 'w', long, default_value_t = false)]
pub write: bool,
}
impl Cli {
pub fn parallel_jobs(&self) -> usize {
let parallel = match &self.command {
Some(Commands::Run(args)) => &args.parallel,
_ => &self.run_args.parallel,
};
if parallel == "auto" {
std::thread::available_parallelism()
.ok()
.map(|n| n.get())
.unwrap_or(4)
} else {
parallel.parse().unwrap_or(1)
}
}
pub fn progress_mode(&self) -> ProgressMode {
let progress = match &self.command {
Some(Commands::Run(args)) => &args.progress,
_ => &self.run_args.progress,
};
match progress.as_str() {
"dots" => ProgressMode::Dots,
"bar" => ProgressMode::Bar,
"none" => ProgressMode::None,
"auto" => {
if self.verbose {
ProgressMode::Verbose
} else {
ProgressMode::Dots
}
}
_ => ProgressMode::Dots,
}
}
pub fn log_format_mode(&self) -> Option<LogFormat> {
let log_format = match &self.command {
Some(Commands::Run(args)) => &args.log_format,
_ => &self.run_args.log_format,
};
log_format.as_ref().map(|fmt| match fmt.as_str() {
"junit" => LogFormat::JUnit,
"json" => LogFormat::Json,
"allure" => LogFormat::Allure,
_ => LogFormat::Console,
})
}
pub fn get_run_args(&self) -> &RunArgs {
match &self.command {
Some(Commands::Run(args)) => args,
_ => &self.run_args,
}
}
}
fn is_json_format(value: &str) -> bool {
value.eq_ignore_ascii_case("json")
}
pub trait HasFormat {
fn format(&self) -> &str;
fn is_json(&self) -> bool {
is_json_format(self.format())
}
}
impl HasFormat for ListArgs {
fn format(&self) -> &str {
&self.format
}
}
impl HasFormat for InspectArgs {
fn format(&self) -> &str {
&self.format
}
}
impl HasFormat for ExplainArgs {
fn format(&self) -> &str {
&self.format
}
}
impl HasFormat for GrpcurlArgs {
fn format(&self) -> &str {
&self.format
}
}
impl HasFormat for CheckArgs {
fn format(&self) -> &str {
&self.format
}
}
impl RunArgs {
pub fn is_json_coverage(&self) -> bool {
is_json_format(&self.coverage_format)
}
}