1use crate::crud_flow::{CrudFlowConfig, CrudFlowDetector};
4use crate::data_driven::{DataDistribution, DataDrivenConfig, DataDrivenGenerator, DataMapping};
5use crate::error::{BenchError, Result};
6use crate::executor::K6Executor;
7use crate::invalid_data::{InvalidDataConfig, InvalidDataGenerator, InvalidDataType};
8use crate::k6_gen::{K6Config, K6ScriptGenerator};
9use crate::mock_integration::{
10 MockIntegrationConfig, MockIntegrationGenerator, MockServerDetector,
11};
12use crate::parallel_executor::{AggregatedResults, ParallelExecutor};
13use crate::parallel_requests::{ParallelConfig, ParallelRequestGenerator};
14use crate::param_overrides::ParameterOverrides;
15use crate::reporter::TerminalReporter;
16use crate::request_gen::RequestGenerator;
17use crate::scenarios::LoadScenario;
18use crate::security_payloads::{
19 SecurityCategory, SecurityPayload, SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
20};
21use crate::spec_dependencies::{
22 topological_sort, DependencyDetector, ExtractedValues, SpecDependencyConfig,
23};
24use crate::spec_parser::SpecParser;
25use crate::target_parser::parse_targets_file;
26use crate::wafbench::WafBenchLoader;
27use mockforge_core::openapi::multi_spec::{
28 load_specs_from_directory, load_specs_from_files, merge_specs, ConflictStrategy,
29};
30use mockforge_core::openapi::spec::OpenApiSpec;
31use std::collections::HashMap;
32use std::path::PathBuf;
33use std::str::FromStr;
34
35pub struct BenchCommand {
37 pub spec: Vec<PathBuf>,
39 pub spec_dir: Option<PathBuf>,
41 pub merge_conflicts: String,
43 pub spec_mode: String,
45 pub dependency_config: Option<PathBuf>,
47 pub target: String,
48 pub base_path: Option<String>,
51 pub duration: String,
52 pub vus: u32,
53 pub scenario: String,
54 pub operations: Option<String>,
55 pub exclude_operations: Option<String>,
59 pub auth: Option<String>,
60 pub headers: Option<String>,
61 pub output: PathBuf,
62 pub generate_only: bool,
63 pub script_output: Option<PathBuf>,
64 pub threshold_percentile: String,
65 pub threshold_ms: u64,
66 pub max_error_rate: f64,
67 pub verbose: bool,
68 pub skip_tls_verify: bool,
69 pub targets_file: Option<PathBuf>,
71 pub max_concurrency: Option<u32>,
73 pub results_format: String,
75 pub params_file: Option<PathBuf>,
80
81 pub crud_flow: bool,
84 pub flow_config: Option<PathBuf>,
86 pub extract_fields: Option<String>,
88
89 pub parallel_create: Option<u32>,
92
93 pub data_file: Option<PathBuf>,
96 pub data_distribution: String,
98 pub data_mappings: Option<String>,
100 pub per_uri_control: bool,
102
103 pub error_rate: Option<f64>,
106 pub error_types: Option<String>,
108
109 pub security_test: bool,
112 pub security_payloads: Option<PathBuf>,
114 pub security_categories: Option<String>,
116 pub security_target_fields: Option<String>,
118
119 pub wafbench_dir: Option<String>,
122}
123
124impl BenchCommand {
125 pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
127 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
128
129 if !self.spec.is_empty() {
131 let specs = load_specs_from_files(self.spec.clone())
132 .await
133 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
134 all_specs.extend(specs);
135 }
136
137 if let Some(spec_dir) = &self.spec_dir {
139 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
140 BenchError::Other(format!("Failed to load specs from directory: {}", e))
141 })?;
142 all_specs.extend(dir_specs);
143 }
144
145 if all_specs.is_empty() {
146 return Err(BenchError::Other(
147 "No spec files provided. Use --spec or --spec-dir.".to_string(),
148 ));
149 }
150
151 if all_specs.len() == 1 {
153 return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
155 }
156
157 let conflict_strategy = match self.merge_conflicts.as_str() {
159 "first" => ConflictStrategy::First,
160 "last" => ConflictStrategy::Last,
161 _ => ConflictStrategy::Error,
162 };
163
164 merge_specs(all_specs, conflict_strategy)
165 .map_err(|e| BenchError::Other(format!("Failed to merge specs: {}", e)))
166 }
167
168 fn get_spec_display_name(&self) -> String {
170 if self.spec.len() == 1 {
171 self.spec[0].to_string_lossy().to_string()
172 } else if !self.spec.is_empty() {
173 format!("{} spec files", self.spec.len())
174 } else if let Some(dir) = &self.spec_dir {
175 format!("specs from {}", dir.display())
176 } else {
177 "no specs".to_string()
178 }
179 }
180
181 pub async fn execute(&self) -> Result<()> {
183 if let Some(targets_file) = &self.targets_file {
185 return self.execute_multi_target(targets_file).await;
186 }
187
188 if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
190 return self.execute_sequential_specs().await;
191 }
192
193 TerminalReporter::print_header(
196 &self.get_spec_display_name(),
197 &self.target,
198 0, &self.scenario,
200 Self::parse_duration(&self.duration)?,
201 );
202
203 if !K6Executor::is_k6_installed() {
205 TerminalReporter::print_error("k6 is not installed");
206 TerminalReporter::print_warning(
207 "Install k6 from: https://k6.io/docs/get-started/installation/",
208 );
209 return Err(BenchError::K6NotFound);
210 }
211
212 TerminalReporter::print_progress("Loading OpenAPI specification(s)...");
214 let merged_spec = self.load_and_merge_specs().await?;
215 let parser = SpecParser::from_spec(merged_spec);
216 if self.spec.len() > 1 || self.spec_dir.is_some() {
217 TerminalReporter::print_success(&format!(
218 "Loaded and merged {} specification(s)",
219 self.spec.len() + self.spec_dir.as_ref().map(|_| 1).unwrap_or(0)
220 ));
221 } else {
222 TerminalReporter::print_success("Specification loaded");
223 }
224
225 let mock_config = self.build_mock_config().await;
227 if mock_config.is_mock_server {
228 TerminalReporter::print_progress("Mock server integration enabled");
229 }
230
231 if self.crud_flow {
233 return self.execute_crud_flow(&parser).await;
234 }
235
236 TerminalReporter::print_progress("Extracting API operations...");
238 let mut operations = if let Some(filter) = &self.operations {
239 parser.filter_operations(filter)?
240 } else {
241 parser.get_operations()
242 };
243
244 if let Some(exclude) = &self.exclude_operations {
246 let before_count = operations.len();
247 operations = parser.exclude_operations(operations, exclude)?;
248 let excluded_count = before_count - operations.len();
249 if excluded_count > 0 {
250 TerminalReporter::print_progress(&format!(
251 "Excluded {} operations matching '{}'",
252 excluded_count, exclude
253 ));
254 }
255 }
256
257 if operations.is_empty() {
258 return Err(BenchError::Other("No operations found in spec".to_string()));
259 }
260
261 TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
262
263 let param_overrides = if let Some(params_file) = &self.params_file {
265 TerminalReporter::print_progress("Loading parameter overrides...");
266 let overrides = ParameterOverrides::from_file(params_file)?;
267 TerminalReporter::print_success(&format!(
268 "Loaded parameter overrides ({} operation-specific, {} defaults)",
269 overrides.operations.len(),
270 if overrides.defaults.is_empty() { 0 } else { 1 }
271 ));
272 Some(overrides)
273 } else {
274 None
275 };
276
277 TerminalReporter::print_progress("Generating request templates...");
279 let templates: Vec<_> = operations
280 .iter()
281 .map(|op| {
282 let op_overrides = param_overrides.as_ref().map(|po| {
283 po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
284 });
285 RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
286 })
287 .collect::<Result<Vec<_>>>()?;
288 TerminalReporter::print_success("Request templates generated");
289
290 let custom_headers = self.parse_headers()?;
292
293 let base_path = self.resolve_base_path(&parser);
295 if let Some(ref bp) = base_path {
296 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
297 }
298
299 TerminalReporter::print_progress("Generating k6 load test script...");
301 let scenario =
302 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
303
304 let k6_config = K6Config {
305 target_url: self.target.clone(),
306 base_path,
307 scenario,
308 duration_secs: Self::parse_duration(&self.duration)?,
309 max_vus: self.vus,
310 threshold_percentile: self.threshold_percentile.clone(),
311 threshold_ms: self.threshold_ms,
312 max_error_rate: self.max_error_rate,
313 auth_header: self.auth.clone(),
314 custom_headers,
315 skip_tls_verify: self.skip_tls_verify,
316 };
317
318 let generator = K6ScriptGenerator::new(k6_config, templates);
319 let mut script = generator.generate()?;
320 TerminalReporter::print_success("k6 script generated");
321
322 let has_advanced_features = self.data_file.is_some()
324 || self.error_rate.is_some()
325 || self.security_test
326 || self.parallel_create.is_some();
327
328 if has_advanced_features {
330 script = self.generate_enhanced_script(&script)?;
331 }
332
333 if mock_config.is_mock_server {
335 let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
336 let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
337 let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
338
339 if let Some(import_end) = script.find("export const options") {
341 script.insert_str(
342 import_end,
343 &format!(
344 "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
345 helper_code, setup_code, teardown_code
346 ),
347 );
348 }
349 }
350
351 TerminalReporter::print_progress("Validating k6 script...");
353 let validation_errors = K6ScriptGenerator::validate_script(&script);
354 if !validation_errors.is_empty() {
355 TerminalReporter::print_error("Script validation failed");
356 for error in &validation_errors {
357 eprintln!(" {}", error);
358 }
359 return Err(BenchError::Other(format!(
360 "Generated k6 script has {} validation error(s). Please check the output above.",
361 validation_errors.len()
362 )));
363 }
364 TerminalReporter::print_success("Script validation passed");
365
366 let script_path = if let Some(output) = &self.script_output {
368 output.clone()
369 } else {
370 self.output.join("k6-script.js")
371 };
372
373 if let Some(parent) = script_path.parent() {
374 std::fs::create_dir_all(parent)?;
375 }
376 std::fs::write(&script_path, &script)?;
377 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
378
379 if self.generate_only {
381 println!("\nScript generated successfully. Run it with:");
382 println!(" k6 run {}", script_path.display());
383 return Ok(());
384 }
385
386 TerminalReporter::print_progress("Executing load test...");
388 let executor = K6Executor::new()?;
389
390 std::fs::create_dir_all(&self.output)?;
391
392 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
393
394 let duration_secs = Self::parse_duration(&self.duration)?;
396 TerminalReporter::print_summary(&results, duration_secs);
397
398 println!("\nResults saved to: {}", self.output.display());
399
400 Ok(())
401 }
402
403 async fn execute_multi_target(&self, targets_file: &PathBuf) -> Result<()> {
405 TerminalReporter::print_progress("Parsing targets file...");
406 let targets = parse_targets_file(targets_file)?;
407 let num_targets = targets.len();
408 TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
409
410 if targets.is_empty() {
411 return Err(BenchError::Other("No targets found in file".to_string()));
412 }
413
414 let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
416 let max_concurrency = max_concurrency.min(num_targets); TerminalReporter::print_header(
420 &self.get_spec_display_name(),
421 &format!("{} targets", num_targets),
422 0,
423 &self.scenario,
424 Self::parse_duration(&self.duration)?,
425 );
426
427 let executor = ParallelExecutor::new(
429 BenchCommand {
430 spec: self.spec.clone(),
432 spec_dir: self.spec_dir.clone(),
433 merge_conflicts: self.merge_conflicts.clone(),
434 spec_mode: self.spec_mode.clone(),
435 dependency_config: self.dependency_config.clone(),
436 target: self.target.clone(), base_path: self.base_path.clone(),
438 duration: self.duration.clone(),
439 vus: self.vus,
440 scenario: self.scenario.clone(),
441 operations: self.operations.clone(),
442 exclude_operations: self.exclude_operations.clone(),
443 auth: self.auth.clone(),
444 headers: self.headers.clone(),
445 output: self.output.clone(),
446 generate_only: self.generate_only,
447 script_output: self.script_output.clone(),
448 threshold_percentile: self.threshold_percentile.clone(),
449 threshold_ms: self.threshold_ms,
450 max_error_rate: self.max_error_rate,
451 verbose: self.verbose,
452 skip_tls_verify: self.skip_tls_verify,
453 targets_file: None,
454 max_concurrency: None,
455 results_format: self.results_format.clone(),
456 params_file: self.params_file.clone(),
457 crud_flow: self.crud_flow,
458 flow_config: self.flow_config.clone(),
459 extract_fields: self.extract_fields.clone(),
460 parallel_create: self.parallel_create,
461 data_file: self.data_file.clone(),
462 data_distribution: self.data_distribution.clone(),
463 data_mappings: self.data_mappings.clone(),
464 per_uri_control: self.per_uri_control,
465 error_rate: self.error_rate,
466 error_types: self.error_types.clone(),
467 security_test: self.security_test,
468 security_payloads: self.security_payloads.clone(),
469 security_categories: self.security_categories.clone(),
470 security_target_fields: self.security_target_fields.clone(),
471 wafbench_dir: self.wafbench_dir.clone(),
472 },
473 targets,
474 max_concurrency,
475 );
476
477 let aggregated_results = executor.execute_all().await?;
479
480 self.report_multi_target_results(&aggregated_results)?;
482
483 Ok(())
484 }
485
486 fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
488 TerminalReporter::print_multi_target_summary(results);
490
491 if self.results_format == "aggregated" || self.results_format == "both" {
493 let summary_path = self.output.join("aggregated_summary.json");
494 let summary_json = serde_json::json!({
495 "total_targets": results.total_targets,
496 "successful_targets": results.successful_targets,
497 "failed_targets": results.failed_targets,
498 "aggregated_metrics": {
499 "total_requests": results.aggregated_metrics.total_requests,
500 "total_failed_requests": results.aggregated_metrics.total_failed_requests,
501 "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
502 "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
503 "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
504 "error_rate": results.aggregated_metrics.error_rate,
505 },
506 "target_results": results.target_results.iter().map(|r| {
507 serde_json::json!({
508 "target_url": r.target_url,
509 "target_index": r.target_index,
510 "success": r.success,
511 "error": r.error,
512 "total_requests": r.results.total_requests,
513 "failed_requests": r.results.failed_requests,
514 "avg_duration_ms": r.results.avg_duration_ms,
515 "p95_duration_ms": r.results.p95_duration_ms,
516 "p99_duration_ms": r.results.p99_duration_ms,
517 "output_dir": r.output_dir.to_string_lossy(),
518 })
519 }).collect::<Vec<_>>(),
520 });
521
522 std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
523 TerminalReporter::print_success(&format!(
524 "Aggregated summary saved to: {}",
525 summary_path.display()
526 ));
527 }
528
529 println!("\nResults saved to: {}", self.output.display());
530 println!(" - Per-target results: {}", self.output.join("target_*").display());
531 if self.results_format == "aggregated" || self.results_format == "both" {
532 println!(
533 " - Aggregated summary: {}",
534 self.output.join("aggregated_summary.json").display()
535 );
536 }
537
538 Ok(())
539 }
540
541 pub fn parse_duration(duration: &str) -> Result<u64> {
543 let duration = duration.trim();
544
545 if let Some(secs) = duration.strip_suffix('s') {
546 secs.parse::<u64>()
547 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
548 } else if let Some(mins) = duration.strip_suffix('m') {
549 mins.parse::<u64>()
550 .map(|m| m * 60)
551 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
552 } else if let Some(hours) = duration.strip_suffix('h') {
553 hours
554 .parse::<u64>()
555 .map(|h| h * 3600)
556 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
557 } else {
558 duration
560 .parse::<u64>()
561 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
562 }
563 }
564
565 pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
567 let mut headers = HashMap::new();
568
569 if let Some(header_str) = &self.headers {
570 for pair in header_str.split(',') {
571 let parts: Vec<&str> = pair.splitn(2, ':').collect();
572 if parts.len() != 2 {
573 return Err(BenchError::Other(format!(
574 "Invalid header format: '{}'. Expected 'Key:Value'",
575 pair
576 )));
577 }
578 headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
579 }
580 }
581
582 Ok(headers)
583 }
584
585 fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
594 if let Some(cli_base_path) = &self.base_path {
596 if cli_base_path.is_empty() {
597 return None;
599 }
600 return Some(cli_base_path.clone());
601 }
602
603 parser.get_base_path()
605 }
606
607 async fn build_mock_config(&self) -> MockIntegrationConfig {
609 if MockServerDetector::looks_like_mock_server(&self.target) {
611 if let Ok(info) = MockServerDetector::detect(&self.target).await {
613 if info.is_mockforge {
614 TerminalReporter::print_success(&format!(
615 "Detected MockForge server (version: {})",
616 info.version.as_deref().unwrap_or("unknown")
617 ));
618 return MockIntegrationConfig::mock_server();
619 }
620 }
621 }
622 MockIntegrationConfig::real_api()
623 }
624
625 fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
627 if !self.crud_flow {
628 return None;
629 }
630
631 if let Some(config_path) = &self.flow_config {
633 match CrudFlowConfig::from_file(config_path) {
634 Ok(config) => return Some(config),
635 Err(e) => {
636 TerminalReporter::print_warning(&format!(
637 "Failed to load flow config: {}. Using auto-detection.",
638 e
639 ));
640 }
641 }
642 }
643
644 let extract_fields = self
646 .extract_fields
647 .as_ref()
648 .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
649 .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
650
651 Some(CrudFlowConfig {
652 flows: Vec::new(), default_extract_fields: extract_fields,
654 })
655 }
656
657 fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
659 let data_file = self.data_file.as_ref()?;
660
661 let distribution = DataDistribution::from_str(&self.data_distribution)
662 .unwrap_or(DataDistribution::UniquePerVu);
663
664 let mappings = self
665 .data_mappings
666 .as_ref()
667 .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
668 .unwrap_or_default();
669
670 Some(DataDrivenConfig {
671 file_path: data_file.to_string_lossy().to_string(),
672 distribution,
673 mappings,
674 csv_has_header: true,
675 per_uri_control: self.per_uri_control,
676 per_uri_columns: crate::data_driven::PerUriColumns::default(),
677 })
678 }
679
680 fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
682 let error_rate = self.error_rate?;
683
684 let error_types = self
685 .error_types
686 .as_ref()
687 .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
688 .unwrap_or_default();
689
690 Some(InvalidDataConfig {
691 error_rate,
692 error_types,
693 target_fields: Vec::new(),
694 })
695 }
696
697 fn build_security_config(&self) -> Option<SecurityTestConfig> {
699 if !self.security_test {
700 return None;
701 }
702
703 let categories = self
704 .security_categories
705 .as_ref()
706 .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
707 .unwrap_or_else(|| {
708 let mut default = std::collections::HashSet::new();
709 default.insert(SecurityCategory::SqlInjection);
710 default.insert(SecurityCategory::Xss);
711 default
712 });
713
714 let target_fields = self
715 .security_target_fields
716 .as_ref()
717 .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
718 .unwrap_or_default();
719
720 let custom_payloads_file =
721 self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
722
723 Some(SecurityTestConfig {
724 enabled: true,
725 categories,
726 target_fields,
727 custom_payloads_file,
728 include_high_risk: false,
729 })
730 }
731
732 fn build_parallel_config(&self) -> Option<ParallelConfig> {
734 let count = self.parallel_create?;
735
736 Some(ParallelConfig::new(count))
737 }
738
739 fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
741 let Some(ref wafbench_dir) = self.wafbench_dir else {
742 return Vec::new();
743 };
744
745 let mut loader = WafBenchLoader::new();
746
747 if let Err(e) = loader.load_from_pattern(wafbench_dir) {
748 TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
749 return Vec::new();
750 }
751
752 let stats = loader.stats();
753
754 if stats.files_processed == 0 {
755 TerminalReporter::print_warning(&format!(
756 "No WAFBench YAML files found matching '{}'",
757 wafbench_dir
758 ));
759 return Vec::new();
760 }
761
762 TerminalReporter::print_progress(&format!(
763 "Loaded {} WAFBench files, {} test cases, {} payloads",
764 stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
765 ));
766
767 for (category, count) in &stats.by_category {
769 TerminalReporter::print_progress(&format!(" - {}: {} tests", category, count));
770 }
771
772 for error in &stats.parse_errors {
774 TerminalReporter::print_warning(&format!(" Parse error: {}", error));
775 }
776
777 loader.to_security_payloads()
778 }
779
780 fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
782 let mut enhanced_script = base_script.to_string();
783 let mut additional_code = String::new();
784
785 if let Some(config) = self.build_data_driven_config() {
787 TerminalReporter::print_progress("Adding data-driven testing support...");
788 additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
789 additional_code.push('\n');
790 TerminalReporter::print_success("Data-driven testing enabled");
791 }
792
793 if let Some(config) = self.build_invalid_data_config() {
795 TerminalReporter::print_progress("Adding invalid data testing support...");
796 additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
797 additional_code.push('\n');
798 additional_code
799 .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
800 additional_code.push('\n');
801 additional_code
802 .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
803 additional_code.push('\n');
804 TerminalReporter::print_success(&format!(
805 "Invalid data testing enabled ({}% error rate)",
806 (self.error_rate.unwrap_or(0.0) * 100.0) as u32
807 ));
808 }
809
810 let security_config = self.build_security_config();
812 let wafbench_payloads = self.load_wafbench_payloads();
813
814 if security_config.is_some() || !wafbench_payloads.is_empty() {
815 TerminalReporter::print_progress("Adding security testing support...");
816
817 let mut payload_list: Vec<SecurityPayload> = Vec::new();
819
820 if let Some(ref config) = security_config {
821 payload_list.extend(SecurityPayloads::get_payloads(config));
822 }
823
824 if !wafbench_payloads.is_empty() {
826 TerminalReporter::print_progress(&format!(
827 "Loading {} WAFBench attack patterns...",
828 wafbench_payloads.len()
829 ));
830 payload_list.extend(wafbench_payloads);
831 }
832
833 let target_fields =
834 security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
835
836 additional_code
837 .push_str(&SecurityTestGenerator::generate_payload_selection(&payload_list));
838 additional_code.push('\n');
839 additional_code
840 .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
841 additional_code.push('\n');
842 additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
843 additional_code.push('\n');
844 TerminalReporter::print_success(&format!(
845 "Security testing enabled ({} payloads)",
846 payload_list.len()
847 ));
848 }
849
850 if let Some(config) = self.build_parallel_config() {
852 TerminalReporter::print_progress("Adding parallel execution support...");
853 additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
854 additional_code.push('\n');
855 TerminalReporter::print_success(&format!(
856 "Parallel execution enabled (count: {})",
857 config.count
858 ));
859 }
860
861 if !additional_code.is_empty() {
863 if let Some(import_end) = enhanced_script.find("export const options") {
865 enhanced_script.insert_str(
866 import_end,
867 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
868 );
869 }
870 }
871
872 Ok(enhanced_script)
873 }
874
875 async fn execute_sequential_specs(&self) -> Result<()> {
877 TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
878
879 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
881
882 if !self.spec.is_empty() {
883 let specs = load_specs_from_files(self.spec.clone())
884 .await
885 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
886 all_specs.extend(specs);
887 }
888
889 if let Some(spec_dir) = &self.spec_dir {
890 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
891 BenchError::Other(format!("Failed to load specs from directory: {}", e))
892 })?;
893 all_specs.extend(dir_specs);
894 }
895
896 if all_specs.is_empty() {
897 return Err(BenchError::Other(
898 "No spec files found for sequential execution".to_string(),
899 ));
900 }
901
902 TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
903
904 let execution_order = if let Some(config_path) = &self.dependency_config {
906 TerminalReporter::print_progress("Loading dependency configuration...");
907 let config = SpecDependencyConfig::from_file(config_path)?;
908
909 if !config.disable_auto_detect && config.execution_order.is_empty() {
910 self.detect_and_sort_specs(&all_specs)?
912 } else {
913 config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
915 }
916 } else {
917 self.detect_and_sort_specs(&all_specs)?
919 };
920
921 TerminalReporter::print_success(&format!(
922 "Execution order: {}",
923 execution_order
924 .iter()
925 .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
926 .collect::<Vec<_>>()
927 .join(" → ")
928 ));
929
930 let mut extracted_values = ExtractedValues::new();
932 let total_specs = execution_order.len();
933
934 for (index, spec_path) in execution_order.iter().enumerate() {
935 let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
936
937 TerminalReporter::print_progress(&format!(
938 "[{}/{}] Executing spec: {}",
939 index + 1,
940 total_specs,
941 spec_name
942 ));
943
944 let spec = all_specs
946 .iter()
947 .find(|(p, _)| p == spec_path)
948 .map(|(_, s)| s.clone())
949 .ok_or_else(|| {
950 BenchError::Other(format!("Spec not found: {}", spec_path.display()))
951 })?;
952
953 let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
955
956 extracted_values.merge(&new_values);
958
959 TerminalReporter::print_success(&format!(
960 "[{}/{}] Completed: {} (extracted {} values)",
961 index + 1,
962 total_specs,
963 spec_name,
964 new_values.values.len()
965 ));
966 }
967
968 TerminalReporter::print_success(&format!(
969 "Sequential execution complete: {} specs executed",
970 total_specs
971 ));
972
973 Ok(())
974 }
975
976 fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
978 TerminalReporter::print_progress("Auto-detecting spec dependencies...");
979
980 let mut detector = DependencyDetector::new();
981 let dependencies = detector.detect_dependencies(specs);
982
983 if dependencies.is_empty() {
984 TerminalReporter::print_progress("No dependencies detected, using file order");
985 return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
986 }
987
988 TerminalReporter::print_progress(&format!(
989 "Detected {} cross-spec dependencies",
990 dependencies.len()
991 ));
992
993 for dep in &dependencies {
994 TerminalReporter::print_progress(&format!(
995 " {} → {} (via field '{}')",
996 dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
997 dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
998 dep.field_name
999 ));
1000 }
1001
1002 topological_sort(specs, &dependencies)
1003 }
1004
1005 async fn execute_single_spec(
1007 &self,
1008 spec: &OpenApiSpec,
1009 spec_name: &str,
1010 _external_values: &ExtractedValues,
1011 ) -> Result<ExtractedValues> {
1012 let parser = SpecParser::from_spec(spec.clone());
1013
1014 if self.crud_flow {
1016 self.execute_crud_flow_with_extraction(&parser, spec_name).await
1018 } else {
1019 self.execute_standard_spec(&parser, spec_name).await?;
1021 Ok(ExtractedValues::new())
1022 }
1023 }
1024
1025 async fn execute_crud_flow_with_extraction(
1027 &self,
1028 parser: &SpecParser,
1029 spec_name: &str,
1030 ) -> Result<ExtractedValues> {
1031 let operations = parser.get_operations();
1032 let flows = CrudFlowDetector::detect_flows(&operations);
1033
1034 if flows.is_empty() {
1035 TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1036 return Ok(ExtractedValues::new());
1037 }
1038
1039 TerminalReporter::print_progress(&format!(
1040 " {} CRUD flow(s) in {}",
1041 flows.len(),
1042 spec_name
1043 ));
1044
1045 let handlebars = handlebars::Handlebars::new();
1047 let template = include_str!("templates/k6_crud_flow.hbs");
1048
1049 let custom_headers = self.parse_headers()?;
1050 let config = self.build_crud_flow_config().unwrap_or_default();
1051
1052 let param_overrides = if let Some(params_file) = &self.params_file {
1054 let overrides = ParameterOverrides::from_file(params_file)?;
1055 Some(overrides)
1056 } else {
1057 None
1058 };
1059
1060 let duration_secs = Self::parse_duration(&self.duration)?;
1062 let scenario =
1063 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1064 let stages = scenario.generate_stages(duration_secs, self.vus);
1065
1066 let api_base_path = self.resolve_base_path(parser);
1068
1069 let mut all_headers = custom_headers.clone();
1071 if let Some(auth) = &self.auth {
1072 all_headers.insert("Authorization".to_string(), auth.clone());
1073 }
1074 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1075
1076 let data = serde_json::json!({
1077 "base_url": self.target,
1078 "flows": flows.iter().map(|f| {
1079 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1080 serde_json::json!({
1081 "name": sanitized_name.clone(),
1082 "display_name": f.name,
1083 "base_path": f.base_path,
1084 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1085 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1087 let method_raw = if !parts.is_empty() {
1088 parts[0].to_uppercase()
1089 } else {
1090 "GET".to_string()
1091 };
1092 let method = if !parts.is_empty() {
1093 let m = parts[0].to_lowercase();
1094 if m == "delete" { "del".to_string() } else { m }
1096 } else {
1097 "get".to_string()
1098 };
1099 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1100 let path = if let Some(ref bp) = api_base_path {
1102 format!("{}{}", bp, raw_path)
1103 } else {
1104 raw_path.to_string()
1105 };
1106 let is_get_or_head = method == "get" || method == "head";
1107 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1109
1110 let body_value = if has_body {
1112 param_overrides.as_ref()
1113 .map(|po| po.get_for_operation(None, &method_raw, &raw_path))
1114 .and_then(|oo| oo.body)
1115 .unwrap_or_else(|| serde_json::json!({}))
1116 } else {
1117 serde_json::json!({})
1118 };
1119
1120 let body_json_str = serde_json::to_string(&body_value)
1122 .unwrap_or_else(|_| "{}".to_string());
1123
1124 serde_json::json!({
1125 "operation": s.operation,
1126 "method": method,
1127 "path": path,
1128 "extract": s.extract,
1129 "use_values": s.use_values,
1130 "description": s.description,
1131 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1132 "is_get_or_head": is_get_or_head,
1133 "has_body": has_body,
1134 "body": body_json_str, "body_is_dynamic": false,
1136 })
1137 }).collect::<Vec<_>>(),
1138 })
1139 }).collect::<Vec<_>>(),
1140 "extract_fields": config.default_extract_fields,
1141 "duration_secs": duration_secs,
1142 "max_vus": self.vus,
1143 "auth_header": self.auth,
1144 "custom_headers": custom_headers,
1145 "skip_tls_verify": self.skip_tls_verify,
1146 "stages": stages.iter().map(|s| serde_json::json!({
1148 "duration": s.duration,
1149 "target": s.target,
1150 })).collect::<Vec<_>>(),
1151 "threshold_percentile": self.threshold_percentile,
1152 "threshold_ms": self.threshold_ms,
1153 "max_error_rate": self.max_error_rate,
1154 "headers": headers_json,
1155 "dynamic_imports": Vec::<String>::new(),
1156 "dynamic_globals": Vec::<String>::new(),
1157 });
1158
1159 let script = handlebars
1160 .render_template(template, &data)
1161 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1162
1163 let script_path =
1165 self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1166
1167 std::fs::create_dir_all(self.output.clone())?;
1168 std::fs::write(&script_path, &script)?;
1169
1170 if !self.generate_only {
1171 let executor = K6Executor::new()?;
1172 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1173 std::fs::create_dir_all(&output_dir)?;
1174
1175 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1176 }
1177
1178 Ok(ExtractedValues::new())
1181 }
1182
1183 async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1185 let mut operations = if let Some(filter) = &self.operations {
1186 parser.filter_operations(filter)?
1187 } else {
1188 parser.get_operations()
1189 };
1190
1191 if let Some(exclude) = &self.exclude_operations {
1192 operations = parser.exclude_operations(operations, exclude)?;
1193 }
1194
1195 if operations.is_empty() {
1196 TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1197 return Ok(());
1198 }
1199
1200 TerminalReporter::print_progress(&format!(
1201 " {} operations in {}",
1202 operations.len(),
1203 spec_name
1204 ));
1205
1206 let templates: Vec<_> = operations
1208 .iter()
1209 .map(RequestGenerator::generate_template)
1210 .collect::<Result<Vec<_>>>()?;
1211
1212 let custom_headers = self.parse_headers()?;
1214
1215 let base_path = self.resolve_base_path(parser);
1217
1218 let scenario =
1220 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1221
1222 let k6_config = K6Config {
1223 target_url: self.target.clone(),
1224 base_path,
1225 scenario,
1226 duration_secs: Self::parse_duration(&self.duration)?,
1227 max_vus: self.vus,
1228 threshold_percentile: self.threshold_percentile.clone(),
1229 threshold_ms: self.threshold_ms,
1230 max_error_rate: self.max_error_rate,
1231 auth_header: self.auth.clone(),
1232 custom_headers,
1233 skip_tls_verify: self.skip_tls_verify,
1234 };
1235
1236 let generator = K6ScriptGenerator::new(k6_config, templates);
1237 let script = generator.generate()?;
1238
1239 let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1241
1242 std::fs::create_dir_all(self.output.clone())?;
1243 std::fs::write(&script_path, &script)?;
1244
1245 if !self.generate_only {
1246 let executor = K6Executor::new()?;
1247 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1248 std::fs::create_dir_all(&output_dir)?;
1249
1250 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1251 }
1252
1253 Ok(())
1254 }
1255
1256 async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1258 TerminalReporter::print_progress("Detecting CRUD operations...");
1259
1260 let operations = parser.get_operations();
1261 let flows = CrudFlowDetector::detect_flows(&operations);
1262
1263 if flows.is_empty() {
1264 return Err(BenchError::Other(
1265 "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1266 ));
1267 }
1268
1269 TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1270
1271 for flow in &flows {
1272 TerminalReporter::print_progress(&format!(
1273 " - {}: {} steps",
1274 flow.name,
1275 flow.steps.len()
1276 ));
1277 }
1278
1279 let handlebars = handlebars::Handlebars::new();
1281 let template = include_str!("templates/k6_crud_flow.hbs");
1282
1283 let custom_headers = self.parse_headers()?;
1284 let config = self.build_crud_flow_config().unwrap_or_default();
1285
1286 let param_overrides = if let Some(params_file) = &self.params_file {
1288 TerminalReporter::print_progress("Loading parameter overrides...");
1289 let overrides = ParameterOverrides::from_file(params_file)?;
1290 TerminalReporter::print_success(&format!(
1291 "Loaded parameter overrides ({} operation-specific, {} defaults)",
1292 overrides.operations.len(),
1293 if overrides.defaults.is_empty() { 0 } else { 1 }
1294 ));
1295 Some(overrides)
1296 } else {
1297 None
1298 };
1299
1300 let duration_secs = Self::parse_duration(&self.duration)?;
1302 let scenario =
1303 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1304 let stages = scenario.generate_stages(duration_secs, self.vus);
1305
1306 let api_base_path = self.resolve_base_path(parser);
1308 if let Some(ref bp) = api_base_path {
1309 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1310 }
1311
1312 let mut all_headers = custom_headers.clone();
1314 if let Some(auth) = &self.auth {
1315 all_headers.insert("Authorization".to_string(), auth.clone());
1316 }
1317 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1318
1319 let data = serde_json::json!({
1320 "base_url": self.target,
1321 "flows": flows.iter().map(|f| {
1322 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1324 serde_json::json!({
1325 "name": sanitized_name.clone(), "display_name": f.name, "base_path": f.base_path,
1328 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1329 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1331 let method_raw = if !parts.is_empty() {
1332 parts[0].to_uppercase()
1333 } else {
1334 "GET".to_string()
1335 };
1336 let method = if !parts.is_empty() {
1337 let m = parts[0].to_lowercase();
1338 if m == "delete" { "del".to_string() } else { m }
1340 } else {
1341 "get".to_string()
1342 };
1343 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1344 let path = if let Some(ref bp) = api_base_path {
1346 format!("{}{}", bp, raw_path)
1347 } else {
1348 raw_path.to_string()
1349 };
1350 let is_get_or_head = method == "get" || method == "head";
1351 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1353
1354 let body_value = if has_body {
1356 param_overrides.as_ref()
1357 .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1358 .and_then(|oo| oo.body)
1359 .unwrap_or_else(|| serde_json::json!({}))
1360 } else {
1361 serde_json::json!({})
1362 };
1363
1364 let body_json_str = serde_json::to_string(&body_value)
1366 .unwrap_or_else(|_| "{}".to_string());
1367
1368 serde_json::json!({
1369 "operation": s.operation,
1370 "method": method,
1371 "path": path,
1372 "extract": s.extract,
1373 "use_values": s.use_values,
1374 "description": s.description,
1375 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1376 "is_get_or_head": is_get_or_head,
1377 "has_body": has_body,
1378 "body": body_json_str, "body_is_dynamic": false,
1380 })
1381 }).collect::<Vec<_>>(),
1382 })
1383 }).collect::<Vec<_>>(),
1384 "extract_fields": config.default_extract_fields,
1385 "duration_secs": duration_secs,
1386 "max_vus": self.vus,
1387 "auth_header": self.auth,
1388 "custom_headers": custom_headers,
1389 "skip_tls_verify": self.skip_tls_verify,
1390 "stages": stages.iter().map(|s| serde_json::json!({
1392 "duration": s.duration,
1393 "target": s.target,
1394 })).collect::<Vec<_>>(),
1395 "threshold_percentile": self.threshold_percentile,
1396 "threshold_ms": self.threshold_ms,
1397 "max_error_rate": self.max_error_rate,
1398 "headers": headers_json,
1399 "dynamic_imports": Vec::<String>::new(),
1400 "dynamic_globals": Vec::<String>::new(),
1401 });
1402
1403 let script = handlebars
1404 .render_template(template, &data)
1405 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1406
1407 TerminalReporter::print_progress("Validating CRUD flow script...");
1409 let validation_errors = K6ScriptGenerator::validate_script(&script);
1410 if !validation_errors.is_empty() {
1411 TerminalReporter::print_error("CRUD flow script validation failed");
1412 for error in &validation_errors {
1413 eprintln!(" {}", error);
1414 }
1415 return Err(BenchError::Other(format!(
1416 "CRUD flow script validation failed with {} error(s)",
1417 validation_errors.len()
1418 )));
1419 }
1420
1421 TerminalReporter::print_success("CRUD flow script generated");
1422
1423 let script_path = if let Some(output) = &self.script_output {
1425 output.clone()
1426 } else {
1427 self.output.join("k6-crud-flow-script.js")
1428 };
1429
1430 if let Some(parent) = script_path.parent() {
1431 std::fs::create_dir_all(parent)?;
1432 }
1433 std::fs::write(&script_path, &script)?;
1434 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1435
1436 if self.generate_only {
1437 println!("\nScript generated successfully. Run it with:");
1438 println!(" k6 run {}", script_path.display());
1439 return Ok(());
1440 }
1441
1442 TerminalReporter::print_progress("Executing CRUD flow test...");
1444 let executor = K6Executor::new()?;
1445 std::fs::create_dir_all(&self.output)?;
1446
1447 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1448
1449 let duration_secs = Self::parse_duration(&self.duration)?;
1450 TerminalReporter::print_summary(&results, duration_secs);
1451
1452 Ok(())
1453 }
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458 use super::*;
1459
1460 #[test]
1461 fn test_parse_duration() {
1462 assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
1463 assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
1464 assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
1465 assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
1466 }
1467
1468 #[test]
1469 fn test_parse_duration_invalid() {
1470 assert!(BenchCommand::parse_duration("invalid").is_err());
1471 assert!(BenchCommand::parse_duration("30x").is_err());
1472 }
1473
1474 #[test]
1475 fn test_parse_headers() {
1476 let cmd = BenchCommand {
1477 spec: vec![PathBuf::from("test.yaml")],
1478 spec_dir: None,
1479 merge_conflicts: "error".to_string(),
1480 spec_mode: "merge".to_string(),
1481 dependency_config: None,
1482 target: "http://localhost".to_string(),
1483 base_path: None,
1484 duration: "1m".to_string(),
1485 vus: 10,
1486 scenario: "ramp-up".to_string(),
1487 operations: None,
1488 exclude_operations: None,
1489 auth: None,
1490 headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
1491 output: PathBuf::from("output"),
1492 generate_only: false,
1493 script_output: None,
1494 threshold_percentile: "p(95)".to_string(),
1495 threshold_ms: 500,
1496 max_error_rate: 0.05,
1497 verbose: false,
1498 skip_tls_verify: false,
1499 targets_file: None,
1500 max_concurrency: None,
1501 results_format: "both".to_string(),
1502 params_file: None,
1503 crud_flow: false,
1504 flow_config: None,
1505 extract_fields: None,
1506 parallel_create: None,
1507 data_file: None,
1508 data_distribution: "unique-per-vu".to_string(),
1509 data_mappings: None,
1510 per_uri_control: false,
1511 error_rate: None,
1512 error_types: None,
1513 security_test: false,
1514 security_payloads: None,
1515 security_categories: None,
1516 security_target_fields: None,
1517 wafbench_dir: None,
1518 };
1519
1520 let headers = cmd.parse_headers().unwrap();
1521 assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
1522 assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
1523 }
1524
1525 #[test]
1526 fn test_get_spec_display_name() {
1527 let cmd = BenchCommand {
1528 spec: vec![PathBuf::from("test.yaml")],
1529 spec_dir: None,
1530 merge_conflicts: "error".to_string(),
1531 spec_mode: "merge".to_string(),
1532 dependency_config: None,
1533 target: "http://localhost".to_string(),
1534 base_path: None,
1535 duration: "1m".to_string(),
1536 vus: 10,
1537 scenario: "ramp-up".to_string(),
1538 operations: None,
1539 exclude_operations: None,
1540 auth: None,
1541 headers: None,
1542 output: PathBuf::from("output"),
1543 generate_only: false,
1544 script_output: None,
1545 threshold_percentile: "p(95)".to_string(),
1546 threshold_ms: 500,
1547 max_error_rate: 0.05,
1548 verbose: false,
1549 skip_tls_verify: false,
1550 targets_file: None,
1551 max_concurrency: None,
1552 results_format: "both".to_string(),
1553 params_file: None,
1554 crud_flow: false,
1555 flow_config: None,
1556 extract_fields: None,
1557 parallel_create: None,
1558 data_file: None,
1559 data_distribution: "unique-per-vu".to_string(),
1560 data_mappings: None,
1561 per_uri_control: false,
1562 error_rate: None,
1563 error_types: None,
1564 security_test: false,
1565 security_payloads: None,
1566 security_categories: None,
1567 security_target_fields: None,
1568 wafbench_dir: None,
1569 };
1570
1571 assert_eq!(cmd.get_spec_display_name(), "test.yaml");
1572
1573 let cmd_multi = BenchCommand {
1575 spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
1576 spec_dir: None,
1577 merge_conflicts: "error".to_string(),
1578 spec_mode: "merge".to_string(),
1579 dependency_config: None,
1580 target: "http://localhost".to_string(),
1581 base_path: None,
1582 duration: "1m".to_string(),
1583 vus: 10,
1584 scenario: "ramp-up".to_string(),
1585 operations: None,
1586 exclude_operations: None,
1587 auth: None,
1588 headers: None,
1589 output: PathBuf::from("output"),
1590 generate_only: false,
1591 script_output: None,
1592 threshold_percentile: "p(95)".to_string(),
1593 threshold_ms: 500,
1594 max_error_rate: 0.05,
1595 verbose: false,
1596 skip_tls_verify: false,
1597 targets_file: None,
1598 max_concurrency: None,
1599 results_format: "both".to_string(),
1600 params_file: None,
1601 crud_flow: false,
1602 flow_config: None,
1603 extract_fields: None,
1604 parallel_create: None,
1605 data_file: None,
1606 data_distribution: "unique-per-vu".to_string(),
1607 data_mappings: None,
1608 per_uri_control: false,
1609 error_rate: None,
1610 error_types: None,
1611 security_test: false,
1612 security_payloads: None,
1613 security_categories: None,
1614 security_target_fields: None,
1615 wafbench_dir: None,
1616 };
1617
1618 assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
1619 }
1620}