1use crate::error::{BenchError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13#[derive(Default)]
14pub enum DataDistribution {
15 #[default]
17 UniquePerVu,
18 UniquePerIteration,
20 Random,
22 Sequential,
24}
25
26impl std::str::FromStr for DataDistribution {
27 type Err = BenchError;
28
29 fn from_str(s: &str) -> Result<Self> {
30 match s.to_lowercase().replace('_', "-").as_str() {
31 "unique-per-vu" | "uniquepervu" => Ok(Self::UniquePerVu),
32 "unique-per-iteration" | "uniqueperiteration" => Ok(Self::UniquePerIteration),
33 "random" => Ok(Self::Random),
34 "sequential" => Ok(Self::Sequential),
35 _ => Err(BenchError::Other(format!(
36 "Invalid data distribution: '{}'. Valid options: unique-per-vu, unique-per-iteration, random, sequential",
37 s
38 ))),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct DataMapping {
46 pub column: String,
48 pub target: String,
50}
51
52impl DataMapping {
53 pub fn new(column: String, target: String) -> Self {
55 Self { column, target }
56 }
57
58 pub fn parse_mappings(s: &str) -> Result<Vec<Self>> {
61 if s.is_empty() {
62 return Ok(Vec::new());
63 }
64
65 s.split(',')
66 .map(|pair| {
67 let parts: Vec<&str> = pair.trim().splitn(2, ':').collect();
68 if parts.len() != 2 {
69 return Err(BenchError::Other(format!(
70 "Invalid mapping format: '{}'. Expected 'column:target'",
71 pair
72 )));
73 }
74 Ok(DataMapping::new(parts[0].trim().to_string(), parts[1].trim().to_string()))
75 })
76 .collect()
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DataDrivenConfig {
83 pub file_path: String,
85 #[serde(default)]
87 pub distribution: DataDistribution,
88 #[serde(default)]
90 pub mappings: Vec<DataMapping>,
91 #[serde(default = "default_true")]
93 pub csv_has_header: bool,
94 #[serde(default)]
96 pub per_uri_control: bool,
97 #[serde(default)]
99 pub per_uri_columns: PerUriColumns,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PerUriColumns {
105 #[serde(default = "default_method_column")]
107 pub method: String,
108 #[serde(default = "default_uri_column")]
110 pub uri: String,
111 #[serde(default = "default_body_column")]
113 pub body: String,
114 #[serde(default = "default_query_params_column")]
116 pub query_params: String,
117 #[serde(default = "default_headers_column")]
119 pub headers: String,
120 #[serde(default = "default_attack_type_column")]
122 pub attack_type: String,
123 #[serde(default = "default_expected_status_column")]
125 pub expected_status: String,
126}
127
128fn default_method_column() -> String {
129 "method".to_string()
130}
131
132fn default_uri_column() -> String {
133 "uri".to_string()
134}
135
136fn default_body_column() -> String {
137 "body".to_string()
138}
139
140fn default_query_params_column() -> String {
141 "query_params".to_string()
142}
143
144fn default_headers_column() -> String {
145 "headers".to_string()
146}
147
148fn default_attack_type_column() -> String {
149 "attack_type".to_string()
150}
151
152fn default_expected_status_column() -> String {
153 "expected_status".to_string()
154}
155
156impl Default for PerUriColumns {
157 fn default() -> Self {
158 Self {
159 method: default_method_column(),
160 uri: default_uri_column(),
161 body: default_body_column(),
162 query_params: default_query_params_column(),
163 headers: default_headers_column(),
164 attack_type: default_attack_type_column(),
165 expected_status: default_expected_status_column(),
166 }
167 }
168}
169
170fn default_true() -> bool {
171 true
172}
173
174impl DataDrivenConfig {
175 pub fn new(file_path: String) -> Self {
177 Self {
178 file_path,
179 distribution: DataDistribution::default(),
180 mappings: Vec::new(),
181 csv_has_header: true,
182 per_uri_control: false,
183 per_uri_columns: PerUriColumns::default(),
184 }
185 }
186
187 pub fn with_distribution(mut self, distribution: DataDistribution) -> Self {
189 self.distribution = distribution;
190 self
191 }
192
193 pub fn with_mappings(mut self, mappings: Vec<DataMapping>) -> Self {
195 self.mappings = mappings;
196 self
197 }
198
199 pub fn with_per_uri_control(mut self, enabled: bool) -> Self {
201 self.per_uri_control = enabled;
202 self
203 }
204
205 pub fn with_per_uri_columns(mut self, columns: PerUriColumns) -> Self {
207 self.per_uri_columns = columns;
208 self
209 }
210
211 pub fn file_type(&self) -> DataFileType {
213 if self.file_path.ends_with(".csv") {
214 DataFileType::Csv
215 } else if self.file_path.ends_with(".json") {
216 DataFileType::Json
217 } else {
218 DataFileType::Csv
220 }
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum DataFileType {
227 Csv,
228 Json,
229}
230
231pub struct DataDrivenGenerator;
233
234impl DataDrivenGenerator {
235 pub fn generate_imports(file_type: DataFileType) -> String {
237 let mut imports = String::new();
238
239 imports.push_str("import { SharedArray } from 'k6/data';\n");
240
241 if file_type == DataFileType::Csv {
242 imports.push_str(
243 "import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';\n",
244 );
245 }
246
247 imports
248 }
249
250 pub fn generate_data_loading(config: &DataDrivenConfig) -> String {
252 let mut code = String::new();
253
254 code.push_str("// Load test data using SharedArray for memory efficiency\n");
255 code.push_str("const testData = new SharedArray('test data', function() {\n");
256
257 match config.file_type() {
258 DataFileType::Csv => {
259 code.push_str(&format!(" const csvData = open('{}');\n", config.file_path));
260 if config.csv_has_header {
261 code.push_str(" return papaparse.parse(csvData, { header: true }).data;\n");
262 } else {
263 code.push_str(" return papaparse.parse(csvData, { header: false }).data;\n");
264 }
265 }
266 DataFileType::Json => {
267 code.push_str(&format!(" return JSON.parse(open('{}'));\n", config.file_path));
268 }
269 }
270
271 code.push_str("});\n\n");
272
273 code
274 }
275
276 pub fn generate_row_selection(distribution: DataDistribution) -> String {
278 match distribution {
279 DataDistribution::UniquePerVu => {
280 "// Unique row per VU (wraps if more VUs than data rows)\n\
281 const rowIndex = (__VU - 1) % testData.length;\n\
282 const row = testData[rowIndex];\n"
283 .to_string()
284 }
285 DataDistribution::UniquePerIteration => {
286 "// Unique row per iteration (cycles through data)\n\
287 const rowIndex = __ITER % testData.length;\n\
288 const row = testData[rowIndex];\n"
289 .to_string()
290 }
291 DataDistribution::Random => "// Random row selection\n\
292 const rowIndex = Math.floor(Math.random() * testData.length);\n\
293 const row = testData[rowIndex];\n"
294 .to_string(),
295 DataDistribution::Sequential => {
296 "// Sequential iteration (same for all VUs, based on iteration)\n\
297 const rowIndex = __ITER % testData.length;\n\
298 const row = testData[rowIndex];\n"
299 .to_string()
300 }
301 }
302 }
303
304 pub fn generate_apply_mappings(mappings: &[DataMapping]) -> String {
306 if mappings.is_empty() {
307 return "// No explicit mappings - row data available as 'row' object\n".to_string();
308 }
309
310 let mut code = String::new();
311 code.push_str("// Apply data mappings\n");
312
313 for mapping in mappings {
314 let target_parts: Vec<&str> = mapping.target.splitn(2, '.').collect();
315 if target_parts.len() == 2 {
316 let target_type = target_parts[0];
317 let field_name = target_parts[1];
318
319 match target_type {
320 "body" => {
321 code.push_str(&format!(
322 "requestBody['{}'] = row['{}'];\n",
323 field_name, mapping.column
324 ));
325 }
326 "path" => {
327 code.push_str(&format!(
328 "pathParams['{}'] = row['{}'];\n",
329 field_name, mapping.column
330 ));
331 }
332 "query" => {
333 code.push_str(&format!(
334 "queryParams['{}'] = row['{}'];\n",
335 field_name, mapping.column
336 ));
337 }
338 "header" => {
339 code.push_str(&format!(
340 "requestHeaders['{}'] = row['{}'];\n",
341 field_name, mapping.column
342 ));
343 }
344 _ => {
345 code.push_str(&format!(
346 "// Unknown target type '{}' for column '{}'\n",
347 target_type, mapping.column
348 ));
349 }
350 }
351 } else {
352 code.push_str(&format!(
354 "requestBody['{}'] = row['{}'];\n",
355 mapping.target, mapping.column
356 ));
357 }
358 }
359
360 code
361 }
362
363 pub fn generate_setup(config: &DataDrivenConfig) -> String {
365 let mut code = String::new();
366
367 code.push_str(&Self::generate_imports(config.file_type()));
368 code.push('\n');
369 code.push_str(&Self::generate_data_loading(config));
370
371 code
372 }
373
374 pub fn generate_iteration_code(config: &DataDrivenConfig) -> String {
376 let mut code = String::new();
377
378 code.push_str(&Self::generate_row_selection(config.distribution));
379 code.push('\n');
380
381 if config.per_uri_control {
382 code.push_str(&Self::generate_per_uri_control_code(config));
383 } else {
384 code.push_str(&Self::generate_apply_mappings(&config.mappings));
385 }
386
387 code
388 }
389
390 pub fn generate_per_uri_control_code(config: &DataDrivenConfig) -> String {
410 let cols = &config.per_uri_columns;
411 let mut code = String::new();
412
413 code.push_str("// Per-URI control mode: each row specifies method, URI, body, etc.\n");
414 code.push_str(&format!(
415 "const method = (row['{}'] || 'GET').toUpperCase();\n",
416 cols.method
417 ));
418 code.push_str(&format!("const uri = row['{}'] || '/';\n", cols.uri));
419 code.push_str(&format!("const bodyStr = row['{}'] || '';\n", cols.body));
420 code.push_str(&format!("const queryParamsStr = row['{}'] || '';\n", cols.query_params));
421 code.push_str(&format!("const extraHeadersStr = row['{}'] || '';\n", cols.headers));
422 code.push_str(&format!("const attackType = row['{}'] || '';\n", cols.attack_type));
423 code.push_str(&format!(
424 "const expectedStatus = row['{}'] ? parseInt(row['{}']) : null;\n",
425 cols.expected_status, cols.expected_status
426 ));
427
428 code.push_str("\n// Parse body if present\n");
429 code.push_str("let requestBody = null;\n");
430 code.push_str("if (bodyStr && bodyStr.trim()) {\n");
431 code.push_str(" try {\n");
432 code.push_str(" requestBody = JSON.parse(bodyStr);\n");
433 code.push_str(" } catch (e) {\n");
434 code.push_str(" // If not valid JSON, use as string (for form data or plain text)\n");
435 code.push_str(" requestBody = bodyStr;\n");
436 code.push_str(" }\n");
437 code.push_str("}\n\n");
438
439 code.push_str("// Parse query parameters if present\n");
440 code.push_str("let queryString = '';\n");
441 code.push_str("if (queryParamsStr && queryParamsStr.trim()) {\n");
442 code.push_str(" try {\n");
443 code.push_str(" // Try parsing as JSON first\n");
444 code.push_str(" const qp = JSON.parse(queryParamsStr);\n");
445 code.push_str(" queryString = '?' + Object.entries(qp).map(([k,v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');\n");
446 code.push_str(" } catch (e) {\n");
447 code.push_str(" // Assume it's already in key=value&key=value format\n");
448 code.push_str(" queryString = queryParamsStr.startsWith('?') ? queryParamsStr : '?' + queryParamsStr;\n");
449 code.push_str(" }\n");
450 code.push_str("}\n\n");
451
452 code.push_str("// Parse extra headers if present\n");
453 code.push_str("let extraHeaders = {};\n");
454 code.push_str("if (extraHeadersStr && extraHeadersStr.trim()) {\n");
455 code.push_str(" try {\n");
456 code.push_str(" extraHeaders = JSON.parse(extraHeadersStr);\n");
457 code.push_str(" } catch (e) {\n");
458 code.push_str(" console.warn('Failed to parse extra headers:', e.message);\n");
459 code.push_str(" }\n");
460 code.push_str("}\n\n");
461
462 code.push_str("// Merge headers\n");
463 code.push_str("const mergedHeaders = Object.assign({}, headers, extraHeaders);\n\n");
464
465 code.push_str("// Build full URL with query string\n");
466 code.push_str("const fullUrl = `${BASE_URL}${uri}${queryString}`;\n\n");
467
468 code.push_str("// Make the request based on method\n");
469 code.push_str("let res;\n");
470 code.push_str("switch (method) {\n");
471 code.push_str(" case 'GET':\n");
472 code.push_str(" res = http.get(fullUrl, { headers: mergedHeaders });\n");
473 code.push_str(" break;\n");
474 code.push_str(" case 'POST':\n");
475 code.push_str(" res = http.post(fullUrl, requestBody ? JSON.stringify(requestBody) : null, { headers: mergedHeaders });\n");
476 code.push_str(" break;\n");
477 code.push_str(" case 'PUT':\n");
478 code.push_str(" res = http.put(fullUrl, requestBody ? JSON.stringify(requestBody) : null, { headers: mergedHeaders });\n");
479 code.push_str(" break;\n");
480 code.push_str(" case 'PATCH':\n");
481 code.push_str(" res = http.patch(fullUrl, requestBody ? JSON.stringify(requestBody) : null, { headers: mergedHeaders });\n");
482 code.push_str(" break;\n");
483 code.push_str(" case 'DELETE':\n");
484 code.push_str(" res = http.del(fullUrl, requestBody ? JSON.stringify(requestBody) : null, { headers: mergedHeaders });\n");
485 code.push_str(" break;\n");
486 code.push_str(" default:\n");
487 code.push_str(" console.error(`Unsupported HTTP method: ${method}`);\n");
488 code.push_str(" return;\n");
489 code.push_str("}\n\n");
490
491 code.push_str("// Validate response status if expected status is specified\n");
492 code.push_str("if (expectedStatus !== null) {\n");
493 code.push_str(" check(res, {\n");
494 code.push_str(" [`${method} ${uri}: status is ${expectedStatus}`]: (r) => r.status === expectedStatus,\n");
495 code.push_str(" });\n");
496 code.push_str("} else {\n");
497 code.push_str(" check(res, {\n");
498 code.push_str(
499 " [`${method} ${uri}: status is 2xx`]: (r) => r.status >= 200 && r.status < 300,\n",
500 );
501 code.push_str(" });\n");
502 code.push_str("}\n\n");
503
504 code.push_str("// Record metrics with operation name\n");
505 code.push_str(
506 "const opName = `${method.toLowerCase()}_${uri.replace(/[^a-zA-Z0-9]/g, '_')}`;\n",
507 );
508 code.push_str("if (typeof perUriLatency !== 'undefined' && perUriLatency[opName]) {\n");
509 code.push_str(" perUriLatency[opName].add(res.timings.duration);\n");
510 code.push_str("}\n\n");
511
512 code.push_str("// Log attack type if security testing\n");
513 code.push_str("if (attackType) {\n");
514 code.push_str(
515 " console.log(`[Security Test] ${attackType}: ${method} ${uri} => ${res.status}`);\n",
516 );
517 code.push_str("}\n");
518
519 code
520 }
521
522 pub fn generate_per_uri_metrics(operations: &[(String, String)]) -> String {
524 let mut code = String::new();
525
526 code.push_str("// Per-URI latency metrics\n");
527 code.push_str("const perUriLatency = {\n");
528
529 for (method, uri) in operations {
530 let op_name = format!(
531 "{}_{}",
532 method.to_lowercase(),
533 uri.replace(|c: char| !c.is_alphanumeric(), "_")
534 );
535 code.push_str(&format!(" '{}': new Trend('{}_latency'),\n", op_name, op_name));
536 }
537
538 code.push_str("};\n\n");
539
540 code
541 }
542
543 pub fn generate_per_uri_control_script(
545 config: &DataDrivenConfig,
546 target_url: &str,
547 custom_headers: &std::collections::HashMap<String, String>,
548 skip_tls_verify: bool,
549 ) -> String {
550 let mut script = String::new();
551
552 script.push_str("import http from 'k6/http';\n");
554 script.push_str("import { check, sleep } from 'k6';\n");
555 script.push_str("import { Trend, Rate } from 'k6/metrics';\n");
556 script.push_str(&Self::generate_imports(config.file_type()));
557 script.push('\n');
558
559 script.push_str(&Self::generate_data_loading(config));
561
562 script.push_str("// Custom metrics\n");
564 script.push_str("const requestLatency = new Trend('request_latency');\n");
565 script.push_str("const requestErrors = new Rate('request_errors');\n\n");
566
567 script.push_str("export const options = {\n");
569 if skip_tls_verify {
570 script.push_str(" insecureSkipTLSVerify: true,\n");
571 }
572 script.push_str(" scenarios: {\n");
573 script.push_str(" per_uri_control: {\n");
574 script.push_str(" executor: 'shared-iterations',\n");
575 script.push_str(" vus: 10,\n");
576 script.push_str(" iterations: testData.length,\n");
577 script.push_str(" maxDuration: '5m',\n");
578 script.push_str(" },\n");
579 script.push_str(" },\n");
580 script.push_str(" thresholds: {\n");
581 script.push_str(" 'http_req_duration': ['p(95)<500'],\n");
582 script.push_str(" 'http_req_failed': ['rate<0.1'],\n");
583 script.push_str(" },\n");
584 script.push_str("};\n\n");
585
586 script.push_str(&format!("const BASE_URL = '{}';\n\n", target_url));
588 let headers_json =
589 serde_json::to_string(custom_headers).unwrap_or_else(|_| "{}".to_string());
590 script.push_str(&format!("const headers = {};\n\n", headers_json));
591
592 script.push_str("export default function () {\n");
594 script.push_str(" ");
595 script.push_str(
596 &Self::generate_iteration_code(config).lines().collect::<Vec<_>>().join("\n "),
597 );
598 script.push_str("\n\n // Record overall latency\n");
599 script.push_str(" if (res) {\n");
600 script.push_str(" requestLatency.add(res.timings.duration);\n");
601 script.push_str(" requestErrors.add(res.status >= 400);\n");
602 script.push_str(" }\n\n");
603 script.push_str(" sleep(0.1);\n");
604 script.push_str("}\n");
605
606 script
607 }
608}
609
610pub fn validate_data_file(path: &Path) -> Result<DataFileInfo> {
612 if !path.exists() {
613 return Err(BenchError::Other(format!("Data file not found: {}", path.display())));
614 }
615
616 let content = std::fs::read_to_string(path)
617 .map_err(|e| BenchError::Other(format!("Failed to read data file: {}", e)))?;
618
619 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
620
621 match extension {
622 "csv" => validate_csv(&content),
623 "json" => validate_json(&content),
624 _ => Err(BenchError::Other(format!(
625 "Unsupported data file format: .{}. Use .csv or .json",
626 extension
627 ))),
628 }
629}
630
631#[derive(Debug, Clone)]
633pub struct DataFileInfo {
634 pub row_count: usize,
636 pub columns: Vec<String>,
638 pub file_type: DataFileType,
640}
641
642fn validate_csv(content: &str) -> Result<DataFileInfo> {
643 let lines: Vec<&str> = content.lines().collect();
644 if lines.is_empty() {
645 return Err(BenchError::Other("CSV file is empty".to_string()));
646 }
647
648 let header = lines[0];
650 let columns: Vec<String> = header.split(',').map(|s| s.trim().to_string()).collect();
651 let row_count = lines.len() - 1; Ok(DataFileInfo {
654 row_count,
655 columns,
656 file_type: DataFileType::Csv,
657 })
658}
659
660fn validate_json(content: &str) -> Result<DataFileInfo> {
661 let value: serde_json::Value = serde_json::from_str(content)
662 .map_err(|e| BenchError::Other(format!("Invalid JSON: {}", e)))?;
663
664 match value {
665 serde_json::Value::Array(arr) => {
666 if arr.is_empty() {
667 return Err(BenchError::Other("JSON array is empty".to_string()));
668 }
669
670 let columns = if let Some(serde_json::Value::Object(obj)) = arr.first() {
672 obj.keys().cloned().collect()
673 } else {
674 Vec::new()
675 };
676
677 Ok(DataFileInfo {
678 row_count: arr.len(),
679 columns,
680 file_type: DataFileType::Json,
681 })
682 }
683 _ => Err(BenchError::Other("JSON data must be an array of objects".to_string())),
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use std::str::FromStr;
691
692 #[test]
693 fn test_data_distribution_default() {
694 assert_eq!(DataDistribution::default(), DataDistribution::UniquePerVu);
695 }
696
697 #[test]
698 fn test_data_distribution_from_str() {
699 assert_eq!(
700 DataDistribution::from_str("unique-per-vu").unwrap(),
701 DataDistribution::UniquePerVu
702 );
703 assert_eq!(
704 DataDistribution::from_str("unique-per-iteration").unwrap(),
705 DataDistribution::UniquePerIteration
706 );
707 assert_eq!(DataDistribution::from_str("random").unwrap(), DataDistribution::Random);
708 assert_eq!(DataDistribution::from_str("sequential").unwrap(), DataDistribution::Sequential);
709 }
710
711 #[test]
712 fn test_data_distribution_from_str_variants() {
713 assert_eq!(
715 DataDistribution::from_str("unique_per_vu").unwrap(),
716 DataDistribution::UniquePerVu
717 );
718
719 assert_eq!(
721 DataDistribution::from_str("uniquePerVu").unwrap(),
722 DataDistribution::UniquePerVu
723 );
724 }
725
726 #[test]
727 fn test_data_distribution_from_str_invalid() {
728 assert!(DataDistribution::from_str("invalid").is_err());
729 }
730
731 #[test]
732 fn test_data_mapping_parse() {
733 let mappings = DataMapping::parse_mappings("name:body.username,id:path.userId").unwrap();
734 assert_eq!(mappings.len(), 2);
735 assert_eq!(mappings[0].column, "name");
736 assert_eq!(mappings[0].target, "body.username");
737 assert_eq!(mappings[1].column, "id");
738 assert_eq!(mappings[1].target, "path.userId");
739 }
740
741 #[test]
742 fn test_data_mapping_parse_empty() {
743 let mappings = DataMapping::parse_mappings("").unwrap();
744 assert!(mappings.is_empty());
745 }
746
747 #[test]
748 fn test_data_mapping_parse_invalid() {
749 assert!(DataMapping::parse_mappings("invalid").is_err());
750 }
751
752 #[test]
753 fn test_data_driven_config_file_type() {
754 let csv_config = DataDrivenConfig::new("data.csv".to_string());
755 assert_eq!(csv_config.file_type(), DataFileType::Csv);
756
757 let json_config = DataDrivenConfig::new("data.json".to_string());
758 assert_eq!(json_config.file_type(), DataFileType::Json);
759
760 let unknown_config = DataDrivenConfig::new("data.txt".to_string());
761 assert_eq!(unknown_config.file_type(), DataFileType::Csv); }
763
764 #[test]
765 fn test_generate_imports_csv() {
766 let imports = DataDrivenGenerator::generate_imports(DataFileType::Csv);
767 assert!(imports.contains("SharedArray"));
768 assert!(imports.contains("papaparse"));
769 }
770
771 #[test]
772 fn test_generate_imports_json() {
773 let imports = DataDrivenGenerator::generate_imports(DataFileType::Json);
774 assert!(imports.contains("SharedArray"));
775 assert!(!imports.contains("papaparse"));
776 }
777
778 #[test]
779 fn test_generate_data_loading_csv() {
780 let config = DataDrivenConfig::new("test.csv".to_string());
781 let code = DataDrivenGenerator::generate_data_loading(&config);
782
783 assert!(code.contains("SharedArray"));
784 assert!(code.contains("open('test.csv')"));
785 assert!(code.contains("papaparse.parse"));
786 assert!(code.contains("header: true"));
787 }
788
789 #[test]
790 fn test_generate_data_loading_json() {
791 let config = DataDrivenConfig::new("test.json".to_string());
792 let code = DataDrivenGenerator::generate_data_loading(&config);
793
794 assert!(code.contains("SharedArray"));
795 assert!(code.contains("open('test.json')"));
796 assert!(code.contains("JSON.parse"));
797 }
798
799 #[test]
800 fn test_generate_row_selection_unique_per_vu() {
801 let code = DataDrivenGenerator::generate_row_selection(DataDistribution::UniquePerVu);
802 assert!(code.contains("__VU - 1"));
803 assert!(code.contains("testData.length"));
804 }
805
806 #[test]
807 fn test_generate_row_selection_unique_per_iteration() {
808 let code =
809 DataDrivenGenerator::generate_row_selection(DataDistribution::UniquePerIteration);
810 assert!(code.contains("__ITER"));
811 assert!(code.contains("testData.length"));
812 }
813
814 #[test]
815 fn test_generate_row_selection_random() {
816 let code = DataDrivenGenerator::generate_row_selection(DataDistribution::Random);
817 assert!(code.contains("Math.random()"));
818 assert!(code.contains("testData.length"));
819 }
820
821 #[test]
822 fn test_generate_apply_mappings() {
823 let mappings = vec![
824 DataMapping::new("name".to_string(), "body.username".to_string()),
825 DataMapping::new("id".to_string(), "path.userId".to_string()),
826 DataMapping::new("token".to_string(), "header.Authorization".to_string()),
827 ];
828
829 let code = DataDrivenGenerator::generate_apply_mappings(&mappings);
830
831 assert!(code.contains("requestBody['username'] = row['name']"));
832 assert!(code.contains("pathParams['userId'] = row['id']"));
833 assert!(code.contains("requestHeaders['Authorization'] = row['token']"));
834 }
835
836 #[test]
837 fn test_generate_apply_mappings_empty() {
838 let code = DataDrivenGenerator::generate_apply_mappings(&[]);
839 assert!(code.contains("No explicit mappings"));
840 }
841
842 #[test]
843 fn test_validate_csv() {
844 let content = "name,email,age\nAlice,alice@test.com,30\nBob,bob@test.com,25";
845 let info = validate_csv(content).unwrap();
846
847 assert_eq!(info.row_count, 2);
848 assert_eq!(info.columns, vec!["name", "email", "age"]);
849 assert_eq!(info.file_type, DataFileType::Csv);
850 }
851
852 #[test]
853 fn test_validate_csv_empty() {
854 let content = "";
855 assert!(validate_csv(content).is_err());
856 }
857
858 #[test]
859 fn test_validate_json() {
860 let content = r#"[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]"#;
861 let info = validate_json(content).unwrap();
862
863 assert_eq!(info.row_count, 2);
864 assert!(info.columns.contains(&"name".to_string()));
865 assert!(info.columns.contains(&"age".to_string()));
866 assert_eq!(info.file_type, DataFileType::Json);
867 }
868
869 #[test]
870 fn test_validate_json_empty_array() {
871 let content = "[]";
872 assert!(validate_json(content).is_err());
873 }
874
875 #[test]
876 fn test_validate_json_not_array() {
877 let content = r#"{"name": "Alice"}"#;
878 assert!(validate_json(content).is_err());
879 }
880
881 #[test]
882 fn test_generate_setup() {
883 let config = DataDrivenConfig::new("users.csv".to_string())
884 .with_distribution(DataDistribution::Random);
885
886 let code = DataDrivenGenerator::generate_setup(&config);
887
888 assert!(code.contains("SharedArray"));
889 assert!(code.contains("papaparse"));
890 assert!(code.contains("users.csv"));
891 }
892
893 #[test]
894 fn test_generate_iteration_code() {
895 let config = DataDrivenConfig::new("data.csv".to_string())
896 .with_distribution(DataDistribution::UniquePerVu)
897 .with_mappings(vec![DataMapping::new(
898 "email".to_string(),
899 "body.email".to_string(),
900 )]);
901
902 let code = DataDrivenGenerator::generate_iteration_code(&config);
903
904 assert!(code.contains("__VU - 1"));
905 assert!(code.contains("requestBody['email'] = row['email']"));
906 }
907
908 #[test]
909 fn test_per_uri_columns_default() {
910 let cols = PerUriColumns::default();
911 assert_eq!(cols.method, "method");
912 assert_eq!(cols.uri, "uri");
913 assert_eq!(cols.body, "body");
914 assert_eq!(cols.query_params, "query_params");
915 assert_eq!(cols.headers, "headers");
916 assert_eq!(cols.attack_type, "attack_type");
917 assert_eq!(cols.expected_status, "expected_status");
918 }
919
920 #[test]
921 fn test_data_driven_config_per_uri_control() {
922 let config = DataDrivenConfig::new("test.csv".to_string()).with_per_uri_control(true);
923
924 assert!(config.per_uri_control);
925 }
926
927 #[test]
928 fn test_generate_per_uri_control_code() {
929 let config = DataDrivenConfig::new("test.csv".to_string()).with_per_uri_control(true);
930
931 let code = DataDrivenGenerator::generate_per_uri_control_code(&config);
932
933 assert!(code.contains("const method = (row['method']"));
935 assert!(code.contains("const uri = row['uri']"));
936 assert!(code.contains("const bodyStr = row['body']"));
937 assert!(code.contains("const queryParamsStr = row['query_params']"));
938 assert!(code.contains("const attackType = row['attack_type']"));
939 assert!(code.contains("const expectedStatus = row['expected_status']"));
940
941 assert!(code.contains("switch (method)"));
943 assert!(code.contains("case 'GET':"));
944 assert!(code.contains("case 'POST':"));
945 assert!(code.contains("case 'PUT':"));
946 assert!(code.contains("case 'PATCH':"));
947 assert!(code.contains("case 'DELETE':"));
948
949 assert!(code.contains("if (expectedStatus !== null)"));
951 assert!(code.contains("check(res"));
952 }
953
954 #[test]
955 fn test_generate_iteration_code_with_per_uri_control() {
956 let config = DataDrivenConfig::new("test.csv".to_string())
957 .with_distribution(DataDistribution::UniquePerIteration)
958 .with_per_uri_control(true);
959
960 let code = DataDrivenGenerator::generate_iteration_code(&config);
961
962 assert!(code.contains("Per-URI control mode"));
964 assert!(code.contains("switch (method)"));
965 assert!(!code.contains("requestBody['"));
966 }
967
968 #[test]
969 fn test_generate_per_uri_metrics() {
970 let operations = vec![
971 ("GET".to_string(), "/users".to_string()),
972 ("POST".to_string(), "/users".to_string()),
973 ("GET".to_string(), "/users/{id}".to_string()),
974 ];
975
976 let code = DataDrivenGenerator::generate_per_uri_metrics(&operations);
977
978 assert!(code.contains("get__users"));
979 assert!(code.contains("post__users"));
980 assert!(code.contains("get__users__id_"));
981 assert!(code.contains("new Trend"));
982 }
983
984 #[test]
985 fn test_generate_per_uri_control_script() {
986 let config = DataDrivenConfig::new("requests.csv".to_string())
987 .with_per_uri_control(true)
988 .with_distribution(DataDistribution::Sequential);
989
990 let headers = std::collections::HashMap::from([(
991 "Content-Type".to_string(),
992 "application/json".to_string(),
993 )]);
994
995 let script = DataDrivenGenerator::generate_per_uri_control_script(
996 &config,
997 "https://api.example.com",
998 &headers,
999 true,
1000 );
1001
1002 assert!(script.contains("import http from 'k6/http'"));
1004 assert!(script.contains("import { check, sleep }"));
1005 assert!(script.contains("SharedArray"));
1006
1007 assert!(script.contains("requests.csv"));
1009
1010 assert!(script.contains("insecureSkipTLSVerify: true"));
1012 assert!(script.contains("per_uri_control:"));
1013
1014 assert!(script.contains("const BASE_URL = 'https://api.example.com'"));
1016
1017 assert!(script.contains("Content-Type"));
1019
1020 assert!(script.contains("export default function"));
1022 assert!(script.contains("switch (method)"));
1023 }
1024}