use std::{fs, path::Path};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use veritas_plugin_api::{FailureSeverity, RiskLevel};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VeritasConfig {
pub budget_seconds: u64,
pub write_generated_tests: bool,
pub fail_on_generated_test_failure: bool,
pub fail_on_findings: bool,
pub planner: PlannerConfig,
pub policy: PolicyConfig,
pub plugins: PluginConfigs,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlannerConfig {
pub mode: PlannerMode,
pub command: Option<String>,
pub fail_on_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PlannerMode {
Deterministic,
ExternalLlm,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PluginConfigs {
pub rust: RustPluginConfig,
pub go: GoPluginConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RustPluginConfig {
pub property_framework: String,
pub command_timeout_seconds: u64,
pub coverage_enabled: bool,
pub coverage_timeout_seconds: u64,
pub cargo_jobs: usize,
pub test_threads: usize,
pub systemd_scope: bool,
pub memory_max: Option<String>,
pub cpu_quota: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GoPluginConfig {
pub fuzz_seconds: u64,
pub fuzz_existing: bool,
pub coverage_enabled: bool,
pub reverse_dependency_depth: usize,
pub max_fuzz_targets: usize,
pub command_timeout_seconds: u64,
pub max_packages: usize,
pub max_mutants: usize,
pub build_tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyConfig {
pub fail_on_severity: FailureSeverity,
pub fail_on_languages: Vec<String>,
pub fail_on_artifact_kinds: Vec<String>,
pub fail_on_target_risks: Vec<RiskLevel>,
}
#[derive(Debug, Clone, Deserialize)]
struct ConfigFile {
veritas: Option<VeritasSection>,
planner: Option<PlannerSection>,
policy: Option<PolicySection>,
plugins: Option<PluginSection>,
}
#[derive(Debug, Clone, Deserialize)]
struct VeritasSection {
budget_seconds: Option<u64>,
write_generated_tests: Option<bool>,
fail_on_generated_test_failure: Option<bool>,
fail_on_findings: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
struct PlannerSection {
mode: Option<PlannerMode>,
command: Option<String>,
fail_on_error: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
struct PolicySection {
fail_on_severity: Option<FailureSeverity>,
fail_on_languages: Option<Vec<String>>,
fail_on_artifact_kinds: Option<Vec<String>>,
fail_on_target_risks: Option<Vec<RiskLevel>>,
}
#[derive(Debug, Clone, Deserialize)]
struct PluginSection {
rust: Option<RustPluginConfigPartial>,
go: Option<GoPluginConfigPartial>,
}
#[derive(Debug, Clone, Deserialize)]
struct RustPluginConfigPartial {
property_framework: Option<String>,
command_timeout_seconds: Option<u64>,
coverage_enabled: Option<bool>,
coverage_timeout_seconds: Option<u64>,
cargo_jobs: Option<usize>,
test_threads: Option<usize>,
systemd_scope: Option<bool>,
memory_max: Option<String>,
cpu_quota: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct GoPluginConfigPartial {
fuzz_seconds: Option<u64>,
fuzz_existing: Option<bool>,
coverage_enabled: Option<bool>,
reverse_dependency_depth: Option<usize>,
max_fuzz_targets: Option<usize>,
command_timeout_seconds: Option<u64>,
max_packages: Option<usize>,
max_mutants: Option<usize>,
build_tags: Option<Vec<String>>,
}
impl Default for VeritasConfig {
fn default() -> Self {
Self {
budget_seconds: 120,
write_generated_tests: true,
fail_on_generated_test_failure: true,
fail_on_findings: false,
planner: PlannerConfig {
mode: PlannerMode::Deterministic,
command: None,
fail_on_error: false,
},
policy: PolicyConfig {
fail_on_severity: FailureSeverity::Error,
fail_on_languages: Vec::new(),
fail_on_artifact_kinds: Vec::new(),
fail_on_target_risks: Vec::new(),
},
plugins: PluginConfigs {
rust: RustPluginConfig {
property_framework: "proptest".to_string(),
command_timeout_seconds: 120,
coverage_enabled: false,
coverage_timeout_seconds: 120,
cargo_jobs: 1,
test_threads: 1,
systemd_scope: false,
memory_max: None,
cpu_quota: None,
},
go: GoPluginConfig {
fuzz_seconds: 10,
fuzz_existing: true,
coverage_enabled: true,
reverse_dependency_depth: 1,
max_fuzz_targets: 20,
command_timeout_seconds: 120,
max_packages: 64,
max_mutants: 8,
build_tags: Vec::new(),
},
},
}
}
}
impl VeritasConfig {
pub fn load(root: &Path) -> Result<Self> {
let mut config = Self::default();
let Some(path) = config_path(root) else {
return Ok(config);
};
let contents = fs::read_to_string(&path)
.with_context(|| format!("failed to read config {}", path.display()))?;
let parsed: ConfigFile = toml::from_str(&contents)
.with_context(|| format!("failed to parse config {}", path.display()))?;
if let Some(veritas) = parsed.veritas {
if let Some(value) = veritas.budget_seconds {
config.budget_seconds = value;
}
if let Some(value) = veritas.write_generated_tests {
config.write_generated_tests = value;
}
if let Some(value) = veritas.fail_on_generated_test_failure {
config.fail_on_generated_test_failure = value;
}
if let Some(value) = veritas.fail_on_findings {
config.fail_on_findings = value;
}
}
if let Some(planner) = parsed.planner {
if let Some(value) = planner.mode {
config.planner.mode = value;
}
if let Some(value) = planner.command {
config.planner.command = Some(value);
}
if let Some(value) = planner.fail_on_error {
config.planner.fail_on_error = value;
}
}
if let Some(policy) = parsed.policy {
if let Some(value) = policy.fail_on_severity {
config.policy.fail_on_severity = value;
}
if let Some(value) = policy.fail_on_languages {
config.policy.fail_on_languages = value;
}
if let Some(value) = policy.fail_on_artifact_kinds {
config.policy.fail_on_artifact_kinds = value;
}
if let Some(value) = policy.fail_on_target_risks {
config.policy.fail_on_target_risks = value;
}
}
if let Some(plugins) = parsed.plugins {
if let Some(rust) = plugins.rust {
if let Some(value) = rust.property_framework {
config.plugins.rust.property_framework = value;
}
if let Some(value) = rust.command_timeout_seconds {
config.plugins.rust.command_timeout_seconds = value;
}
if let Some(value) = rust.coverage_enabled {
config.plugins.rust.coverage_enabled = value;
}
if let Some(value) = rust.coverage_timeout_seconds {
config.plugins.rust.coverage_timeout_seconds = value;
}
if let Some(value) = rust.cargo_jobs {
config.plugins.rust.cargo_jobs = value;
}
if let Some(value) = rust.test_threads {
config.plugins.rust.test_threads = value;
}
if let Some(value) = rust.systemd_scope {
config.plugins.rust.systemd_scope = value;
}
if let Some(value) = rust.memory_max {
config.plugins.rust.memory_max = Some(value);
}
if let Some(value) = rust.cpu_quota {
config.plugins.rust.cpu_quota = Some(value);
}
}
if let Some(go) = plugins.go {
if let Some(value) = go.fuzz_seconds {
config.plugins.go.fuzz_seconds = value;
}
if let Some(value) = go.fuzz_existing {
config.plugins.go.fuzz_existing = value;
}
if let Some(value) = go.coverage_enabled {
config.plugins.go.coverage_enabled = value;
}
if let Some(value) = go.reverse_dependency_depth {
config.plugins.go.reverse_dependency_depth = value;
}
if let Some(value) = go.max_fuzz_targets {
config.plugins.go.max_fuzz_targets = value;
}
if let Some(value) = go.command_timeout_seconds {
config.plugins.go.command_timeout_seconds = value;
}
if let Some(value) = go.max_packages {
config.plugins.go.max_packages = value;
}
if let Some(value) = go.max_mutants {
config.plugins.go.max_mutants = value;
}
if let Some(value) = go.build_tags {
config.plugins.go.build_tags = value;
}
}
}
Ok(config)
}
}
fn config_path(root: &Path) -> Option<std::path::PathBuf> {
let candidates = [root.join("veritas.toml"), root.join(".veritas.toml")];
candidates.into_iter().find(|candidate| candidate.exists())
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::{Path, PathBuf},
process,
time::{SystemTime, UNIX_EPOCH},
};
use veritas_plugin_api::{FailureSeverity, RiskLevel};
use super::VeritasConfig;
#[test]
fn loads_go_production_and_policy_config() {
let root = TempRoot::new();
fs::write(
root.path().join("veritas.toml"),
r#"
[policy]
fail_on_severity = "warning"
fail_on_languages = ["go"]
fail_on_artifact_kinds = ["mutation_check"]
fail_on_target_risks = ["high"]
[plugins.go]
fuzz_seconds = 3
fuzz_existing = false
coverage_enabled = false
reverse_dependency_depth = 2
max_fuzz_targets = 4
command_timeout_seconds = 9
max_packages = 12
max_mutants = 5
build_tags = ["integration", "sqlite"]
[plugins.rust]
property_framework = "proptest"
command_timeout_seconds = 33
coverage_enabled = true
coverage_timeout_seconds = 44
cargo_jobs = 2
test_threads = 3
systemd_scope = true
memory_max = "4G"
cpu_quota = "150%"
"#,
)
.expect("write config");
let config = VeritasConfig::load(root.path()).expect("load config");
assert_eq!(config.policy.fail_on_severity, FailureSeverity::Warning);
assert_eq!(config.policy.fail_on_languages, vec!["go"]);
assert_eq!(config.policy.fail_on_artifact_kinds, vec!["mutation_check"]);
assert_eq!(config.policy.fail_on_target_risks, vec![RiskLevel::High]);
assert_eq!(config.plugins.go.fuzz_seconds, 3);
assert!(!config.plugins.go.fuzz_existing);
assert!(!config.plugins.go.coverage_enabled);
assert_eq!(config.plugins.go.reverse_dependency_depth, 2);
assert_eq!(config.plugins.go.max_fuzz_targets, 4);
assert_eq!(config.plugins.go.command_timeout_seconds, 9);
assert_eq!(config.plugins.go.max_packages, 12);
assert_eq!(config.plugins.go.max_mutants, 5);
assert_eq!(config.plugins.go.build_tags, vec!["integration", "sqlite"]);
assert_eq!(config.plugins.rust.command_timeout_seconds, 33);
assert!(config.plugins.rust.coverage_enabled);
assert_eq!(config.plugins.rust.coverage_timeout_seconds, 44);
assert_eq!(config.plugins.rust.cargo_jobs, 2);
assert_eq!(config.plugins.rust.test_threads, 3);
assert!(config.plugins.rust.systemd_scope);
assert_eq!(config.plugins.rust.memory_max.as_deref(), Some("4G"));
assert_eq!(config.plugins.rust.cpu_quota.as_deref(), Some("150%"));
}
struct TempRoot {
path: PathBuf,
}
impl TempRoot {
fn new() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after UNIX_EPOCH")
.as_nanos();
let path =
std::env::temp_dir().join(format!("veritas-config-test-{}-{nanos}", process::id()));
fs::create_dir_all(&path).expect("create temp root");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
}