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