mockforge_bench/
command.rs

1//! Bench command implementation
2
3use 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
36/// Bench command configuration
37pub struct BenchCommand {
38    /// OpenAPI spec file(s) - can specify multiple
39    pub spec: Vec<PathBuf>,
40    /// Directory containing OpenAPI spec files (discovers .json, .yaml, .yml files)
41    pub spec_dir: Option<PathBuf>,
42    /// Conflict resolution strategy when merging multiple specs: "error" (default), "first", "last"
43    pub merge_conflicts: String,
44    /// Spec mode: "merge" (default) combines all specs, "sequential" runs them in order
45    pub spec_mode: String,
46    /// Dependency configuration file for cross-spec value passing (used with sequential mode)
47    pub dependency_config: Option<PathBuf>,
48    pub target: String,
49    /// API base path prefix (e.g., "/api" or "/v2/api")
50    /// If None, extracts from OpenAPI spec's servers URL
51    pub base_path: Option<String>,
52    pub duration: String,
53    pub vus: u32,
54    pub scenario: String,
55    pub operations: Option<String>,
56    /// Exclude operations from testing (comma-separated)
57    ///
58    /// Supports "METHOD /path" or just "METHOD" to exclude all operations of that type.
59    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    /// Optional file containing multiple targets
71    pub targets_file: Option<PathBuf>,
72    /// Maximum number of parallel executions (for multi-target mode)
73    pub max_concurrency: Option<u32>,
74    /// Results format: "per-target", "aggregated", or "both"
75    pub results_format: String,
76    /// Optional file containing parameter value overrides (JSON or YAML)
77    ///
78    /// Allows users to provide custom values for path parameters, query parameters,
79    /// headers, and request bodies instead of auto-generated placeholder values.
80    pub params_file: Option<PathBuf>,
81
82    // === CRUD Flow Options ===
83    /// Enable CRUD flow mode
84    pub crud_flow: bool,
85    /// Custom CRUD flow configuration file
86    pub flow_config: Option<PathBuf>,
87    /// Fields to extract from responses
88    pub extract_fields: Option<String>,
89
90    // === Parallel Execution Options ===
91    /// Number of resources to create in parallel
92    pub parallel_create: Option<u32>,
93
94    // === Data-Driven Testing Options ===
95    /// Test data file (CSV or JSON)
96    pub data_file: Option<PathBuf>,
97    /// Data distribution strategy
98    pub data_distribution: String,
99    /// Data column to field mappings
100    pub data_mappings: Option<String>,
101    /// Enable per-URI control mode (each row specifies method, uri, body, etc.)
102    pub per_uri_control: bool,
103
104    // === Invalid Data Testing Options ===
105    /// Percentage of requests with invalid data
106    pub error_rate: Option<f64>,
107    /// Types of invalid data to generate
108    pub error_types: Option<String>,
109
110    // === Security Testing Options ===
111    /// Enable security testing
112    pub security_test: bool,
113    /// Custom security payloads file
114    pub security_payloads: Option<PathBuf>,
115    /// Security test categories
116    pub security_categories: Option<String>,
117    /// Fields to target for security injection
118    pub security_target_fields: Option<String>,
119
120    // === WAFBench Integration ===
121    /// WAFBench test directory or glob pattern for loading CRS attack patterns
122    pub wafbench_dir: Option<String>,
123}
124
125impl BenchCommand {
126    /// Load and merge specs from --spec files and --spec-dir
127    pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
128        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
129
130        // Load specs from --spec flags
131        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        // Load specs from --spec-dir if provided
139        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 only one spec, return it directly (extract just the OpenApiSpec)
153        if all_specs.len() == 1 {
154            // Safe to unwrap because we just checked len() == 1
155            return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
156        }
157
158        // Merge multiple specs
159        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    /// Get a display name for the spec(s)
170    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    /// Execute the bench command
183    pub async fn execute(&self) -> Result<()> {
184        // Check if we're in multi-target mode
185        if let Some(targets_file) = &self.targets_file {
186            return self.execute_multi_target(targets_file).await;
187        }
188
189        // Check if we're in sequential spec mode (for dependency handling)
190        if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
191            return self.execute_sequential_specs().await;
192        }
193
194        // Single target mode (existing behavior)
195        // Print header
196        TerminalReporter::print_header(
197            &self.get_spec_display_name(),
198            &self.target,
199            0, // Will be updated later
200            &self.scenario,
201            Self::parse_duration(&self.duration)?,
202        );
203
204        // Validate k6 installation
205        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        // Load and parse spec(s)
214        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        // Check for mock server integration
227        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        // Check for CRUD flow mode
233        if self.crud_flow {
234            return self.execute_crud_flow(&parser).await;
235        }
236
237        // Get operations
238        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        // Apply exclusions if provided
246        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        // Load parameter overrides if provided
265        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        // Generate request templates
279        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        // Parse headers
292        let custom_headers = self.parse_headers()?;
293
294        // Resolve base path (CLI option takes priority over spec's servers URL)
295        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        // Generate k6 script
301        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        // Check if any advanced features are enabled
324        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        // Enhance script with advanced features
330        if has_advanced_features {
331            script = self.generate_enhanced_script(&script)?;
332        }
333
334        // Add mock server integration code
335        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            // Insert mock server code after imports
341            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        // Validate the generated script
353        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        // Write script to file
368        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 generate-only mode, exit here
381        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        // Execute k6
388        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        // Print results
396        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    /// Execute multi-target bench testing
405    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        // Determine max concurrency
416        let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
417        let max_concurrency = max_concurrency.min(num_targets); // Don't exceed number of targets
418
419        // Print header for multi-target mode
420        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        // Create parallel executor
429        let executor = ParallelExecutor::new(
430            BenchCommand {
431                // Clone all fields except targets_file (we don't need it in the executor)
432                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(), // Not used in multi-target mode, but kept for compatibility
438                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        // Execute all targets
479        let aggregated_results = executor.execute_all().await?;
480
481        // Organize and report results
482        self.report_multi_target_results(&aggregated_results)?;
483
484        Ok(())
485    }
486
487    /// Report results for multi-target execution
488    fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
489        // Print summary
490        TerminalReporter::print_multi_target_summary(results);
491
492        // Save aggregated summary if requested
493        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    /// Parse duration string (e.g., "30s", "5m", "1h") to seconds
543    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            // Try parsing as seconds without suffix
560            duration
561                .parse::<u64>()
562                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
563        }
564    }
565
566    /// Parse headers from command line format (Key:Value,Key2:Value2)
567    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    /// Resolve the effective base path for API endpoints
587    ///
588    /// Priority:
589    /// 1. CLI --base-path option (if provided, even if empty string)
590    /// 2. Base path extracted from OpenAPI spec's servers URL
591    /// 3. None (no base path)
592    ///
593    /// An empty string from CLI explicitly disables base path.
594    fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
595        // CLI option takes priority (including empty string to disable)
596        if let Some(cli_base_path) = &self.base_path {
597            if cli_base_path.is_empty() {
598                // Empty string explicitly means "no base path"
599                return None;
600            }
601            return Some(cli_base_path.clone());
602        }
603
604        // Fall back to spec's base path
605        parser.get_base_path()
606    }
607
608    /// Build mock server integration configuration
609    async fn build_mock_config(&self) -> MockIntegrationConfig {
610        // Check if target looks like a mock server
611        if MockServerDetector::looks_like_mock_server(&self.target) {
612            // Try to detect if it's actually a MockForge server
613            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    /// Build CRUD flow configuration
627    fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
628        if !self.crud_flow {
629            return None;
630        }
631
632        // If flow_config file is provided, load it
633        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        // Parse extract fields
646        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(), // Will be auto-detected
654            default_extract_fields: extract_fields,
655        })
656    }
657
658    /// Build data-driven testing configuration
659    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    /// Build invalid data testing configuration
682    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    /// Build security testing configuration
699    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    /// Build parallel execution configuration
734    fn build_parallel_config(&self) -> Option<ParallelConfig> {
735        let count = self.parallel_create?;
736
737        Some(ParallelConfig::new(count))
738    }
739
740    /// Load WAFBench payloads from the specified directory or pattern
741    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        // Print category breakdown
769        for (category, count) in &stats.by_category {
770            TerminalReporter::print_progress(&format!("  - {}: {} tests", category, count));
771        }
772
773        // Report any parse errors
774        for error in &stats.parse_errors {
775            TerminalReporter::print_warning(&format!("  Parse error: {}", error));
776        }
777
778        loader.to_security_payloads()
779    }
780
781    /// Generate enhanced k6 script with advanced features
782    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        // Add data-driven testing code
787        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        // Add invalid data generation code
795        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        // Add security testing code
812        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            // Combine built-in payloads with WAFBench payloads
819            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            // Add WAFBench payloads
826            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        // Add parallel execution code
852        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        // Insert additional code after the imports section
863        if !additional_code.is_empty() {
864            // Find the end of the import section
865            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    /// Execute specs sequentially with dependency ordering and value passing
877    async fn execute_sequential_specs(&self) -> Result<()> {
878        TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
879
880        // Load all specs (without merging)
881        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        // Load dependency config or auto-detect
906        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                // Auto-detect if config doesn't specify order
912                self.detect_and_sort_specs(&all_specs)?
913            } else {
914                // Use configured order
915                config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
916            }
917        } else {
918            // Auto-detect dependencies
919            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        // Execute each spec in order
932        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            // Find the spec in our loaded specs
946            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            // Execute this spec with any extracted values from previous specs
955            let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
956
957            // Merge extracted values for the next spec
958            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    /// Detect dependencies and return topologically sorted spec paths
978    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    /// Execute a single spec and extract values for dependent specs
1007    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        // For now, we execute in CRUD flow mode if enabled, otherwise standard mode
1016        if self.crud_flow {
1017            // Execute CRUD flow and extract values
1018            self.execute_crud_flow_with_extraction(&parser, spec_name).await
1019        } else {
1020            // Execute standard benchmark (no value extraction in non-CRUD mode)
1021            self.execute_standard_spec(&parser, spec_name).await?;
1022            Ok(ExtractedValues::new())
1023        }
1024    }
1025
1026    /// Execute CRUD flow with value extraction for sequential mode
1027    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        // Generate and execute the CRUD flow script
1047        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        // Load parameter overrides if provided (for body configurations)
1054        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        // Generate stages from scenario
1062        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        // Resolve base path (CLI option takes priority over spec's servers URL)
1068        let api_base_path = self.resolve_base_path(parser);
1069
1070        // Build headers JSON string for the template
1071        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        // Track all dynamic placeholders across all operations
1078        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                    // Parse operation to get method and path
1088                    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                        // k6 uses 'del' for DELETE
1097                        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                    // Prepend API base path if configured
1103                    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                    // POST, PUT, PATCH typically have bodies
1110                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1111
1112                    // Look up body from params file if available
1113                    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                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1123                    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        // Collect all placeholders from all steps
1144        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        // Get required imports and globals based on placeholders used
1169        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            // Add missing template fields
1182            "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        // Write and execute script
1199        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        // For now, return empty extracted values
1214        // TODO: Parse k6 output to extract actual values
1215        Ok(ExtractedValues::new())
1216    }
1217
1218    /// Execute standard (non-CRUD) spec benchmark
1219    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        // Generate request templates
1242        let templates: Vec<_> = operations
1243            .iter()
1244            .map(RequestGenerator::generate_template)
1245            .collect::<Result<Vec<_>>>()?;
1246
1247        // Parse headers
1248        let custom_headers = self.parse_headers()?;
1249
1250        // Resolve base path
1251        let base_path = self.resolve_base_path(parser);
1252
1253        // Generate k6 script
1254        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        // Write and execute script
1275        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    /// Execute CRUD flow testing mode
1292    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        // Generate CRUD flow script
1315        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        // Load parameter overrides if provided (for body configurations)
1322        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        // Generate stages from scenario
1336        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        // Resolve base path (CLI option takes priority over spec's servers URL)
1342        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        // Build headers JSON string for the template
1348        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        // Track all dynamic placeholders across all operations
1355        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1356
1357        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1358            // Sanitize flow name for use as JavaScript variable and k6 metric names
1359            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1360            serde_json::json!({
1361                "name": sanitized_name.clone(),  // Use sanitized name for variable names
1362                "display_name": f.name,          // Keep original for comments/display
1363                "base_path": f.base_path,
1364                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1365                    // Parse operation to get method and path
1366                    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                        // k6 uses 'del' for DELETE
1375                        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                    // Prepend API base path if configured
1381                    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                    // POST, PUT, PATCH typically have bodies
1388                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1389
1390                    // Look up body from params file if available (use raw_path for matching)
1391                    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                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1401                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1402                    // Note: all_placeholders is captured by the closure but we can't mutate it directly
1403                    // We'll collect placeholders separately below
1404
1405                    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        // Collect all placeholders from all steps
1424        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                                // Parse placeholder from debug string
1431                                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        // Get required imports and globals based on placeholders used
1450        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            // Add missing template fields
1463            "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        // Validate the generated CRUD flow script
1480        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        // Write and execute script
1496        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        // Execute k6
1515        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        // Test multiple specs
1646        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}