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

gripmock / grpctestify-rust / 29161871358

11 Jul 2026 05:34PM UTC coverage: 72.411% (-1.2%) from 73.631%
29161871358

Pull #53

github

web-flow
Merge 1683addcd into 3505dfb2a
Pull Request #53: feat: play

1133 of 2329 new or added lines in 26 files covered. (48.65%)

10 existing lines in 10 files now uncovered.

30306 of 41853 relevant lines covered (72.41%)

24296.51 hits per line

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

86.0
/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
    None,
11
    Verbose,
12
}
13

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

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

27
/// Log format types
28
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29
pub enum LogFormat {
30
    Console,
31
    Json,
32
    Yaml,
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
    /// Optimizer level (0=none, 1=safe, 2=advisory, 3=aggressive)
57
    #[arg(long = "optimize", short = 'O', value_name = "LEVEL", global = true, default_value_t = String::new())]
58
    pub optimize: String,
59

60
    /// Install shell completion (bash, zsh, fish, elvish, powershell)
61
    #[arg(long, value_name = "SHELL_TYPE", value_parser = ["bash", "zsh", "fish", "elvish", "powershell"])]
62
    pub completion: Option<String>,
63
}
64

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

102
#[derive(Args, Debug, Clone)]
103
pub struct HealthArgs {
104
    /// Server address (host:port)
105
    #[arg(required = true)]
106
    pub address: String,
107

108
    /// Service name to check (default: empty — checks overall server health)
109
    #[arg(long, default_value = "")]
110
    pub service: String,
111

112
    /// Output format (text, json)
113
    #[arg(long, default_value = "text")]
114
    pub format: String,
115

116
    /// Skip TLS verification
117
    #[arg(long, default_value_t = false)]
118
    pub insecure: bool,
119

120
    /// Timeout in seconds
121
    #[arg(long, default_value_t = 10)]
122
    pub timeout: u64,
123
}
124

125
#[derive(Args, Debug, Clone)]
126
pub struct GrpcurlArgs {
127
    /// File to convert into grpcurl command
128
    #[arg(required = true)]
129
    pub file: PathBuf,
130

131
    /// Document index for multi-document .gctf files (1-based)
132
    #[arg(long)]
133
    pub doc_index: Option<usize>,
134

135
    /// Output format (text, json)
136
    #[arg(long, default_value = "text")]
137
    pub format: String,
138
}
139

140
#[derive(Args, Debug, Clone)]
141
pub struct LspArgs {
142
    /// Use stdio for communication (default)
143
    #[arg(long, default_value_t = true)]
144
    pub stdio: bool,
145
}
146

147
#[derive(Args, Debug, Clone)]
148
pub struct BenchArgs {
149
    /// Path to test file or directory to benchmark
150
    #[arg(required = true)]
151
    pub test_paths: Vec<PathBuf>,
152

153
    /// Benchmark profile (functional, load, stress, spike, soak)
154
    #[arg(long, value_name = "PROFILE")]
155
    pub profile: Option<String>,
156

157
    /// Benchmark mode (fixed, stepping, adaptive)
158
    #[arg(long, value_name = "MODE")]
159
    pub mode: Option<String>,
160

161
    /// Number of concurrent workers
162
    #[arg(short = 'c', long, value_name = "N")]
163
    pub concurrency: Option<u32>,
164

165
    /// Total number of requests to send
166
    #[arg(short = 'n', long, value_name = "N")]
167
    pub requests: Option<u64>,
168

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

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

177
    /// Warmup period excluded from final metrics (e.g., 5s)
178
    #[arg(long, value_name = "DURATION")]
179
    pub warmup: Option<String>,
180

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

185
    /// Maximum requests per second (rate limit)
186
    #[arg(long, value_name = "RPS")]
187
    pub max_rps: Option<f64>,
188

189
    /// Load schedule strategy (const, step, line)
190
    #[arg(long = "load-schedule", value_name = "SCHEDULE")]
191
    pub load_schedule: Option<String>,
192

193
    /// Starting RPS for step/line load schedules
194
    #[arg(long = "load-start", value_name = "RPS")]
195
    pub load_start: Option<f64>,
196

197
    /// Step/slope RPS delta for step/line schedules
198
    #[arg(long = "load-step", value_name = "RPS_DELTA")]
199
    pub load_step: Option<f64>,
200

201
    /// Optional ending RPS for step/line schedules
202
    #[arg(long = "load-end", value_name = "RPS")]
203
    pub load_end: Option<f64>,
204

205
    /// Duration of each step for step schedule
206
    #[arg(long = "load-step-duration", value_name = "DURATION")]
207
    pub load_step_duration: Option<String>,
208

209
    /// Maximum duration of load adjustments
210
    #[arg(long = "load-max-duration", value_name = "DURATION")]
211
    pub load_max_duration: Option<String>,
212

213
    /// Number of gRPC connections to use (<= concurrency)
214
    #[arg(long, value_name = "N")]
215
    pub connections: Option<u32>,
216

217
    /// Connection timeout (e.g., 10s)
218
    #[arg(long, value_name = "DURATION")]
219
    pub connect_timeout: Option<String>,
220

221
    /// Keepalive interval (e.g., 30s)
222
    #[arg(long, value_name = "DURATION")]
223
    pub keepalive: Option<String>,
224

225
    /// Number of CPU cores to use
226
    #[arg(long, value_name = "N")]
227
    pub cpus: Option<usize>,
228

229
    /// User-defined benchmark run name
230
    #[arg(long, value_name = "NAME")]
231
    pub name: Option<String>,
232

233
    /// Assertion mode (fail_fast, collect_all, skip)
234
    #[arg(long, visible_alias = "bench-assert-mode", value_name = "MODE")]
235
    pub assert_mode: Option<String>,
236

237
    /// Disable ASSERTS evaluation to measure transport baseline
238
    #[arg(long, visible_alias = "bench-no-assert", default_value_t = false)]
239
    pub no_assert: bool,
240

241
    /// Sample rate for detailed logging (0.0-1.0)
242
    #[arg(long, value_name = "RATE")]
243
    pub sample_rate: Option<f64>,
244

245
    /// Enable reflection/proto caching
246
    #[arg(long)]
247
    pub cache: Option<bool>,
248

249
    /// Skip first N requests in latency metrics
250
    #[arg(long, value_name = "N")]
251
    pub skip_first: Option<u32>,
252

253
    /// Include errors in latency calculation
254
    #[arg(long)]
255
    pub count_errors_in_latency: Option<bool>,
256

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

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

265
    /// Progress heartbeat interval (e.g. 5s)
266
    #[arg(long = "progress-interval", value_name = "DURATION")]
267
    pub progress_interval: Option<String>,
268

269
    /// Output format (console, json, csv, ndjson, prometheus)
270
    #[arg(
271
        long = "log-format",
272
        visible_alias = "bench-format",
273
        default_value = "console"
274
    )]
275
    pub format: String,
276

277
    /// Output file for benchmark report
278
    #[arg(
279
        short = 'o',
280
        long = "log-output",
281
        visible_alias = "bench-output",
282
        value_name = "OUTPUT_FILE"
283
    )]
284
    pub output: Option<PathBuf>,
285

286
    /// Custom MiniJinja template file for benchmark report
287
    #[arg(long, value_name = "TEMPLATE_FILE")]
288
    pub report_template: Option<PathBuf>,
289

290
    /// Allure output directory for benchmark attachments
291
    #[arg(long, value_name = "DIR")]
292
    pub allure_output_dir: Option<PathBuf>,
293

294
    /// Compact console output (omit histogram)
295
    #[arg(long, default_value_t = false)]
296
    pub compact: bool,
297

298
    /// Filter by tags (AND - file must have ALL tags)
299
    #[arg(long = "tags", value_name = "TAGS")]
300
    pub tags: Vec<String>,
301

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

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

310
    /// List available benchmark profiles and exit
311
    #[arg(long, default_value_t = false)]
312
    pub list_profiles: bool,
313

314
    /// Path to custom profile YAML file
315
    #[arg(long, value_name = "FILE")]
316
    pub profile_file: Option<PathBuf>,
317

318
    /// Direct gRPC method call (service/method) — no .gctf file needed
319
    #[arg(long, value_name = "SERVICE/METHOD")]
320
    pub call: Option<String>,
321

322
    /// Inline JSON request body (used with --call)
323
    #[arg(long, value_name = "JSON")]
324
    pub data: Option<String>,
325
}
326

327
#[derive(Args, Debug, Clone)]
328
pub struct IndexArgs {
329
    /// .gctf file(s) or directory with BENCH.sources definitions
330
    #[arg(required = true)]
331
    pub sources: Vec<PathBuf>,
332

333
    /// Force rebuild of all required indexes
334
    #[arg(long, default_value_t = false)]
335
    pub force: bool,
336

337
    /// Show index file statistics
338
    #[arg(long, default_value_t = false)]
339
    pub stats: bool,
340
}
341

342
#[derive(Args, Debug, Clone)]
343
pub struct QueryArgs {
344
    /// Files or directories to query (default: interactive shell)
345
    #[arg(required = false)]
346
    pub files: Vec<PathBuf>,
347

348
    /// Query expression to execute
349
    #[arg(short = 'q', long, value_name = "EXPR")]
350
    pub query: Option<String>,
351

352
    /// Run in interactive shell mode
353
    #[arg(short = 's', long, default_value_t = false)]
354
    pub shell: bool,
355

356
    /// Index column for direct file mode
357
    #[arg(short = 'i', long, value_name = "COLUMN")]
358
    pub indexed_by: Option<String>,
359

360
    /// Output format (json, csv, table, line, tsv)
361
    #[arg(short = 'f', long, default_value = "table")]
362
    pub format: String,
363

364
    /// Maximum number of rows to return
365
    #[arg(short = 'n', long, value_name = "N")]
366
    pub limit: Option<usize>,
367

368
    /// Skip N rows
369
    #[arg(short = 'o', long, value_name = "N")]
370
    pub offset: Option<usize>,
371

372
    /// Output columns (comma-separated)
373
    #[arg(short = 'c', long, value_name = "COLS")]
374
    pub columns: Option<String>,
375

376
    /// Sort by column (prefix with - for DESC)
377
    #[arg(long, value_name = "COLUMN")]
378
    pub order_by: Option<String>,
379

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

384
    /// Skip header row in output
385
    #[arg(long, default_value_t = false)]
386
    pub no_header: bool,
387
}
388

389
#[derive(Args, Debug, Clone)]
390
pub struct ListArgs {
391
    /// Path to test file or directory to list
392
    #[arg(required = false)]
393
    pub path: Option<PathBuf>,
394

395
    /// Output format (text, json)
396
    #[arg(long, default_value = "json")]
397
    pub format: String,
398

399
    /// Include test range information
400
    #[arg(long, default_value_t = false)]
401
    pub with_range: bool,
402
}
403

404
#[derive(Args, Debug, Clone)]
405
pub struct InspectArgs {
406
    /// File to inspect
407
    #[arg(required = true)]
408
    pub file: PathBuf,
409

410
    /// Output format (text, json)
411
    #[arg(long, default_value = "text")]
412
    pub format: String,
413
}
414

415
#[derive(Args, Debug, Clone)]
416
pub struct ExplainArgs {
417
    /// File to explain
418
    #[arg(required = true)]
419
    pub file: PathBuf,
420

421
    /// Output format (text, json)
422
    #[arg(long, default_value = "text")]
423
    pub format: String,
424
}
425

426
#[derive(Args, Debug, Clone)]
427
pub struct CheckArgs {
428
    /// Files to validate
429
    #[arg(required = true)]
430
    pub files: Vec<PathBuf>,
431

432
    /// Output format (text, json)
433
    #[arg(long, default_value = "text")]
434
    pub format: String,
435

436
    /// Validate BENCH section configuration
437
    #[arg(long, default_value_t = false)]
438
    pub bench: bool,
439
}
440

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

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

457
    /// Filter by tags (AND - file must have ALL tags)
458
    #[arg(long = "tags", value_name = "TAGS")]
459
    pub tags: Vec<String>,
460

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

465
    /// Run tests in parallel with N workers
466
    #[arg(short = 'p', long, default_value = "auto")]
467
    pub parallel: String,
468

469
    /// Show commands that would be executed without running them
470
    #[arg(short = 'd', long, default_value_t = false)]
471
    pub dry_run: bool,
472

473
    /// Sort test files by type
474
    #[arg(short = 's', long, default_value = "path")]
475
    pub sort: String,
476

477
    /// Generate test reports in specified format
478
    #[arg(long, value_name = "FORMAT")]
479
    pub log_format: Option<String>,
480

481
    /// Output file for test reports (use with --log-format)
482
    #[arg(long, value_name = "OUTPUT_FILE")]
483
    pub log_output: Option<PathBuf>,
484

485
    /// Output streaming JSON events (for IDE integration)
486
    #[arg(long, default_value_t = false)]
487
    pub stream: bool,
488

489
    /// Set timeout for individual tests (seconds)
490
    #[arg(short = 't', long, default_value_t = 30)]
491
    pub timeout: u64,
492

493
    /// Number of retries for failed network calls
494
    #[arg(short = 'r', long, default_value_t = 0)]
495
    pub retry: u32,
496

497
    /// Initial delay between retries (seconds)
498
    #[arg(long, default_value_t = 1.0)]
499
    pub retry_delay: f64,
500

501
    /// Disable retry mechanisms completely
502
    #[arg(long, default_value_t = false)]
503
    pub no_retry: bool,
504

505
    /// Progress indicator style
506
    #[arg(long, default_value = "auto")]
507
    pub progress: String,
508

509
    /// Skip assertion evaluation and print raw server responses
510
    #[arg(long, default_value_t = false)]
511
    pub no_assert: bool,
512

513
    /// Generate Proto API coverage report
514
    #[arg(long, default_value_t = false)]
515
    pub coverage: bool,
516

517
    /// Coverage output format (text, json)
518
    #[arg(long, default_value = "text")]
519
    pub coverage_format: String,
520

521
    /// Write/Overwrite test files with actual server responses (Snapshot Mode)
522
    #[arg(short = 'w', long, default_value_t = false)]
523
    pub write: bool,
524
}
525

526
#[derive(Args, Debug, Clone)]
527
pub struct ReflectArgs {
528
    /// Service/method symbol OR .gctf file path
529
    pub symbol: Option<String>,
530

531
    /// Server address (overrides environment variable)
532
    #[arg(long)]
533
    pub address: Option<String>,
534

535
    /// Plaintext connection (no TLS). If omitted, localhost/http addresses default to plaintext.
536
    #[arg(long, default_value_t = false)]
537
    pub plaintext: bool,
538

539
    /// Output format (text, json)
540
    #[arg(long, default_value = "text")]
541
    pub format: String,
542

543
    /// List all methods with full signatures
544
    #[arg(long, default_value_t = false)]
545
    pub list_methods: bool,
546

547
    /// Describe a method's request and response message fields
548
    #[arg(long, value_name = "SERVICE/METHOD")]
549
    pub describe: Option<String>,
550

551
    /// CA certificate path for TLS
552
    #[arg(long)]
553
    pub tls_ca: Option<String>,
554

555
    /// Client certificate path for TLS
556
    #[arg(long)]
557
    pub tls_cert: Option<String>,
558

559
    /// Client key path for TLS
560
    #[arg(long)]
561
    pub tls_key: Option<String>,
562
}
563

564
#[derive(Args, Debug, Clone)]
565
pub struct FmtArgs {
566
    /// Files to format
567
    #[arg(required = true)]
568
    pub files: Vec<PathBuf>,
569

570
    /// Write changes to file instead of stdout
571
    #[arg(short = 'w', long, default_value_t = false)]
572
    pub write: bool,
573
}
574

575
#[derive(Args, Debug, Clone)]
576
pub struct CallArgs {
577
    /// File to call (omit if using -e)
578
    pub file: Option<PathBuf>,
579

580
    /// Inline endpoint (package.Service/Method), skips file
581
    #[arg(
582
        short = 'e',
583
        long,
584
        value_name = "SERVICE/METHOD",
585
        conflicts_with = "file"
586
    )]
587
    pub endpoint: Option<String>,
588

589
    /// Inline JSON request body (used with -e)
590
    #[arg(short = 'd', long, value_name = "JSON", requires = "endpoint")]
591
    pub data: Option<String>,
592

593
    /// Document index for multi-document .gctf files (1-based)
594
    #[arg(long)]
595
    pub doc_index: Option<usize>,
596

597
    /// Include response headers in output, printed before body (-i)
598
    #[arg(short = 'i', long, default_value_t = false)]
599
    pub include: bool,
600

601
    /// Verbose mode: show request/response metadata (-v)
602
    #[arg(short = 'v', long, default_value_t = false)]
603
    pub verbose: bool,
604

605
    /// Extra verbose mode: verbose output plus timing (-vv)
606
    #[arg(long = "vv", default_value_t = false)]
607
    pub very_verbose: bool,
608

609
    /// Output to file instead of stdout (-o)
610
    #[arg(short = 'o', long)]
611
    pub output: Option<PathBuf>,
612

613
    /// Dump response headers to file (-D)
614
    #[arg(short = 'D', long)]
615
    pub dump_header: Option<PathBuf>,
616

617
    /// Silent mode (-s)
618
    #[arg(short = 's', long, default_value_t = false)]
619
    pub silent: bool,
620

621
    /// Show errors (-S)
622
    #[arg(short = 'S', long, default_value_t = false)]
623
    pub show_error: bool,
624

625
    /// Connection timeout in seconds
626
    #[arg(long, default_value_t = 30)]
627
    pub connect_timeout: u64,
628

629
    /// Skip TLS certificate verification
630
    #[arg(long, default_value_t = false)]
631
    pub insecure: bool,
632

633
    /// Request timeout in seconds
634
    #[arg(long, default_value_t = 60)]
635
    pub max_time: u64,
636

637
    /// Run as benchmark instead of single call
638
    #[arg(long, default_value_t = false)]
639
    pub bench: bool,
640

641
    /// Benchmark concurrency (with --bench)
642
    #[arg(long, requires = "bench")]
643
    pub concurrency: Option<u32>,
644

645
    /// Benchmark requests (with --bench)
646
    #[arg(long, requires = "bench")]
647
    pub requests: Option<u64>,
648

649
    /// Benchmark duration (with --bench), e.g. "30s"
650
    #[arg(long, requires = "bench")]
651
    pub duration: Option<String>,
652
}
653

654
#[derive(Args, Debug, Clone)]
655
pub struct GenArgs {
656
    /// Output file for generated gctf (stdout if omitted)
657
    #[arg(short = 'o', long)]
658
    pub output: Option<PathBuf>,
659

660
    #[command(subcommand)]
661
    pub source: GenSource,
662
}
663

664
#[derive(Subcommand, Debug, Clone)]
665
pub enum GenSource {
666
    /// Generate from grpcurl invocation
667
    Grpcurl(GenGrpcurlArgs),
668
}
669

670
#[derive(Args, Debug, Clone)]
671
#[command(trailing_var_arg = true)]
672
pub struct GenGrpcurlArgs {
673
    /// Execute invocation and append RESPONSE/ERROR
674
    #[arg(short = 'e', long, default_value_t = false)]
675
    pub execute: bool,
676

677
    /// grpcurl command arguments after `gen grpcurl`
678
    #[arg(required = true, allow_hyphen_values = true)]
679
    pub grpcurl_args: Vec<String>,
680
}
681

682
impl Cli {
683
    /// Get parallel job count (auto-detect if set to "auto")
684
    pub fn parallel_jobs(&self) -> usize {
1✔
685
        let parallel = match &self.command {
1✔
686
            Some(Commands::Run(args)) => &args.parallel,
1✔
687
            _ => &self.run_args.parallel,
×
688
        };
689

690
        if parallel == "auto" {
1✔
691
            // Auto-detect CPU count
692
            std::thread::available_parallelism()
1✔
693
                .ok()
1✔
694
                .map(|n| n.get())
1✔
695
                .unwrap_or(4)
1✔
696
        } else {
697
            parallel.parse().unwrap_or(1)
×
698
        }
699
    }
1✔
700

701
    /// Get progress mode
702
    pub fn progress_mode(&self) -> ProgressMode {
1✔
703
        let progress = match &self.command {
1✔
704
            Some(Commands::Run(args)) => &args.progress,
1✔
705
            _ => &self.run_args.progress,
×
706
        };
707

708
        match progress.as_str() {
1✔
709
            "dots" => ProgressMode::Dots,
1✔
710
            "bar" => ProgressMode::Dots,
1✔
711
            "none" => ProgressMode::None,
1✔
712
            "auto" => {
1✔
713
                if self.verbose {
1✔
714
                    ProgressMode::Verbose
×
715
                } else {
716
                    ProgressMode::Dots
1✔
717
                }
718
            }
719
            _ => ProgressMode::Dots,
×
720
        }
721
    }
1✔
722

723
    /// Get log format
724
    pub fn log_format_mode(&self) -> Option<LogFormat> {
1✔
725
        let log_format = match &self.command {
1✔
726
            Some(Commands::Run(args)) => &args.log_format,
1✔
727
            _ => &self.run_args.log_format,
×
728
        };
729

730
        log_format.as_ref().map(|fmt| match fmt.as_str() {
1✔
731
            "junit" => LogFormat::JUnit,
×
732
            "json" => LogFormat::Json,
×
NEW
733
            "yaml" => LogFormat::Yaml,
×
734
            "allure" => LogFormat::Allure,
×
735
            _ => LogFormat::Console,
×
736
        })
×
737
    }
1✔
738

739
    /// Get optimizer level from CLI flag or default for command
740
    pub fn optimize_level(
27✔
741
        &self,
27✔
742
        default: crate::optimizer::OptimizeLevel,
27✔
743
    ) -> crate::optimizer::OptimizeLevel {
27✔
744
        use crate::optimizer::OptimizeLevel;
745
        match self.optimize.as_str() {
27✔
746
            "0" | "none" => OptimizeLevel::None,
27✔
747
            "1" | "safe" => OptimizeLevel::Safe,
27✔
748
            "2" | "advisory" => OptimizeLevel::Advisory,
27✔
749
            "3" | "aggressive" => OptimizeLevel::Aggressive,
26✔
750
            _ => default,
25✔
751
        }
752
    }
27✔
753

754
    /// Helper to get effective RunArgs
755
    pub fn get_run_args(&self) -> &RunArgs {
×
756
        match &self.command {
×
757
            Some(Commands::Run(args)) => args,
×
758
            _ => &self.run_args,
×
759
        }
760
    }
×
761
}
762

763
fn is_json_format(value: &str) -> bool {
33✔
764
    value.eq_ignore_ascii_case("json")
33✔
765
}
33✔
766

767
/// Trait for CLI argument types that have a `--format` option.
768
pub trait HasFormat {
769
    fn format(&self) -> &str;
770

771
    fn is_json(&self) -> bool {
33✔
772
        is_json_format(self.format())
33✔
773
    }
33✔
774
}
775

776
impl HasFormat for ListArgs {
777
    fn format(&self) -> &str {
3✔
778
        &self.format
3✔
779
    }
3✔
780
}
781

782
impl HasFormat for InspectArgs {
783
    fn format(&self) -> &str {
8✔
784
        &self.format
8✔
785
    }
8✔
786
}
787

788
impl HasFormat for ExplainArgs {
789
    fn format(&self) -> &str {
3✔
790
        &self.format
3✔
791
    }
3✔
792
}
793

794
impl HasFormat for GrpcurlArgs {
795
    fn format(&self) -> &str {
9✔
796
        &self.format
9✔
797
    }
9✔
798
}
799

800
impl HasFormat for CheckArgs {
801
    fn format(&self) -> &str {
10✔
802
        &self.format
10✔
803
    }
10✔
804
}
805

806
impl HasFormat for BenchArgs {
807
    fn format(&self) -> &str {
×
808
        &self.format
×
809
    }
×
810
}
811

812
#[derive(Args, Debug, Clone)]
813
pub struct PlayArgs {
814
    /// Port to listen on (default: 4755)
815
    #[arg(long, default_value = "4755")]
816
    pub port: u16,
817

818
    /// Directory with .gctf collections (default: current dir)
819
    #[arg(long, default_value = ".")]
820
    pub dir: std::path::PathBuf,
821

822
    /// Open browser automatically
823
    #[arg(long, default_value_t = false)]
824
    pub open: bool,
825

826
    /// Initialize .grpctestify project directory
827
    #[arg(long, default_value_t = false)]
828
    pub init: bool,
829
}
830

831
impl RunArgs {
832
    #[must_use]
833
    pub fn is_json_coverage(&self) -> bool {
×
834
        is_json_format(&self.coverage_format)
×
835
    }
×
836
}
837

838
#[cfg(test)]
839
mod tests {
840
    use super::*;
841
    use clap::Parser;
842

843
    #[test]
844
    fn parse_call_defaults() {
1✔
845
        let cli = Cli::parse_from(["grpctestify", "call", "test.gctf"]);
1✔
846
        let Some(Commands::Call(call)) = cli.command else {
1✔
847
            panic!("expected call command");
×
848
        };
849

850
        assert_eq!(call.file, Some(PathBuf::from("test.gctf")));
1✔
851
        assert_eq!(call.doc_index, None);
1✔
852
        assert_eq!(call.endpoint, None);
1✔
853
        assert_eq!(call.data, None);
1✔
854
        assert!(!call.include);
1✔
855
        assert!(!call.verbose);
1✔
856
        assert!(!call.very_verbose);
1✔
857
        assert!(!call.silent);
1✔
858
        assert!(!call.show_error);
1✔
859
        assert_eq!(call.connect_timeout, 30);
1✔
860
        assert_eq!(call.max_time, 60);
1✔
861
    }
1✔
862

863
    #[test]
864
    fn parse_call_inline_endpoint() {
1✔
865
        let cli = Cli::parse_from([
1✔
866
            "grpctestify",
1✔
867
            "call",
1✔
868
            "-e",
1✔
869
            "svc.Method/Call",
1✔
870
            "-d",
1✔
871
            r#"{"name":"test"}"#,
1✔
872
        ]);
1✔
873
        let Some(Commands::Call(call)) = cli.command else {
1✔
NEW
874
            panic!("expected call command");
×
875
        };
876
        assert_eq!(call.endpoint.as_deref(), Some("svc.Method/Call"));
1✔
877
        assert_eq!(call.data.as_deref(), Some(r#"{"name":"test"}"#));
1✔
878
        assert_eq!(call.file, None);
1✔
879
    }
1✔
880

881
    #[test]
882
    fn parse_call_verbose_flags() {
1✔
883
        let cli = Cli::parse_from(["grpctestify", "call", "-v", "test.gctf"]);
1✔
884
        let Some(Commands::Call(call)) = cli.command else {
1✔
885
            panic!()
×
886
        };
887
        assert!(call.verbose);
1✔
888
        assert!(!call.very_verbose);
1✔
889

890
        let cli = Cli::parse_from(["grpctestify", "call", "--vv", "test.gctf"]);
1✔
891
        let Some(Commands::Call(call)) = cli.command else {
1✔
892
            panic!()
×
893
        };
894
        assert!(!call.verbose);
1✔
895
        assert!(call.very_verbose);
1✔
896
    }
1✔
897

898
    #[test]
899
    fn parse_call_include_and_dump_header() {
1✔
900
        let cli = Cli::parse_from(["grpctestify", "call", "-i", "-D", "/tmp/h.txt", "test.gctf"]);
1✔
901
        let Some(Commands::Call(call)) = cli.command else {
1✔
902
            panic!()
×
903
        };
904
        assert!(call.include);
1✔
905
        assert_eq!(call.dump_header, Some(PathBuf::from("/tmp/h.txt")));
1✔
906
    }
1✔
907

908
    #[test]
909
    fn parse_call_silent_and_show_error() {
1✔
910
        let cli = Cli::parse_from(["grpctestify", "call", "-s", "-S", "test.gctf"]);
1✔
911
        let Some(Commands::Call(call)) = cli.command else {
1✔
912
            panic!()
×
913
        };
914
        assert!(call.silent);
1✔
915
        assert!(call.show_error);
1✔
916
    }
1✔
917

918
    #[test]
919
    fn parse_gen_with_output_before_source() {
1✔
920
        let cli = Cli::parse_from([
1✔
921
            "grpctestify",
1✔
922
            "gen",
1✔
923
            "-o",
1✔
924
            "out.gctf",
1✔
925
            "grpcurl",
1✔
926
            "-plaintext",
1✔
927
            "localhost:4770",
1✔
928
            "svc.Method/Call",
1✔
929
        ]);
1✔
930

931
        let Some(Commands::Gen(gen_args)) = cli.command else {
1✔
932
            panic!("expected gen command");
×
933
        };
934
        assert_eq!(gen_args.output, Some(PathBuf::from("out.gctf")));
1✔
935

936
        let GenSource::Grpcurl(grpcurl) = gen_args.source;
1✔
937
        assert_eq!(
1✔
938
            grpcurl.grpcurl_args,
939
            vec![
1✔
940
                "-plaintext".to_string(),
1✔
941
                "localhost:4770".to_string(),
1✔
942
                "svc.Method/Call".to_string()
1✔
943
            ]
944
        );
945
    }
1✔
946

947
    #[test]
948
    fn parse_gen_grpcurl_preserves_hyphen_args() {
1✔
949
        let cli = Cli::parse_from([
1✔
950
            "grpctestify",
1✔
951
            "gen",
1✔
952
            "grpcurl",
1✔
953
            "-H",
1✔
954
            "x-api-key: abc",
1✔
955
            "-d",
1✔
956
            "{}",
1✔
957
            "localhost:4770",
1✔
958
            "svc.Method/Call",
1✔
959
        ]);
1✔
960

961
        let Some(Commands::Gen(gen_args)) = cli.command else {
1✔
962
            panic!("expected gen command");
×
963
        };
964

965
        let GenSource::Grpcurl(grpcurl) = gen_args.source;
1✔
966
        assert_eq!(grpcurl.grpcurl_args[0], "-H");
1✔
967
        assert_eq!(grpcurl.grpcurl_args[2], "-d");
1✔
968
        assert_eq!(grpcurl.grpcurl_args[3], "{}");
1✔
969
        assert_eq!(grpcurl.grpcurl_args[4], "localhost:4770");
1✔
970
    }
1✔
971

972
    #[test]
973
    fn parse_bench_extended_options() {
1✔
974
        let cli = Cli::parse_from([
1✔
975
            "grpctestify",
1✔
976
            "bench",
1✔
977
            "tests/",
1✔
978
            "-c",
1✔
979
            "8",
1✔
980
            "-n",
1✔
981
            "1000",
1✔
982
            "--max-duration",
1✔
983
            "30s",
1✔
984
            "--connections",
1✔
985
            "4",
1✔
986
            "--connect-timeout",
1✔
987
            "2s",
1✔
988
            "--keepalive",
1✔
989
            "10s",
1✔
990
            "--cpus",
1✔
991
            "2",
1✔
992
            "--name",
1✔
993
            "smoke-bench",
1✔
994
        ]);
1✔
995

996
        let Some(Commands::Bench(args)) = cli.command else {
1✔
997
            panic!("expected bench command");
×
998
        };
999

1000
        assert_eq!(args.test_paths, vec![PathBuf::from("tests/")]);
1✔
1001
        assert_eq!(args.concurrency, Some(8));
1✔
1002
        assert_eq!(args.requests, Some(1000));
1✔
1003
        assert_eq!(args.max_duration.as_deref(), Some("30s"));
1✔
1004
        assert_eq!(args.connections, Some(4));
1✔
1005
        assert_eq!(args.connect_timeout.as_deref(), Some("2s"));
1✔
1006
        assert_eq!(args.keepalive.as_deref(), Some("10s"));
1✔
1007
        assert_eq!(args.cpus, Some(2));
1✔
1008
        assert_eq!(args.name.as_deref(), Some("smoke-bench"));
1✔
1009
    }
1✔
1010

1011
    #[test]
1012
    fn parse_bench_run_style_option_names() {
1✔
1013
        let cli = Cli::parse_from([
1✔
1014
            "grpctestify",
1✔
1015
            "bench",
1✔
1016
            "tests/",
1✔
1017
            "--no-assert",
1✔
1018
            "--assert-mode",
1✔
1019
            "sampled",
1✔
1020
            "--log-format",
1✔
1021
            "json",
1✔
1022
            "--log-output",
1✔
1023
            "bench.json",
1✔
1024
            "--latency-percentiles",
1✔
1025
            "p50,p90,p99",
1✔
1026
            "--duration-stop",
1✔
1027
            "wait",
1✔
1028
            "--progress-interval",
1✔
1029
            "3s",
1✔
1030
            "--ramp-up",
1✔
1031
            "3s",
1✔
1032
            "--warmup",
1✔
1033
            "1s",
1✔
1034
        ]);
1✔
1035

1036
        let Some(Commands::Bench(args)) = cli.command else {
1✔
1037
            panic!("expected bench command");
×
1038
        };
1039

1040
        assert!(args.no_assert);
1✔
1041
        assert_eq!(args.assert_mode.as_deref(), Some("sampled"));
1✔
1042
        assert_eq!(args.format, "json");
1✔
1043
        assert_eq!(args.output, Some(PathBuf::from("bench.json")));
1✔
1044
        assert_eq!(args.latency_percentiles.as_deref(), Some("p50,p90,p99"));
1✔
1045
        assert_eq!(args.duration_stop.as_deref(), Some("wait"));
1✔
1046
        assert_eq!(args.progress_interval.as_deref(), Some("3s"));
1✔
1047
        assert_eq!(args.ramp_up.as_deref(), Some("3s"));
1✔
1048
        assert_eq!(args.warmup.as_deref(), Some("1s"));
1✔
1049
    }
1✔
1050

1051
    #[test]
1052
    fn parse_bench_load_schedule_options() {
1✔
1053
        let cli = Cli::parse_from([
1✔
1054
            "grpctestify",
1✔
1055
            "bench",
1✔
1056
            "tests/",
1✔
1057
            "-c",
1✔
1058
            "10",
1✔
1059
            "-n",
1✔
1060
            "10000",
1✔
1061
            "--load-schedule",
1✔
1062
            "step",
1✔
1063
            "--load-start",
1✔
1064
            "50",
1✔
1065
            "--load-end",
1✔
1066
            "150",
1✔
1067
            "--load-step",
1✔
1068
            "10",
1✔
1069
            "--load-step-duration",
1✔
1070
            "5s",
1✔
1071
            "--load-max-duration",
1✔
1072
            "40s",
1✔
1073
        ]);
1✔
1074

1075
        let Some(Commands::Bench(args)) = cli.command else {
1✔
1076
            panic!("expected bench command");
×
1077
        };
1078

1079
        assert_eq!(args.concurrency, Some(10));
1✔
1080
        assert_eq!(args.requests, Some(10000));
1✔
1081
        assert_eq!(args.load_schedule.as_deref(), Some("step"));
1✔
1082
        assert_eq!(args.load_start, Some(50.0));
1✔
1083
        assert_eq!(args.load_end, Some(150.0));
1✔
1084
        assert_eq!(args.load_step, Some(10.0));
1✔
1085
        assert_eq!(args.load_step_duration.as_deref(), Some("5s"));
1✔
1086
        assert_eq!(args.load_max_duration.as_deref(), Some("40s"));
1✔
1087
    }
1✔
1088

1089
    #[test]
1090
    fn parse_index_command() {
1✔
1091
        let cli = Cli::parse_from([
1✔
1092
            "grpctestify",
1✔
1093
            "index",
1✔
1094
            "tests/bench/user_lookup.gctf",
1✔
1095
            "--force",
1✔
1096
        ]);
1✔
1097

1098
        let Some(Commands::Index(args)) = cli.command else {
1✔
1099
            panic!("expected index command");
×
1100
        };
1101
        assert_eq!(
1✔
1102
            args.sources,
1103
            vec![PathBuf::from("tests/bench/user_lookup.gctf")]
1✔
1104
        );
1105
        assert!(args.force);
1✔
1106
    }
1✔
1107
}
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