• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 29050229333

09 Jul 2026 09:06PM UTC coverage: 71.813% (-6.4%) from 78.205%
29050229333

Pull #46

github

web-flow
Merge 2f140b5a3 into c7b8c50f3
Pull Request #46: [1.7] add BENCH section

27062 of 37684 relevant lines covered (71.81%)

26911.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.24
/src/cli/args.rs
1
// CLI argument definitions using Clap
2

3
use clap::{Args, Parser, Subcommand};
4
use std::path::PathBuf;
5

6
/// Progress indicator modes
7
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
pub enum ProgressMode {
9
    Dots,
10
    Bar,
11
    None,
12
    Verbose,
13
}
14

15
impl std::str::FromStr for ProgressMode {
16
    type Err = ();
17

18
    fn from_str(s: &str) -> Result<Self, Self::Err> {
×
19
        match s {
×
20
            "dots" => Ok(Self::Dots),
×
21
            "bar" => Ok(Self::Bar),
×
22
            "none" => Ok(Self::None),
×
23
            _ => Ok(Self::Dots),
×
24
        }
25
    }
×
26
}
27

28
/// Log format types
29
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30
pub enum LogFormat {
31
    Console,
32
    Json,
33
    JUnit,
34
    Allure,
35
}
36

37
/// gRPC testing utility written in Rust
38
#[derive(Parser, Debug)]
39
#[command(name = "grpctestify")]
40
#[command(author = "grpctestify team")]
41
#[command(version = env!("CARGO_PKG_VERSION"))]
42
#[command(about = "Test gRPC services with simple .gctf files", long_about = None)]
43
pub struct Cli {
44
    #[command(subcommand)]
45
    pub command: Option<Commands>,
46

47
    // Flatten RunArgs to support implicit run command at top-level.
48
    // This allows `grpctestify tests/` to work as expected.
49
    #[command(flatten)]
50
    pub run_args: RunArgs,
51

52
    /// Enable verbose debug output
53
    #[arg(short = 'v', long, global = true, default_value_t = false)]
54
    pub verbose: bool,
55

56
    /// Install shell completion (bash, zsh, fish, elvish, powershell)
57
    #[arg(long, value_name = "SHELL_TYPE", value_parser = ["bash", "zsh", "fish", "elvish", "powershell"])]
58
    pub completion: Option<String>,
59
}
60

61
#[allow(clippy::large_enum_variant)]
62
#[derive(Subcommand, Debug)]
63
pub enum Commands {
64
    /// Run tests (default)
65
    Run(Box<RunArgs>),
66
    /// Call gRPC endpoint without assertions
67
    Call(CallArgs),
68
    /// Generate .gctf file from external invocations
69
    Gen(GenArgs),
70
    /// Reflect gRPC service and list methods
71
    Reflect(ReflectArgs),
72
    /// Format files
73
    Fmt(FmtArgs),
74
    /// Validate files
75
    Check(CheckArgs),
76
    /// Show test information
77
    Inspect(InspectArgs),
78
    /// Explain test execution flow
79
    Explain(ExplainArgs),
80
    /// Generate grpcurl invocation from a .gctf file
81
    Grpcurl(GrpcurlArgs),
82
    /// List discovered .gctf test files
83
    List(ListArgs),
84
    /// LSP server
85
    Lsp(LspArgs),
86
    /// Run benchmark tests
87
    Bench(BenchArgs),
88
    /// Manage data source indexes
89
    Index(IndexArgs),
90
    /// Query data sources interactively
91
    Query(QueryArgs),
92
    /// Check gRPC service health
93
    Health(HealthArgs),
94
}
95

96
#[derive(Args, Debug, Clone)]
97
pub struct HealthArgs {
98
    /// Server address (host:port)
99
    #[arg(required = true)]
100
    pub address: String,
101

102
    /// Service name to check (default: empty — checks overall server health)
103
    #[arg(long, default_value = "")]
104
    pub service: String,
105

106
    /// Output format (text, json)
107
    #[arg(long, default_value = "text")]
108
    pub format: String,
109

110
    /// Skip TLS verification
111
    #[arg(long, default_value_t = false)]
112
    pub insecure: bool,
113

114
    /// Timeout in seconds
115
    #[arg(long, default_value_t = 10)]
116
    pub timeout: u64,
117
}
118

119
#[derive(Args, Debug, Clone)]
120
pub struct GrpcurlArgs {
121
    /// File to convert into grpcurl command
122
    #[arg(required = true)]
123
    pub file: PathBuf,
124

125
    /// Document index for multi-document .gctf files (1-based)
126
    #[arg(long)]
127
    pub doc_index: Option<usize>,
128

129
    /// Output format (text, json)
130
    #[arg(long, default_value = "text")]
131
    pub format: String,
132
}
133

134
#[derive(Args, Debug, Clone)]
135
pub struct LspArgs {
136
    /// Use stdio for communication (default)
137
    #[arg(long, default_value_t = true)]
138
    pub stdio: bool,
139
}
140

141
#[derive(Args, Debug, Clone)]
142
pub struct BenchArgs {
143
    /// Path to test file or directory to benchmark
144
    #[arg(required = true)]
145
    pub test_paths: Vec<PathBuf>,
146

147
    /// Benchmark profile (functional, load, stress, spike, soak)
148
    #[arg(long, value_name = "PROFILE")]
149
    pub profile: Option<String>,
150

151
    /// Benchmark mode (fixed, stepping, adaptive)
152
    #[arg(long, value_name = "MODE")]
153
    pub mode: Option<String>,
154

155
    /// Number of concurrent workers
156
    #[arg(short = 'c', long, value_name = "N")]
157
    pub concurrency: Option<u32>,
158

159
    /// Total number of requests to send
160
    #[arg(short = 'n', long, value_name = "N")]
161
    pub requests: Option<u64>,
162

163
    /// Duration of benchmark (e.g., 30s, 5m)
164
    #[arg(short = 'd', long, value_name = "DURATION")]
165
    pub duration: Option<String>,
166

167
    /// Ramp-up duration before steady-state load (e.g., 10s)
168
    #[arg(long = "ramp-up", alias = "ramp_up", value_name = "DURATION")]
169
    pub ramp_up: Option<String>,
170

171
    /// Warmup period excluded from final metrics (e.g., 5s)
172
    #[arg(long, value_name = "DURATION")]
173
    pub warmup: Option<String>,
174

175
    /// Maximum runtime with request-count mode (e.g., 30s, 5m)
176
    #[arg(long, value_name = "DURATION")]
177
    pub max_duration: Option<String>,
178

179
    /// Maximum requests per second (rate limit)
180
    #[arg(long, value_name = "RPS")]
181
    pub max_rps: Option<f64>,
182

183
    /// Load schedule strategy (const, step, line)
184
    #[arg(long = "load-schedule", value_name = "SCHEDULE")]
185
    pub load_schedule: Option<String>,
186

187
    /// Starting RPS for step/line load schedules
188
    #[arg(long = "load-start", value_name = "RPS")]
189
    pub load_start: Option<f64>,
190

191
    /// Step/slope RPS delta for step/line schedules
192
    #[arg(long = "load-step", value_name = "RPS_DELTA")]
193
    pub load_step: Option<f64>,
194

195
    /// Optional ending RPS for step/line schedules
196
    #[arg(long = "load-end", value_name = "RPS")]
197
    pub load_end: Option<f64>,
198

199
    /// Duration of each step for step schedule
200
    #[arg(long = "load-step-duration", value_name = "DURATION")]
201
    pub load_step_duration: Option<String>,
202

203
    /// Maximum duration of load adjustments
204
    #[arg(long = "load-max-duration", value_name = "DURATION")]
205
    pub load_max_duration: Option<String>,
206

207
    /// Number of gRPC connections to use (<= concurrency)
208
    #[arg(long, value_name = "N")]
209
    pub connections: Option<u32>,
210

211
    /// Connection timeout (e.g., 10s)
212
    #[arg(long, value_name = "DURATION")]
213
    pub connect_timeout: Option<String>,
214

215
    /// Keepalive interval (e.g., 30s)
216
    #[arg(long, value_name = "DURATION")]
217
    pub keepalive: Option<String>,
218

219
    /// Number of CPU cores to use
220
    #[arg(long, value_name = "N")]
221
    pub cpus: Option<usize>,
222

223
    /// User-defined benchmark run name
224
    #[arg(long, value_name = "NAME")]
225
    pub name: Option<String>,
226

227
    /// Assertion mode (fail_fast, collect_all, skip)
228
    #[arg(long, visible_alias = "bench-assert-mode", value_name = "MODE")]
229
    pub assert_mode: Option<String>,
230

231
    /// Disable ASSERTS evaluation to measure transport baseline
232
    #[arg(long, visible_alias = "bench-no-assert", default_value_t = false)]
233
    pub no_assert: bool,
234

235
    /// Sample rate for detailed logging (0.0-1.0)
236
    #[arg(long, value_name = "RATE")]
237
    pub sample_rate: Option<f64>,
238

239
    /// Enable reflection/proto caching
240
    #[arg(long)]
241
    pub cache: Option<bool>,
242

243
    /// Skip first N requests in latency metrics
244
    #[arg(long, value_name = "N")]
245
    pub skip_first: Option<u32>,
246

247
    /// Include errors in latency calculation
248
    #[arg(long)]
249
    pub count_errors_in_latency: Option<bool>,
250

251
    /// In-flight handling when duration limit is reached (close, wait, ignore)
252
    #[arg(long, value_name = "MODE")]
253
    pub duration_stop: Option<String>,
254

255
    /// Latency percentiles to report (comma-separated, e.g. p50,p90,p95,p99)
256
    #[arg(long, value_name = "LIST")]
257
    pub latency_percentiles: Option<String>,
258

259
    /// Progress heartbeat interval (e.g. 5s)
260
    #[arg(long = "progress-interval", value_name = "DURATION")]
261
    pub progress_interval: Option<String>,
262

263
    /// Output format (console, json, csv, ndjson, prometheus)
264
    #[arg(
265
        long = "log-format",
266
        visible_alias = "bench-format",
267
        default_value = "console"
268
    )]
269
    pub format: String,
270

271
    /// Output file for benchmark report
272
    #[arg(
273
        short = 'o',
274
        long = "log-output",
275
        visible_alias = "bench-output",
276
        value_name = "OUTPUT_FILE"
277
    )]
278
    pub output: Option<PathBuf>,
279

280
    /// Custom MiniJinja template file for benchmark report
281
    #[arg(long, value_name = "TEMPLATE_FILE")]
282
    pub report_template: Option<PathBuf>,
283

284
    /// Allure output directory for benchmark attachments
285
    #[arg(long, value_name = "DIR")]
286
    pub allure_output_dir: Option<PathBuf>,
287

288
    /// Compact console output (omit histogram)
289
    #[arg(long, default_value_t = false)]
290
    pub compact: bool,
291

292
    /// Filter by tags (AND - file must have ALL tags)
293
    #[arg(long = "tags", value_name = "TAGS")]
294
    pub tags: Vec<String>,
295

296
    /// Skip files with these tags (NOT OR - exclude if ANY matches)
297
    #[arg(long = "skip-tags", value_name = "TAGS")]
298
    pub skip_tags: Vec<String>,
299

300
    /// Exclude files/directories matching the given glob pattern (can be used multiple times)
301
    #[arg(long, value_name = "PATTERN")]
302
    pub exclude: Vec<String>,
303

304
    /// List available benchmark profiles and exit
305
    #[arg(long, default_value_t = false)]
306
    pub list_profiles: bool,
307

308
    /// Path to custom profile YAML file
309
    #[arg(long, value_name = "FILE")]
310
    pub profile_file: Option<PathBuf>,
311

312
    /// Direct gRPC method call (service/method) — no .gctf file needed
313
    #[arg(long, value_name = "SERVICE/METHOD")]
314
    pub call: Option<String>,
315

316
    /// Inline JSON request body (used with --call)
317
    #[arg(long, value_name = "JSON")]
318
    pub data: Option<String>,
319
}
320

321
#[derive(Args, Debug, Clone)]
322
pub struct IndexArgs {
323
    /// .gctf file(s) or directory with BENCH.sources definitions
324
    #[arg(required = true)]
325
    pub sources: Vec<PathBuf>,
326

327
    /// Force rebuild of all required indexes
328
    #[arg(long, default_value_t = false)]
329
    pub force: bool,
330

331
    /// Show index file statistics
332
    #[arg(long, default_value_t = false)]
333
    pub stats: bool,
334
}
335

336
#[derive(Args, Debug, Clone)]
337
pub struct QueryArgs {
338
    /// Files or directories to query (default: interactive shell)
339
    #[arg(required = false)]
340
    pub files: Vec<PathBuf>,
341

342
    /// Query expression to execute
343
    #[arg(short = 'q', long, value_name = "EXPR")]
344
    pub query: Option<String>,
345

346
    /// Run in interactive shell mode
347
    #[arg(short = 's', long, default_value_t = false)]
348
    pub shell: bool,
349

350
    /// Index column for direct file mode
351
    #[arg(short = 'i', long, value_name = "COLUMN")]
352
    pub indexed_by: Option<String>,
353

354
    /// Output format (json, csv, table, line, tsv)
355
    #[arg(short = 'f', long, default_value = "table")]
356
    pub format: String,
357

358
    /// Maximum number of rows to return
359
    #[arg(short = 'n', long, value_name = "N")]
360
    pub limit: Option<usize>,
361

362
    /// Skip N rows
363
    #[arg(short = 'o', long, value_name = "N")]
364
    pub offset: Option<usize>,
365

366
    /// Output columns (comma-separated)
367
    #[arg(short = 'c', long, value_name = "COLS")]
368
    pub columns: Option<String>,
369

370
    /// Sort by column (prefix with - for DESC)
371
    #[arg(long, value_name = "COLUMN")]
372
    pub order_by: Option<String>,
373

374
    /// Output file (format auto-detected from extension: .csv, .tsv, .ndjson, .json)
375
    #[arg(long, value_name = "FILE")]
376
    pub output: Option<PathBuf>,
377

378
    /// Skip header row in output
379
    #[arg(long, default_value_t = false)]
380
    pub no_header: bool,
381
}
382

383
#[derive(Args, Debug, Clone)]
384
pub struct ListArgs {
385
    /// Path to test file or directory to list
386
    #[arg(required = false)]
387
    pub path: Option<PathBuf>,
388

389
    /// Output format (text, json)
390
    #[arg(long, default_value = "json")]
391
    pub format: String,
392

393
    /// Include test range information
394
    #[arg(long, default_value_t = false)]
395
    pub with_range: bool,
396
}
397

398
#[derive(Args, Debug, Clone)]
399
pub struct InspectArgs {
400
    /// File to inspect
401
    #[arg(required = true)]
402
    pub file: PathBuf,
403

404
    /// Output format (text, json)
405
    #[arg(long, default_value = "text")]
406
    pub format: String,
407
}
408

409
#[derive(Args, Debug, Clone)]
410
pub struct ExplainArgs {
411
    /// File to explain
412
    #[arg(required = true)]
413
    pub file: PathBuf,
414

415
    /// Output format (text, json)
416
    #[arg(long, default_value = "text")]
417
    pub format: String,
418
}
419

420
#[derive(Args, Debug, Clone)]
421
pub struct CheckArgs {
422
    /// Files to validate
423
    #[arg(required = true)]
424
    pub files: Vec<PathBuf>,
425

426
    /// Output format (text, json)
427
    #[arg(long, default_value = "text")]
428
    pub format: String,
429

430
    /// Validate BENCH section configuration
431
    #[arg(long, default_value_t = false)]
432
    pub bench: bool,
433
}
434

435
#[derive(Args, Debug, Clone)]
436
pub struct RunArgs {
437
    /// Path to test file or directory to execute
438
    // We make this optional so it doesn't conflict with subcommands when parsed at top level,
439
    // but we'll enforce it manually if no subcommand is present.
440
    // However, if we use `flatten` at top level, and `subcommand` is optional,
441
    // Clap might be ambiguous if `test_paths` matches a subcommand name.
442
    // But since `test_paths` are files/dirs, usually they won't clash with "run", "reflect", etc.
443
    // We remove `required` constraint here and handle validation manually.
444
    #[arg(required = false)]
445
    pub test_paths: Vec<PathBuf>,
446

447
    /// Exclude files/directories matching the given glob pattern (can be used multiple times)
448
    #[arg(long = "exclude", value_name = "PATTERN")]
449
    pub exclude: Vec<String>,
450

451
    /// Filter by tags (AND - file must have ALL tags)
452
    #[arg(long = "tags", value_name = "TAGS")]
453
    pub tags: Vec<String>,
454

455
    /// Skip files with these tags (NOT OR - exclude if ANY matches)
456
    #[arg(long = "skip-tags", value_name = "TAGS")]
457
    pub skip_tags: Vec<String>,
458

459
    /// Run tests in parallel with N workers
460
    #[arg(short = 'p', long, default_value = "auto")]
461
    pub parallel: String,
462

463
    /// Show commands that would be executed without running them
464
    #[arg(short = 'd', long, default_value_t = false)]
465
    pub dry_run: bool,
466

467
    /// Sort test files by type
468
    #[arg(short = 's', long, default_value = "path")]
469
    pub sort: String,
470

471
    /// Generate test reports in specified format
472
    #[arg(long, value_name = "FORMAT")]
473
    pub log_format: Option<String>,
474

475
    /// Output file for test reports (use with --log-format)
476
    #[arg(long, value_name = "OUTPUT_FILE")]
477
    pub log_output: Option<PathBuf>,
478

479
    /// Output streaming JSON events (for IDE integration)
480
    #[arg(long, default_value_t = false)]
481
    pub stream: bool,
482

483
    /// Set timeout for individual tests (seconds)
484
    #[arg(short = 't', long, default_value_t = 30)]
485
    pub timeout: u64,
486

487
    /// Number of retries for failed network calls
488
    #[arg(short = 'r', long, default_value_t = 0)]
489
    pub retry: u32,
490

491
    /// Initial delay between retries (seconds)
492
    #[arg(long, default_value_t = 1.0)]
493
    pub retry_delay: f64,
494

495
    /// Disable retry mechanisms completely
496
    #[arg(long, default_value_t = false)]
497
    pub no_retry: bool,
498

499
    /// Progress indicator style
500
    #[arg(long, default_value = "auto")]
501
    pub progress: String,
502

503
    /// Skip assertion evaluation and print raw server responses
504
    #[arg(long, default_value_t = false)]
505
    pub no_assert: bool,
506

507
    /// Generate Proto API coverage report
508
    #[arg(long, default_value_t = false)]
509
    pub coverage: bool,
510

511
    /// Coverage output format (text, json)
512
    #[arg(long, default_value = "text")]
513
    pub coverage_format: String,
514

515
    /// Write/Overwrite test files with actual server responses (Snapshot Mode)
516
    #[arg(short = 'w', long, default_value_t = false)]
517
    pub write: bool,
518
}
519

520
#[derive(Args, Debug, Clone)]
521
pub struct ReflectArgs {
522
    /// Service/method symbol OR .gctf file path
523
    pub symbol: Option<String>,
524

525
    /// Server address (overrides environment variable)
526
    #[arg(long)]
527
    pub address: Option<String>,
528

529
    /// Plaintext connection (no TLS). If omitted, localhost/http addresses default to plaintext.
530
    #[arg(long, default_value_t = false)]
531
    pub plaintext: bool,
532

533
    /// Output format (text, json)
534
    #[arg(long, default_value = "text")]
535
    pub format: String,
536

537
    /// List all methods with full signatures
538
    #[arg(long, default_value_t = false)]
539
    pub list_methods: bool,
540

541
    /// Describe a method's request and response message fields
542
    #[arg(long, value_name = "SERVICE/METHOD")]
543
    pub describe: Option<String>,
544

545
    /// CA certificate path for TLS
546
    #[arg(long)]
547
    pub tls_ca: Option<String>,
548

549
    /// Client certificate path for TLS
550
    #[arg(long)]
551
    pub tls_cert: Option<String>,
552

553
    /// Client key path for TLS
554
    #[arg(long)]
555
    pub tls_key: Option<String>,
556
}
557

558
#[derive(Args, Debug, Clone)]
559
pub struct FmtArgs {
560
    /// Files to format
561
    #[arg(required = true)]
562
    pub files: Vec<PathBuf>,
563

564
    /// Write changes to file instead of stdout
565
    #[arg(short = 'w', long, default_value_t = false)]
566
    pub write: bool,
567
}
568

569
#[derive(Args, Debug, Clone)]
570
pub struct CallArgs {
571
    /// File to call
572
    #[arg(required = true)]
573
    pub file: PathBuf,
574

575
    /// Document index for multi-document .gctf files (1-based)
576
    #[arg(long)]
577
    pub doc_index: Option<usize>,
578

579
    /// Include response headers in output, printed before body (-i)
580
    #[arg(short = 'i', long, default_value_t = false)]
581
    pub include: bool,
582

583
    /// Verbose mode: show request/response metadata (-v)
584
    #[arg(short = 'v', long, default_value_t = false)]
585
    pub verbose: bool,
586

587
    /// Extra verbose mode: verbose output plus timing (-vv)
588
    #[arg(long = "vv", default_value_t = false)]
589
    pub very_verbose: bool,
590

591
    /// Output to file instead of stdout (-o)
592
    #[arg(short = 'o', long)]
593
    pub output: Option<PathBuf>,
594

595
    /// Dump response headers to file (-D)
596
    #[arg(short = 'D', long)]
597
    pub dump_header: Option<PathBuf>,
598

599
    /// Silent mode (-s)
600
    #[arg(short = 's', long, default_value_t = false)]
601
    pub silent: bool,
602

603
    /// Show errors (-S)
604
    #[arg(short = 'S', long, default_value_t = false)]
605
    pub show_error: bool,
606

607
    /// Connection timeout in seconds
608
    #[arg(long, default_value_t = 30)]
609
    pub connect_timeout: u64,
610

611
    /// Skip TLS certificate verification
612
    #[arg(long, default_value_t = false)]
613
    pub insecure: bool,
614

615
    /// Request timeout in seconds
616
    #[arg(long, default_value_t = 60)]
617
    pub max_time: u64,
618

619
    /// Run as benchmark instead of single call
620
    #[arg(long, default_value_t = false)]
621
    pub bench: bool,
622

623
    /// Benchmark concurrency (with --bench)
624
    #[arg(long, requires = "bench")]
625
    pub concurrency: Option<u32>,
626

627
    /// Benchmark requests (with --bench)
628
    #[arg(long, requires = "bench")]
629
    pub requests: Option<u64>,
630

631
    /// Benchmark duration (with --bench), e.g. "30s"
632
    #[arg(long, requires = "bench")]
633
    pub duration: Option<String>,
634
}
635

636
#[derive(Args, Debug, Clone)]
637
pub struct GenArgs {
638
    /// Output file for generated gctf (stdout if omitted)
639
    #[arg(short = 'o', long)]
640
    pub output: Option<PathBuf>,
641

642
    #[command(subcommand)]
643
    pub source: GenSource,
644
}
645

646
#[derive(Subcommand, Debug, Clone)]
647
pub enum GenSource {
648
    /// Generate from grpcurl invocation
649
    Grpcurl(GenGrpcurlArgs),
650
}
651

652
#[derive(Args, Debug, Clone)]
653
#[command(trailing_var_arg = true)]
654
pub struct GenGrpcurlArgs {
655
    /// Execute invocation and append RESPONSE/ERROR
656
    #[arg(short = 'e', long, default_value_t = false)]
657
    pub execute: bool,
658

659
    /// grpcurl command arguments after `gen grpcurl`
660
    #[arg(required = true, allow_hyphen_values = true)]
661
    pub grpcurl_args: Vec<String>,
662
}
663

664
impl Cli {
665
    /// Get parallel job count (auto-detect if set to "auto")
666
    pub fn parallel_jobs(&self) -> usize {
1✔
667
        let parallel = match &self.command {
1✔
668
            Some(Commands::Run(args)) => &args.parallel,
1✔
669
            _ => &self.run_args.parallel,
×
670
        };
671

672
        if parallel == "auto" {
1✔
673
            // Auto-detect CPU count
674
            std::thread::available_parallelism()
1✔
675
                .ok()
1✔
676
                .map(|n| n.get())
1✔
677
                .unwrap_or(4)
1✔
678
        } else {
679
            parallel.parse().unwrap_or(1)
×
680
        }
681
    }
1✔
682

683
    /// Get progress mode
684
    pub fn progress_mode(&self) -> ProgressMode {
1✔
685
        let progress = match &self.command {
1✔
686
            Some(Commands::Run(args)) => &args.progress,
1✔
687
            _ => &self.run_args.progress,
×
688
        };
689

690
        match progress.as_str() {
1✔
691
            "dots" => ProgressMode::Dots,
1✔
692
            "bar" => ProgressMode::Bar,
1✔
693
            "none" => ProgressMode::None,
1✔
694
            "auto" => {
1✔
695
                if self.verbose {
1✔
696
                    ProgressMode::Verbose
×
697
                } else {
698
                    ProgressMode::Dots
1✔
699
                }
700
            }
701
            _ => ProgressMode::Dots,
×
702
        }
703
    }
1✔
704

705
    /// Get log format
706
    pub fn log_format_mode(&self) -> Option<LogFormat> {
1✔
707
        let log_format = match &self.command {
1✔
708
            Some(Commands::Run(args)) => &args.log_format,
1✔
709
            _ => &self.run_args.log_format,
×
710
        };
711

712
        log_format.as_ref().map(|fmt| match fmt.as_str() {
1✔
713
            "junit" => LogFormat::JUnit,
×
714
            "json" => LogFormat::Json,
×
715
            "allure" => LogFormat::Allure,
×
716
            _ => LogFormat::Console,
×
717
        })
×
718
    }
1✔
719

720
    /// Helper to get effective RunArgs
721
    pub fn get_run_args(&self) -> &RunArgs {
×
722
        match &self.command {
×
723
            Some(Commands::Run(args)) => args,
×
724
            _ => &self.run_args,
×
725
        }
726
    }
×
727
}
728

729
fn is_json_format(value: &str) -> bool {
33✔
730
    value.eq_ignore_ascii_case("json")
33✔
731
}
33✔
732

733
/// Trait for CLI argument types that have a `--format` option.
734
pub trait HasFormat {
735
    fn format(&self) -> &str;
736

737
    fn is_json(&self) -> bool {
33✔
738
        is_json_format(self.format())
33✔
739
    }
33✔
740
}
741

742
impl HasFormat for ListArgs {
743
    fn format(&self) -> &str {
3✔
744
        &self.format
3✔
745
    }
3✔
746
}
747

748
impl HasFormat for InspectArgs {
749
    fn format(&self) -> &str {
8✔
750
        &self.format
8✔
751
    }
8✔
752
}
753

754
impl HasFormat for ExplainArgs {
755
    fn format(&self) -> &str {
3✔
756
        &self.format
3✔
757
    }
3✔
758
}
759

760
impl HasFormat for GrpcurlArgs {
761
    fn format(&self) -> &str {
9✔
762
        &self.format
9✔
763
    }
9✔
764
}
765

766
impl HasFormat for CheckArgs {
767
    fn format(&self) -> &str {
10✔
768
        &self.format
10✔
769
    }
10✔
770
}
771

772
impl HasFormat for BenchArgs {
773
    fn format(&self) -> &str {
×
774
        &self.format
×
775
    }
×
776
}
777

778
impl RunArgs {
779
    pub fn is_json_coverage(&self) -> bool {
×
780
        is_json_format(&self.coverage_format)
×
781
    }
×
782
}
783

784
#[cfg(test)]
785
mod tests {
786
    use super::*;
787
    use clap::Parser;
788

789
    #[test]
790
    fn parse_call_defaults() {
1✔
791
        let cli = Cli::parse_from(["grpctestify", "call", "test.gctf"]);
1✔
792
        let Some(Commands::Call(call)) = cli.command else {
1✔
793
            panic!("expected call command");
×
794
        };
795

796
        assert_eq!(call.file, PathBuf::from("test.gctf"));
1✔
797
        assert_eq!(call.doc_index, None);
1✔
798
        assert!(!call.include);
1✔
799
        assert!(!call.verbose);
1✔
800
        assert!(!call.very_verbose);
1✔
801
        assert!(!call.silent);
1✔
802
        assert!(!call.show_error);
1✔
803
        assert_eq!(call.connect_timeout, 30);
1✔
804
        assert_eq!(call.max_time, 60);
1✔
805
    }
1✔
806

807
    #[test]
808
    fn parse_call_verbose_flags() {
1✔
809
        let cli = Cli::parse_from(["grpctestify", "call", "-v", "test.gctf"]);
1✔
810
        let Some(Commands::Call(call)) = cli.command else {
1✔
811
            panic!()
×
812
        };
813
        assert!(call.verbose);
1✔
814
        assert!(!call.very_verbose);
1✔
815

816
        let cli = Cli::parse_from(["grpctestify", "call", "--vv", "test.gctf"]);
1✔
817
        let Some(Commands::Call(call)) = cli.command else {
1✔
818
            panic!()
×
819
        };
820
        assert!(!call.verbose);
1✔
821
        assert!(call.very_verbose);
1✔
822
    }
1✔
823

824
    #[test]
825
    fn parse_call_include_and_dump_header() {
1✔
826
        let cli = Cli::parse_from(["grpctestify", "call", "-i", "-D", "/tmp/h.txt", "test.gctf"]);
1✔
827
        let Some(Commands::Call(call)) = cli.command else {
1✔
828
            panic!()
×
829
        };
830
        assert!(call.include);
1✔
831
        assert_eq!(call.dump_header, Some(PathBuf::from("/tmp/h.txt")));
1✔
832
    }
1✔
833

834
    #[test]
835
    fn parse_call_silent_and_show_error() {
1✔
836
        let cli = Cli::parse_from(["grpctestify", "call", "-s", "-S", "test.gctf"]);
1✔
837
        let Some(Commands::Call(call)) = cli.command else {
1✔
838
            panic!()
×
839
        };
840
        assert!(call.silent);
1✔
841
        assert!(call.show_error);
1✔
842
    }
1✔
843

844
    #[test]
845
    fn parse_gen_with_output_before_source() {
1✔
846
        let cli = Cli::parse_from([
1✔
847
            "grpctestify",
1✔
848
            "gen",
1✔
849
            "-o",
1✔
850
            "out.gctf",
1✔
851
            "grpcurl",
1✔
852
            "-plaintext",
1✔
853
            "localhost:4770",
1✔
854
            "svc.Method/Call",
1✔
855
        ]);
1✔
856

857
        let Some(Commands::Gen(gen_args)) = cli.command else {
1✔
858
            panic!("expected gen command");
×
859
        };
860
        assert_eq!(gen_args.output, Some(PathBuf::from("out.gctf")));
1✔
861

862
        let GenSource::Grpcurl(grpcurl) = gen_args.source;
1✔
863
        assert_eq!(
1✔
864
            grpcurl.grpcurl_args,
865
            vec![
1✔
866
                "-plaintext".to_string(),
1✔
867
                "localhost:4770".to_string(),
1✔
868
                "svc.Method/Call".to_string()
1✔
869
            ]
870
        );
871
    }
1✔
872

873
    #[test]
874
    fn parse_gen_grpcurl_preserves_hyphen_args() {
1✔
875
        let cli = Cli::parse_from([
1✔
876
            "grpctestify",
1✔
877
            "gen",
1✔
878
            "grpcurl",
1✔
879
            "-H",
1✔
880
            "x-api-key: abc",
1✔
881
            "-d",
1✔
882
            "{}",
1✔
883
            "localhost:4770",
1✔
884
            "svc.Method/Call",
1✔
885
        ]);
1✔
886

887
        let Some(Commands::Gen(gen_args)) = cli.command else {
1✔
888
            panic!("expected gen command");
×
889
        };
890

891
        let GenSource::Grpcurl(grpcurl) = gen_args.source;
1✔
892
        assert_eq!(grpcurl.grpcurl_args[0], "-H");
1✔
893
        assert_eq!(grpcurl.grpcurl_args[2], "-d");
1✔
894
        assert_eq!(grpcurl.grpcurl_args[3], "{}");
1✔
895
        assert_eq!(grpcurl.grpcurl_args[4], "localhost:4770");
1✔
896
    }
1✔
897

898
    #[test]
899
    fn parse_bench_extended_options() {
1✔
900
        let cli = Cli::parse_from([
1✔
901
            "grpctestify",
1✔
902
            "bench",
1✔
903
            "tests/",
1✔
904
            "-c",
1✔
905
            "8",
1✔
906
            "-n",
1✔
907
            "1000",
1✔
908
            "--max-duration",
1✔
909
            "30s",
1✔
910
            "--connections",
1✔
911
            "4",
1✔
912
            "--connect-timeout",
1✔
913
            "2s",
1✔
914
            "--keepalive",
1✔
915
            "10s",
1✔
916
            "--cpus",
1✔
917
            "2",
1✔
918
            "--name",
1✔
919
            "smoke-bench",
1✔
920
        ]);
1✔
921

922
        let Some(Commands::Bench(args)) = cli.command else {
1✔
923
            panic!("expected bench command");
×
924
        };
925

926
        assert_eq!(args.test_paths, vec![PathBuf::from("tests/")]);
1✔
927
        assert_eq!(args.concurrency, Some(8));
1✔
928
        assert_eq!(args.requests, Some(1000));
1✔
929
        assert_eq!(args.max_duration.as_deref(), Some("30s"));
1✔
930
        assert_eq!(args.connections, Some(4));
1✔
931
        assert_eq!(args.connect_timeout.as_deref(), Some("2s"));
1✔
932
        assert_eq!(args.keepalive.as_deref(), Some("10s"));
1✔
933
        assert_eq!(args.cpus, Some(2));
1✔
934
        assert_eq!(args.name.as_deref(), Some("smoke-bench"));
1✔
935
    }
1✔
936

937
    #[test]
938
    fn parse_bench_run_style_option_names() {
1✔
939
        let cli = Cli::parse_from([
1✔
940
            "grpctestify",
1✔
941
            "bench",
1✔
942
            "tests/",
1✔
943
            "--no-assert",
1✔
944
            "--assert-mode",
1✔
945
            "sampled",
1✔
946
            "--log-format",
1✔
947
            "json",
1✔
948
            "--log-output",
1✔
949
            "bench.json",
1✔
950
            "--latency-percentiles",
1✔
951
            "p50,p90,p99",
1✔
952
            "--duration-stop",
1✔
953
            "wait",
1✔
954
            "--progress-interval",
1✔
955
            "3s",
1✔
956
            "--ramp-up",
1✔
957
            "3s",
1✔
958
            "--warmup",
1✔
959
            "1s",
1✔
960
        ]);
1✔
961

962
        let Some(Commands::Bench(args)) = cli.command else {
1✔
963
            panic!("expected bench command");
×
964
        };
965

966
        assert!(args.no_assert);
1✔
967
        assert_eq!(args.assert_mode.as_deref(), Some("sampled"));
1✔
968
        assert_eq!(args.format, "json");
1✔
969
        assert_eq!(args.output, Some(PathBuf::from("bench.json")));
1✔
970
        assert_eq!(args.latency_percentiles.as_deref(), Some("p50,p90,p99"));
1✔
971
        assert_eq!(args.duration_stop.as_deref(), Some("wait"));
1✔
972
        assert_eq!(args.progress_interval.as_deref(), Some("3s"));
1✔
973
        assert_eq!(args.ramp_up.as_deref(), Some("3s"));
1✔
974
        assert_eq!(args.warmup.as_deref(), Some("1s"));
1✔
975
    }
1✔
976

977
    #[test]
978
    fn parse_bench_load_schedule_options() {
1✔
979
        let cli = Cli::parse_from([
1✔
980
            "grpctestify",
1✔
981
            "bench",
1✔
982
            "tests/",
1✔
983
            "-c",
1✔
984
            "10",
1✔
985
            "-n",
1✔
986
            "10000",
1✔
987
            "--load-schedule",
1✔
988
            "step",
1✔
989
            "--load-start",
1✔
990
            "50",
1✔
991
            "--load-end",
1✔
992
            "150",
1✔
993
            "--load-step",
1✔
994
            "10",
1✔
995
            "--load-step-duration",
1✔
996
            "5s",
1✔
997
            "--load-max-duration",
1✔
998
            "40s",
1✔
999
        ]);
1✔
1000

1001
        let Some(Commands::Bench(args)) = cli.command else {
1✔
1002
            panic!("expected bench command");
×
1003
        };
1004

1005
        assert_eq!(args.concurrency, Some(10));
1✔
1006
        assert_eq!(args.requests, Some(10000));
1✔
1007
        assert_eq!(args.load_schedule.as_deref(), Some("step"));
1✔
1008
        assert_eq!(args.load_start, Some(50.0));
1✔
1009
        assert_eq!(args.load_end, Some(150.0));
1✔
1010
        assert_eq!(args.load_step, Some(10.0));
1✔
1011
        assert_eq!(args.load_step_duration.as_deref(), Some("5s"));
1✔
1012
        assert_eq!(args.load_max_duration.as_deref(), Some("40s"));
1✔
1013
    }
1✔
1014

1015
    #[test]
1016
    fn parse_index_command() {
1✔
1017
        let cli = Cli::parse_from([
1✔
1018
            "grpctestify",
1✔
1019
            "index",
1✔
1020
            "tests/bench/user_lookup.gctf",
1✔
1021
            "--force",
1✔
1022
        ]);
1✔
1023

1024
        let Some(Commands::Index(args)) = cli.command else {
1✔
1025
            panic!("expected index command");
×
1026
        };
1027
        assert_eq!(
1✔
1028
            args.sources,
1029
            vec![PathBuf::from("tests/bench/user_lookup.gctf")]
1✔
1030
        );
1031
        assert!(args.force);
1✔
1032
    }
1✔
1033
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc