1use std::{fs, path::Path};
2
3use anyhow::{bail, Context, Result};
4use serde::{Deserialize, Serialize};
5use veritas_plugin_api::{FailureSeverity, RiskLevel};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8pub struct VeritasConfig {
9 pub budget_seconds: u64,
10 pub write_generated_tests: bool,
11 pub fail_on_generated_test_failure: bool,
12 pub fail_on_findings: bool,
13 pub planner: PlannerConfig,
14 pub policy: PolicyConfig,
15 pub plugins: PluginConfigs,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct PlannerConfig {
20 pub mode: PlannerMode,
21 pub command: Option<String>,
22 pub fail_on_error: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "snake_case")]
27pub enum PlannerMode {
28 Deterministic,
29 ExternalLlm,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct PluginConfigs {
34 pub rust: RustPluginConfig,
35 pub go: GoPluginConfig,
36 pub python: PythonPluginConfig,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub struct RustPluginConfig {
41 pub property_framework: String,
42 pub command_timeout_seconds: u64,
43 pub coverage_enabled: bool,
44 pub coverage_timeout_seconds: u64,
45 pub cargo_jobs: usize,
46 pub test_threads: usize,
47 pub systemd_scope: bool,
48 pub memory_max: Option<String>,
49 pub cpu_quota: Option<String>,
50 pub mutation: MutationConfig,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54pub struct GoPluginConfig {
55 pub fuzz_seconds: u64,
56 pub fuzz_existing: bool,
57 pub fuzz_concurrency: usize,
58 pub coverage_enabled: bool,
59 pub reverse_dependency_depth: usize,
60 pub max_fuzz_targets: usize,
61 pub command_timeout_seconds: u64,
62 pub max_packages: usize,
63 pub max_mutants: usize,
64 pub build_tags: Vec<String>,
65 pub mutation: MutationConfig,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct PythonPluginConfig {
70 pub command_timeout_seconds: u64,
71 pub coverage_enabled: bool,
72 pub mutation: MutationConfig,
73}
74
75#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
76pub struct MutationConfig {
77 pub enabled_domains: Vec<String>,
78 pub disabled_domains: Vec<String>,
79 pub enabled_operators: Vec<String>,
80 pub disabled_operators: Vec<String>,
81 pub include_paths: Vec<String>,
82 pub exclude_paths: Vec<String>,
83 pub include_symbols: Vec<String>,
84 pub exclude_symbols: Vec<String>,
85 pub include_target_ids: Vec<String>,
86 pub exclude_target_ids: Vec<String>,
87 pub include_mutant_ids: Vec<String>,
88 pub exclude_mutant_ids: Vec<String>,
89 pub report_filtered: bool,
90 pub dry_run: bool,
91 pub max_mutants: Option<usize>,
92 pub disable_test_selection: bool,
93 pub baseline_timing: bool,
94 pub workers: usize,
95 pub test_cpu: Option<usize>,
96 pub timeout_coefficient: u64,
97 pub timeout_min_seconds: Option<u64>,
98 pub timeout_max_seconds: Option<u64>,
99 pub shard_index: Option<usize>,
100 pub shard_count: Option<usize>,
101 pub output_statuses: Vec<String>,
102 pub isolation_exclude_paths: Vec<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct PolicyConfig {
107 pub fail_on_severity: FailureSeverity,
108 pub fail_on_languages: Vec<String>,
109 pub fail_on_artifact_kinds: Vec<String>,
110 pub fail_on_target_risks: Vec<RiskLevel>,
111 pub min_mutation_score: Option<u8>,
112 pub min_mutation_efficacy: Option<u8>,
113 pub min_mutant_coverage: Option<u8>,
114}
115
116#[derive(Debug, Clone, Deserialize)]
117struct ConfigFile {
118 veritas: Option<VeritasSection>,
119 planner: Option<PlannerSection>,
120 policy: Option<PolicySection>,
121 mutation: Option<MutationConfigPartial>,
122 plugins: Option<PluginSection>,
123}
124
125#[derive(Debug, Clone, Deserialize)]
126struct VeritasSection {
127 budget_seconds: Option<u64>,
128 write_generated_tests: Option<bool>,
129 fail_on_generated_test_failure: Option<bool>,
130 fail_on_findings: Option<bool>,
131}
132
133#[derive(Debug, Clone, Deserialize)]
134struct PlannerSection {
135 mode: Option<PlannerMode>,
136 command: Option<String>,
137 fail_on_error: Option<bool>,
138}
139
140#[derive(Debug, Clone, Deserialize)]
141struct PolicySection {
142 fail_on_severity: Option<FailureSeverity>,
143 fail_on_languages: Option<Vec<String>>,
144 fail_on_artifact_kinds: Option<Vec<String>>,
145 fail_on_target_risks: Option<Vec<RiskLevel>>,
146 min_mutation_score: Option<u8>,
147 min_mutation_efficacy: Option<u8>,
148 min_mutant_coverage: Option<u8>,
149}
150
151#[derive(Debug, Clone, Deserialize)]
152struct PluginSection {
153 rust: Option<RustPluginConfigPartial>,
154 go: Option<GoPluginConfigPartial>,
155 python: Option<PythonPluginConfigPartial>,
156}
157
158#[derive(Debug, Clone, Deserialize)]
159struct RustPluginConfigPartial {
160 property_framework: Option<String>,
161 command_timeout_seconds: Option<u64>,
162 coverage_enabled: Option<bool>,
163 coverage_timeout_seconds: Option<u64>,
164 cargo_jobs: Option<usize>,
165 test_threads: Option<usize>,
166 systemd_scope: Option<bool>,
167 memory_max: Option<String>,
168 cpu_quota: Option<String>,
169 mutation: Option<MutationConfigPartial>,
170}
171
172#[derive(Debug, Clone, Deserialize)]
173struct GoPluginConfigPartial {
174 fuzz_seconds: Option<u64>,
175 fuzz_existing: Option<bool>,
176 fuzz_concurrency: Option<usize>,
177 coverage_enabled: Option<bool>,
178 reverse_dependency_depth: Option<usize>,
179 max_fuzz_targets: Option<usize>,
180 command_timeout_seconds: Option<u64>,
181 max_packages: Option<usize>,
182 max_mutants: Option<usize>,
183 build_tags: Option<Vec<String>>,
184 mutation: Option<MutationConfigPartial>,
185}
186
187#[derive(Debug, Clone, Deserialize)]
188struct PythonPluginConfigPartial {
189 command_timeout_seconds: Option<u64>,
190 coverage_enabled: Option<bool>,
191 mutation: Option<MutationConfigPartial>,
192}
193
194#[derive(Debug, Clone, Deserialize)]
195struct MutationConfigPartial {
196 enabled_domains: Option<Vec<String>>,
197 disabled_domains: Option<Vec<String>>,
198 enabled_operators: Option<Vec<String>>,
199 disabled_operators: Option<Vec<String>>,
200 include_paths: Option<Vec<String>>,
201 exclude_paths: Option<Vec<String>>,
202 include_symbols: Option<Vec<String>>,
203 exclude_symbols: Option<Vec<String>>,
204 include_target_ids: Option<Vec<String>>,
205 exclude_target_ids: Option<Vec<String>>,
206 include_mutant_ids: Option<Vec<String>>,
207 exclude_mutant_ids: Option<Vec<String>>,
208 report_filtered: Option<bool>,
209 dry_run: Option<bool>,
210 max_mutants: Option<usize>,
211 disable_test_selection: Option<bool>,
212 baseline_timing: Option<bool>,
213 workers: Option<usize>,
214 test_cpu: Option<usize>,
215 timeout_coefficient: Option<u64>,
216 timeout_min_seconds: Option<u64>,
217 timeout_max_seconds: Option<u64>,
218 shard_index: Option<usize>,
219 shard_count: Option<usize>,
220 output_statuses: Option<Vec<String>>,
221 isolation_exclude_paths: Option<Vec<String>>,
222}
223
224impl Default for VeritasConfig {
225 fn default() -> Self {
226 Self {
227 budget_seconds: 120,
228 write_generated_tests: true,
229 fail_on_generated_test_failure: true,
230 fail_on_findings: false,
231 planner: PlannerConfig {
232 mode: PlannerMode::Deterministic,
233 command: None,
234 fail_on_error: false,
235 },
236 policy: PolicyConfig {
237 fail_on_severity: FailureSeverity::Error,
238 fail_on_languages: Vec::new(),
239 fail_on_artifact_kinds: Vec::new(),
240 fail_on_target_risks: Vec::new(),
241 min_mutation_score: None,
242 min_mutation_efficacy: None,
243 min_mutant_coverage: None,
244 },
245 plugins: PluginConfigs {
246 rust: RustPluginConfig {
247 property_framework: "proptest".to_string(),
248 command_timeout_seconds: 120,
249 coverage_enabled: false,
250 coverage_timeout_seconds: 120,
251 cargo_jobs: 1,
252 test_threads: 1,
253 systemd_scope: false,
254 memory_max: None,
255 cpu_quota: None,
256 mutation: MutationConfig::default(),
257 },
258 go: GoPluginConfig {
259 fuzz_seconds: 10,
260 fuzz_existing: true,
261 fuzz_concurrency: 2,
262 coverage_enabled: true,
263 reverse_dependency_depth: 1,
264 max_fuzz_targets: 20,
265 command_timeout_seconds: 120,
266 max_packages: 64,
267 max_mutants: 8,
268 build_tags: Vec::new(),
269 mutation: MutationConfig::default(),
270 },
271 python: PythonPluginConfig {
272 command_timeout_seconds: 120,
273 coverage_enabled: false,
274 mutation: MutationConfig::default(),
275 },
276 },
277 }
278 }
279}
280
281impl VeritasConfig {
282 pub fn load(root: &Path) -> Result<Self> {
283 let mut config = Self::default();
284 let Some(path) = config_path(root) else {
285 return Ok(config);
286 };
287
288 let contents = fs::read_to_string(&path)
289 .with_context(|| format!("failed to read config {}", path.display()))?;
290 let parsed: ConfigFile = toml::from_str(&contents)
291 .with_context(|| format!("failed to parse config {}", path.display()))?;
292
293 if let Some(veritas) = parsed.veritas {
294 if let Some(value) = veritas.budget_seconds {
295 config.budget_seconds = value;
296 }
297 if let Some(value) = veritas.write_generated_tests {
298 config.write_generated_tests = value;
299 }
300 if let Some(value) = veritas.fail_on_generated_test_failure {
301 config.fail_on_generated_test_failure = value;
302 }
303 if let Some(value) = veritas.fail_on_findings {
304 config.fail_on_findings = value;
305 }
306 }
307
308 if let Some(planner) = parsed.planner {
309 if let Some(value) = planner.mode {
310 config.planner.mode = value;
311 }
312 if let Some(value) = planner.command {
313 config.planner.command = Some(value);
314 }
315 if let Some(value) = planner.fail_on_error {
316 config.planner.fail_on_error = value;
317 }
318 }
319
320 if let Some(policy) = parsed.policy {
321 if let Some(value) = policy.fail_on_severity {
322 config.policy.fail_on_severity = value;
323 }
324 if let Some(value) = policy.fail_on_languages {
325 config.policy.fail_on_languages = value;
326 }
327 if let Some(value) = policy.fail_on_artifact_kinds {
328 config.policy.fail_on_artifact_kinds = value;
329 }
330 if let Some(value) = policy.fail_on_target_risks {
331 config.policy.fail_on_target_risks = value;
332 }
333 if let Some(value) = policy.min_mutation_score {
334 config.policy.min_mutation_score = Some(value.min(100));
335 }
336 if let Some(value) = policy.min_mutation_efficacy {
337 config.policy.min_mutation_efficacy = Some(value.min(100));
338 }
339 if let Some(value) = policy.min_mutant_coverage {
340 config.policy.min_mutant_coverage = Some(value.min(100));
341 }
342 }
343
344 if let Some(mutation) = parsed.mutation {
345 apply_mutation_config(&mut config.plugins.rust.mutation, &mutation);
346 apply_mutation_config(&mut config.plugins.go.mutation, &mutation);
347 apply_mutation_config(&mut config.plugins.python.mutation, &mutation);
348 }
349
350 if let Some(plugins) = parsed.plugins {
351 if let Some(rust) = plugins.rust {
352 if let Some(value) = rust.property_framework {
353 config.plugins.rust.property_framework = value;
354 }
355 if let Some(value) = rust.command_timeout_seconds {
356 config.plugins.rust.command_timeout_seconds = value;
357 }
358 if let Some(value) = rust.coverage_enabled {
359 config.plugins.rust.coverage_enabled = value;
360 }
361 if let Some(value) = rust.coverage_timeout_seconds {
362 config.plugins.rust.coverage_timeout_seconds = value;
363 }
364 if let Some(value) = rust.cargo_jobs {
365 config.plugins.rust.cargo_jobs = value;
366 }
367 if let Some(value) = rust.test_threads {
368 config.plugins.rust.test_threads = value;
369 }
370 if let Some(value) = rust.systemd_scope {
371 config.plugins.rust.systemd_scope = value;
372 }
373 if let Some(value) = rust.memory_max {
374 config.plugins.rust.memory_max = Some(value);
375 }
376 if let Some(value) = rust.cpu_quota {
377 config.plugins.rust.cpu_quota = Some(value);
378 }
379 if let Some(value) = rust.mutation {
380 apply_mutation_config(&mut config.plugins.rust.mutation, &value);
381 }
382 }
383 if let Some(go) = plugins.go {
384 if let Some(value) = go.fuzz_seconds {
385 config.plugins.go.fuzz_seconds = value;
386 }
387 if let Some(value) = go.fuzz_existing {
388 config.plugins.go.fuzz_existing = value;
389 }
390 if let Some(value) = go.fuzz_concurrency {
391 config.plugins.go.fuzz_concurrency = value.max(1);
392 }
393 if let Some(value) = go.coverage_enabled {
394 config.plugins.go.coverage_enabled = value;
395 }
396 if let Some(value) = go.reverse_dependency_depth {
397 config.plugins.go.reverse_dependency_depth = value;
398 }
399 if let Some(value) = go.max_fuzz_targets {
400 config.plugins.go.max_fuzz_targets = value;
401 }
402 if let Some(value) = go.command_timeout_seconds {
403 config.plugins.go.command_timeout_seconds = value;
404 }
405 if let Some(value) = go.max_packages {
406 config.plugins.go.max_packages = value;
407 }
408 if let Some(value) = go.max_mutants {
409 config.plugins.go.max_mutants = value;
410 }
411 if let Some(value) = go.build_tags {
412 config.plugins.go.build_tags = value;
413 }
414 if let Some(value) = go.mutation {
415 apply_mutation_config(&mut config.plugins.go.mutation, &value);
416 }
417 }
418 if let Some(python) = plugins.python {
419 if let Some(value) = python.command_timeout_seconds {
420 config.plugins.python.command_timeout_seconds = value;
421 }
422 if let Some(value) = python.coverage_enabled {
423 config.plugins.python.coverage_enabled = value;
424 }
425 if let Some(value) = python.mutation {
426 apply_mutation_config(&mut config.plugins.python.mutation, &value);
427 }
428 }
429 }
430
431 validate_shard_config("plugins.rust.mutation", &config.plugins.rust.mutation)?;
432 validate_shard_config("plugins.go.mutation", &config.plugins.go.mutation)?;
433 validate_shard_config("plugins.python.mutation", &config.plugins.python.mutation)?;
434
435 Ok(config)
436 }
437}
438
439fn validate_shard_config(label: &str, mutation: &MutationConfig) -> Result<()> {
440 let Some(shard_count) = mutation.shard_count else {
441 if mutation.shard_index.is_some() {
442 bail!("{label}.shard_index requires {label}.shard_count");
443 }
444 return Ok(());
445 };
446 if shard_count == 0 {
447 bail!("{label}.shard_count must be greater than zero");
448 }
449 if let Some(shard_index) = mutation.shard_index {
450 if shard_index >= shard_count {
451 bail!(
452 "{label}.shard_index {shard_index} must be less than {label}.shard_count {shard_count}"
453 );
454 }
455 }
456 Ok(())
457}
458
459fn apply_mutation_config(config: &mut MutationConfig, partial: &MutationConfigPartial) {
460 if let Some(value) = &partial.enabled_domains {
461 config.enabled_domains = value.clone();
462 }
463 if let Some(value) = &partial.disabled_domains {
464 config.disabled_domains = value.clone();
465 }
466 if let Some(value) = &partial.enabled_operators {
467 config.enabled_operators = value.clone();
468 }
469 if let Some(value) = &partial.disabled_operators {
470 config.disabled_operators = value.clone();
471 }
472 if let Some(value) = &partial.include_paths {
473 config.include_paths = value.clone();
474 }
475 if let Some(value) = &partial.exclude_paths {
476 config.exclude_paths = value.clone();
477 }
478 if let Some(value) = &partial.include_symbols {
479 config.include_symbols = value.clone();
480 }
481 if let Some(value) = &partial.exclude_symbols {
482 config.exclude_symbols = value.clone();
483 }
484 if let Some(value) = &partial.include_target_ids {
485 config.include_target_ids = value.clone();
486 }
487 if let Some(value) = &partial.exclude_target_ids {
488 config.exclude_target_ids = value.clone();
489 }
490 if let Some(value) = &partial.include_mutant_ids {
491 config.include_mutant_ids = value.clone();
492 }
493 if let Some(value) = &partial.exclude_mutant_ids {
494 config.exclude_mutant_ids = value.clone();
495 }
496 if let Some(value) = partial.report_filtered {
497 config.report_filtered = value;
498 }
499 if let Some(value) = partial.dry_run {
500 config.dry_run = value;
501 }
502 if let Some(value) = partial.max_mutants {
503 config.max_mutants = Some(value.max(1));
504 }
505 if let Some(value) = partial.disable_test_selection {
506 config.disable_test_selection = value;
507 }
508 if let Some(value) = partial.baseline_timing {
509 config.baseline_timing = value;
510 }
511 if let Some(value) = partial.workers {
512 config.workers = value;
513 }
514 if let Some(value) = partial.test_cpu {
515 config.test_cpu = Some(value.max(1));
516 }
517 if let Some(value) = partial.timeout_coefficient {
518 config.timeout_coefficient = value;
519 }
520 if let Some(value) = partial.timeout_min_seconds {
521 config.timeout_min_seconds = Some(value);
522 }
523 if let Some(value) = partial.timeout_max_seconds {
524 config.timeout_max_seconds = Some(value);
525 }
526 if let Some(value) = partial.shard_index {
527 config.shard_index = Some(value);
528 }
529 if let Some(value) = partial.shard_count {
530 config.shard_count = Some(value);
531 }
532 if let Some(value) = &partial.output_statuses {
533 config.output_statuses = value.clone();
534 }
535 if let Some(value) = &partial.isolation_exclude_paths {
536 config.isolation_exclude_paths = value.clone();
537 }
538}
539
540fn config_path(root: &Path) -> Option<std::path::PathBuf> {
541 let candidates = [root.join("veritas.toml"), root.join(".veritas.toml")];
542 candidates.into_iter().find(|candidate| candidate.exists())
543}
544
545#[cfg(test)]
546mod tests {
547 use std::{
548 fs,
549 path::{Path, PathBuf},
550 process,
551 time::{SystemTime, UNIX_EPOCH},
552 };
553
554 use veritas_plugin_api::{FailureSeverity, RiskLevel};
555
556 use super::VeritasConfig;
557
558 #[test]
559 fn loads_go_production_and_policy_config() {
560 let root = TempRoot::new();
561 fs::write(
562 root.path().join("veritas.toml"),
563 r#"
564[policy]
565fail_on_severity = "warning"
566fail_on_languages = ["go"]
567fail_on_artifact_kinds = ["mutation_check"]
568fail_on_target_risks = ["high"]
569min_mutation_score = 70
570min_mutation_efficacy = 75
571min_mutant_coverage = 80
572
573[mutation]
574disabled_operators = ["loop"]
575exclude_paths = ["vendor/", "_generated.go$"]
576include_target_ids = ["exact:rust:src/lib.rs:parse"]
577exclude_target_ids = ["regex:^rust:vendor/"]
578report_filtered = true
579dry_run = true
580max_mutants = 17
581disable_test_selection = true
582baseline_timing = true
583workers = 4
584test_cpu = 2
585timeout_coefficient = 3
586output_statuses = ["lived", "timed_out"]
587
588[plugins.go]
589fuzz_seconds = 3
590fuzz_existing = false
591fuzz_concurrency = 3
592coverage_enabled = false
593reverse_dependency_depth = 2
594max_fuzz_targets = 4
595command_timeout_seconds = 9
596max_packages = 12
597max_mutants = 5
598build_tags = ["integration", "sqlite"]
599
600[plugins.go.mutation]
601enabled_operators = ["arithmetic", "comparison"]
602dry_run = false
603
604[plugins.rust]
605property_framework = "proptest"
606command_timeout_seconds = 33
607coverage_enabled = true
608coverage_timeout_seconds = 44
609cargo_jobs = 2
610test_threads = 3
611systemd_scope = true
612memory_max = "4G"
613cpu_quota = "150%"
614"#,
615 )
616 .expect("write config");
617
618 let config = VeritasConfig::load(root.path()).expect("load config");
619
620 assert_eq!(config.policy.fail_on_severity, FailureSeverity::Warning);
621 assert_eq!(config.policy.fail_on_languages, vec!["go"]);
622 assert_eq!(config.policy.fail_on_artifact_kinds, vec!["mutation_check"]);
623 assert_eq!(config.policy.fail_on_target_risks, vec![RiskLevel::High]);
624 assert_eq!(config.policy.min_mutation_score, Some(70));
625 assert_eq!(config.policy.min_mutation_efficacy, Some(75));
626 assert_eq!(config.policy.min_mutant_coverage, Some(80));
627 assert_eq!(
628 config.plugins.rust.mutation.disabled_operators,
629 vec!["loop"]
630 );
631 assert!(config.plugins.rust.mutation.dry_run);
632 assert_eq!(config.plugins.rust.mutation.max_mutants, Some(17));
633 assert!(config.plugins.rust.mutation.disable_test_selection);
634 assert!(config.plugins.rust.mutation.baseline_timing);
635 assert_eq!(
636 config.plugins.rust.mutation.include_target_ids,
637 vec!["exact:rust:src/lib.rs:parse"]
638 );
639 assert_eq!(
640 config.plugins.rust.mutation.exclude_target_ids,
641 vec!["regex:^rust:vendor/"]
642 );
643 assert!(config.plugins.rust.mutation.report_filtered);
644 assert_eq!(config.plugins.rust.mutation.test_cpu, Some(2));
645 assert_eq!(config.plugins.rust.mutation.timeout_coefficient, 3);
646 assert_eq!(
647 config.plugins.go.mutation.enabled_operators,
648 vec!["arithmetic", "comparison"]
649 );
650 assert_eq!(
651 config.plugins.go.mutation.exclude_paths,
652 vec!["vendor/", "_generated.go$"]
653 );
654 assert_eq!(
655 config.plugins.python.mutation.exclude_paths,
656 vec!["vendor/", "_generated.go$"]
657 );
658 assert_eq!(config.plugins.python.mutation.max_mutants, Some(17));
659 assert!(!config.plugins.go.mutation.dry_run);
660 assert_eq!(config.plugins.go.fuzz_seconds, 3);
661 assert!(!config.plugins.go.fuzz_existing);
662 assert_eq!(config.plugins.go.fuzz_concurrency, 3);
663 assert!(!config.plugins.go.coverage_enabled);
664 assert_eq!(config.plugins.go.reverse_dependency_depth, 2);
665 assert_eq!(config.plugins.go.max_fuzz_targets, 4);
666 assert_eq!(config.plugins.go.command_timeout_seconds, 9);
667 assert_eq!(config.plugins.go.max_packages, 12);
668 assert_eq!(config.plugins.go.max_mutants, 5);
669 assert_eq!(config.plugins.go.build_tags, vec!["integration", "sqlite"]);
670 assert_eq!(config.plugins.rust.command_timeout_seconds, 33);
671 assert!(config.plugins.rust.coverage_enabled);
672 assert_eq!(config.plugins.rust.coverage_timeout_seconds, 44);
673 assert_eq!(config.plugins.rust.cargo_jobs, 2);
674 assert_eq!(config.plugins.rust.test_threads, 3);
675 assert!(config.plugins.rust.systemd_scope);
676 assert_eq!(config.plugins.rust.memory_max.as_deref(), Some("4G"));
677 assert_eq!(config.plugins.rust.cpu_quota.as_deref(), Some("150%"));
678 }
679
680 #[test]
681 fn rejects_misconfigured_mutation_shards() {
682 let root = TempRoot::new();
683 fs::write(
684 root.path().join("veritas.toml"),
685 r#"
686[plugins.rust.mutation]
687shard_index = 2
688shard_count = 2
689"#,
690 )
691 .expect("write config");
692
693 let error = VeritasConfig::load(root.path()).expect_err("invalid shard config");
694 assert!(error.to_string().contains(
695 "plugins.rust.mutation.shard_index 2 must be less than plugins.rust.mutation.shard_count 2"
696 ));
697 }
698
699 struct TempRoot {
700 path: PathBuf,
701 }
702
703 impl TempRoot {
704 fn new() -> Self {
705 let nanos = SystemTime::now()
706 .duration_since(UNIX_EPOCH)
707 .expect("system time should be after UNIX_EPOCH")
708 .as_nanos();
709 let path =
710 std::env::temp_dir().join(format!("veritas-config-test-{}-{nanos}", process::id()));
711 fs::create_dir_all(&path).expect("create temp root");
712 Self { path }
713 }
714
715 fn path(&self) -> &Path {
716 &self.path
717 }
718 }
719
720 impl Drop for TempRoot {
721 fn drop(&mut self) {
722 let _ = fs::remove_dir_all(&self.path);
723 }
724 }
725}