use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::services::ConfigService;
pub fn validate_config(path: Option<&Path>, schemas_dir: Option<&Path>) -> Result<PathBuf> {
if let Some(p) = path {
if is_model_catalog_file(p) {
validate_model_catalog_file(p)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", p.display(), e))?;
return Ok(p.to_path_buf());
}
if is_behavior_config_file(p) {
let content = std::fs::read_to_string(p)
.map_err(|e| anyhow::anyhow!("Failed to read behavior config file {}: {}", p.display(), e))?;
let value: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse JSON from {}: {}", p.display(), e))?;
mecha10_behavior_runtime::config::validate_behavior_composition_json(&value)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", p.display(), e))?;
return Ok(p.to_path_buf());
}
}
let target = ConfigService::resolve_and_validate(path)?;
if path.is_none() {
let project_root = target.parent().unwrap_or_else(|| Path::new("."));
validate_environment_configs(project_root, schemas_dir)?;
validate_active_behavior_tree_config(project_root)?;
validate_rl_job_configs(project_root)?;
}
Ok(target)
}
pub fn needs_environment_schema(project_root: &Path) -> bool {
project_root
.join(mecha10_cli_core::paths::project::SIMULATION_ENVIRONMENTS_DIR)
.exists()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConfigKind {
ProjectAndNodes,
ModelCatalog,
SimulationEnvironments,
ActiveBehaviorTree,
RlJobs,
}
impl ConfigKind {
pub const ALL: &'static [ConfigKind] = &[
ConfigKind::ProjectAndNodes,
ConfigKind::ModelCatalog,
ConfigKind::SimulationEnvironments,
ConfigKind::ActiveBehaviorTree,
ConfigKind::RlJobs,
];
}
pub fn validate_configs(project_root: &Path, kinds: &[ConfigKind], schemas_dir: Option<&Path>) -> Result<()> {
let mut failures: Vec<String> = Vec::new();
for kind in kinds {
let result = match kind {
ConfigKind::ProjectAndNodes => {
let project_path = project_root.join(mecha10_cli_core::paths::PROJECT_CONFIG);
ConfigService::resolve_and_validate(Some(&project_path)).map(|_| ())
}
ConfigKind::ModelCatalog => validate_model_catalog_if_present(project_root),
ConfigKind::SimulationEnvironments => validate_environment_configs(project_root, schemas_dir),
ConfigKind::ActiveBehaviorTree => validate_active_behavior_tree_config(project_root),
ConfigKind::RlJobs => validate_rl_job_configs(project_root),
};
if let Err(e) = result {
failures.push(format!("{:#}", e));
}
}
if failures.is_empty() {
Ok(())
} else {
Err(anyhow::anyhow!(
"Config validation failed ({} problem{}):\n\n{}",
failures.len(),
if failures.len() == 1 { "" } else { "s" },
failures.join("\n\n")
))
}
}
fn validate_model_catalog_if_present(project_root: &Path) -> Result<()> {
for candidate in [
project_root.join("model_catalog.toml"),
project_root.join("configs").join("model_catalog.toml"),
] {
if candidate.exists() {
validate_model_catalog_file(&candidate)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", candidate.display(), e))?;
}
}
Ok(())
}
fn validate_environment_configs(project_root: &Path, schemas_dir: Option<&Path>) -> Result<()> {
let environments_dir = project_root.join(mecha10_cli_core::paths::project::SIMULATION_ENVIRONMENTS_DIR);
if !environments_dir.exists() {
return Ok(());
}
let mut errors = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(&environments_dir)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", environments_dir.display(), e))?
.filter_map(|e| e.ok())
.collect();
entries.sort_by_key(std::fs::DirEntry::path);
for entry in entries {
let env_dir = entry.path();
if !env_dir.is_dir() {
continue;
}
let env_json = env_dir.join("environment.json");
if !env_json.exists() {
continue;
}
if let Err(e) = validate_one_environment_config(&env_json, schemas_dir) {
errors.push(e.to_string());
}
}
if errors.is_empty() {
Ok(())
} else {
Err(anyhow::anyhow!(errors.join("\n\n")))
}
}
fn validate_one_environment_config(path: &Path, schemas_dir: Option<&Path>) -> Result<()> {
let schemas_dir = schemas_dir.ok_or_else(|| {
anyhow::anyhow!(
"No mecha10-assets schemas directory available to validate {} against",
path.display()
)
})?;
let content = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read environment config {}: {}", path.display(), e))?;
let value: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse JSON from {}: {}", path.display(), e))?;
mecha10_schema_registry::validate_environment_config_json(&value, schemas_dir)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", path.display(), e))
}
fn validate_active_behavior_tree_config(project_root: &Path) -> Result<()> {
let project_path = project_root.join(mecha10_cli_core::paths::PROJECT_CONFIG);
if !project_path.exists() {
return Ok(());
}
let content = std::fs::read_to_string(&project_path)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", project_path.display(), e))?;
let value: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse JSON from {}: {}", project_path.display(), e))?;
let Some(active) = value
.get("behaviors")
.and_then(|b| b.get("active"))
.and_then(|a| a.as_str())
else {
return Ok(());
};
let config_path = mecha10_behavior_runtime::resolve_behavior_config_path(active, project_root)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for behaviors.active '{active}': {e}"))?;
let content = std::fs::read_to_string(&config_path)
.map_err(|e| anyhow::anyhow!("Failed to read behavior config {}: {}", config_path.display(), e))?;
let behavior_value: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse JSON from {}: {}", config_path.display(), e))?;
mecha10_behavior_runtime::validate_behavior_composition_json(&behavior_value)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", config_path.display(), e))
}
fn validate_rl_job_configs(project_root: &Path) -> Result<()> {
crate::services::load_job_catalog(project_root)
.map_err(|e| anyhow::anyhow!("Configuration validation failed for RL job configs: {:#}", e))?;
Ok(())
}
fn validate_model_catalog_file(path: &Path) -> Result<()> {
let content = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read model catalog file {}: {}", path.display(), e))?;
let toml_value: toml::Value =
toml::from_str(&content).map_err(|e| anyhow::anyhow!("Failed to parse TOML from {}: {}", path.display(), e))?;
let json_value = serde_json::to_value(toml_value)
.map_err(|e| anyhow::anyhow!("Failed to convert TOML from {}: {}", path.display(), e))?;
validate_model_catalog_json(&json_value).map_err(|e| anyhow::anyhow!("{}: {}", path.display(), e))
}
fn validate_model_catalog_json(value: &serde_json::Value) -> Result<()> {
validate_json_against_schema(value, &model_catalog_schema(), "Model catalog")?;
validate_unique_model_names(value)?;
Ok(())
}
fn model_catalog_schema() -> serde_json::Value {
let generator = schemars::SchemaGenerator::default();
let root_schema = generator.into_root_schema_for::<mecha10_models::services::model_service::ModelCatalogEntry>();
let mut entry_schema = serde_json::to_value(root_schema).expect("schemars Schema always serializes to JSON");
let definitions = entry_schema.as_object_mut().and_then(|obj| obj.remove("$defs"));
if let Some(obj) = entry_schema.as_object_mut() {
obj.remove("$schema");
}
let mut schema = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://mecha10.dev/schemas/model-catalog.schema.json",
"title": "Mecha10 Model Catalog",
"description": "Schema for model_catalog.toml, the mecha10-models model catalog",
"type": "object",
"required": ["models"],
"properties": {
"models": {
"type": "array",
"items": entry_schema
}
}
});
if let Some(definitions) = definitions {
schema["$defs"] = definitions;
}
schema
}
fn validate_unique_model_names(value: &serde_json::Value) -> Result<()> {
let Some(models) = value.get("models").and_then(serde_json::Value::as_array) else {
return Ok(());
};
let mut seen = std::collections::HashSet::new();
for model in models {
if let Some(name) = model.get("name").and_then(serde_json::Value::as_str) {
if !seen.insert(name.to_string()) {
anyhow::bail!(
"Duplicate model name '{}' found. Model catalog entry names must be unique.",
name
);
}
}
}
Ok(())
}
pub fn validate_preset_config_json(value: &serde_json::Value, schemas_dir: &Path) -> Result<()> {
mecha10_schema_registry::validate_preset_config_json(value, schemas_dir)
}
fn validate_json_against_schema(value: &serde_json::Value, schema: &serde_json::Value, context: &str) -> Result<()> {
let validator = jsonschema::options()
.with_draft(jsonschema::Draft::Draft7)
.build(schema)
.map_err(|e| anyhow::anyhow!("Invalid {context} schema: {e}"))?;
if !validator.is_valid(value) {
let messages: Vec<String> = validator
.iter_errors(value)
.map(|e| format!(" - {}: {} ({})", e.instance_path(), e, e.instance()))
.collect();
anyhow::bail!("{context} schema validation failed:\n{}", messages.join("\n"));
}
Ok(())
}
fn is_model_catalog_file(path: &Path) -> bool {
path.file_name().and_then(|n| n.to_str()) == Some("model_catalog.toml")
}
fn is_behavior_config_file(path: &Path) -> bool {
if path.file_name().and_then(|n| n.to_str()) != Some("config.json") {
return false;
}
path.components().any(|c| c.as_os_str().to_str() == Some("behaviors"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn schemas_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/schemas")
}
#[test]
fn validate_config_validates_a_valid_model_catalog_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("model_catalog.toml");
fs::write(
&path,
r#"
[[models]]
name = "yolov8n"
description = "YOLOv8 Nano"
task = "object-detection"
repo = "some/repo"
filename = "model.onnx"
"#,
)
.unwrap();
let result = validate_config(Some(&path), Some(&schemas_dir()));
assert!(result.is_ok(), "{:?}", result.err());
assert_eq!(result.unwrap(), path);
}
#[test]
fn validate_config_rejects_an_invalid_model_catalog_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("model_catalog.toml");
fs::write(&path, "[[models]]\nname = \"bad\"\ndescription = \"d\"\n").unwrap();
assert!(validate_config(Some(&path), Some(&schemas_dir())).is_err());
}
#[test]
fn validate_config_rejects_duplicate_model_names() {
let dir = tempdir().unwrap();
let path = dir.path().join("model_catalog.toml");
fs::write(
&path,
r#"
[[models]]
name = "dup"
description = "d1"
task = "object-detection"
repo = "r"
filename = "f.onnx"
[[models]]
name = "dup"
description = "d2"
task = "object-detection"
repo = "r"
filename = "f2.onnx"
"#,
)
.unwrap();
let err = validate_config(Some(&path), Some(&schemas_dir())).unwrap_err();
assert!(err.to_string().to_lowercase().contains("duplicate"));
}
#[test]
fn validate_config_resolves_a_built_in_node_type_via_its_shipped_schema_file() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("config")).unwrap();
fs::write(
dir.path().join("config/behavior.json"),
serde_json::json!({"custom": {}}).to_string(),
)
.unwrap();
let node_dir = dir.path().join("nodes/behavior");
fs::create_dir_all(&node_dir).unwrap();
let schema = serde_json::json!({
"type": "object",
"properties": {
"relay_topics": { "type": "array", "items": { "type": "string" } },
"custom": {
"type": "object",
"properties": {
"behavior_name": { "type": "string" }
},
"required": ["behavior_name"],
"additionalProperties": false
}
},
"additionalProperties": false
});
fs::write(
node_dir.join("behavior_executor.schema.json"),
serde_json::to_string(&schema).unwrap(),
)
.unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"nodes": {
"custom": [
{"name": "behavior_executor", "path": "./nodes/behavior", "config": "./config/behavior.json"}
]
}
})
.to_string(),
)
.unwrap();
let err = validate_config(Some(&project_path), Some(&schemas_dir())).unwrap_err();
assert!(
err.to_string().to_lowercase().contains("behavior_name"),
"expected the specific BehaviorExecutorConfig schema error, got: {}",
err
);
}
#[test]
fn validate_config_rejects_legacy_top_level_logging_block() {
let dir = tempdir().unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"logging": { "disk_logging_enabled": false }
})
.to_string(),
)
.unwrap();
let err = validate_config(Some(&project_path), Some(&schemas_dir())).unwrap_err();
assert!(
err.to_string().contains("environments.<env>.logging"),
"expected the error to point at the new per-environment shape, got: {}",
err
);
}
#[test]
fn validate_config_resolves_a_custom_schema_file_for_an_unrecognized_node_type() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("config")).unwrap();
fs::write(
dir.path().join("config/custom.json"),
serde_json::json!({"id": "custom_node", "executable": "./bin/custom"}).to_string(),
)
.unwrap();
fs::create_dir_all(dir.path().join("nodes/custom")).unwrap();
fs::write(
dir.path().join("nodes/custom/my_totally_custom_node.schema.json"),
serde_json::json!({
"type": "object",
"required": ["id", "executable"],
"properties": {
"id": {"type": "string"},
"executable": {"type": "string"}
}
})
.to_string(),
)
.unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"nodes": {
"custom": [
{"name": "my_totally_custom_node", "path": "./nodes/custom", "config": "./config/custom.json"}
]
}
})
.to_string(),
)
.unwrap();
assert!(validate_config(Some(&project_path), Some(&schemas_dir())).is_ok());
}
#[test]
fn validate_config_errors_when_an_unrecognized_node_type_has_no_declared_schema() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("config")).unwrap();
fs::write(
dir.path().join("config/custom.json"),
serde_json::json!({"id": "custom_node", "executable": "./bin/custom"}).to_string(),
)
.unwrap();
let project_path = dir.path().join("mecha10.json");
fs::write(
&project_path,
serde_json::json!({
"name": "proj",
"version": "1.0.0",
"nodes": {
"custom": [
{"name": "my_totally_custom_node", "path": "./nodes/custom", "config": "./config/custom.json"}
]
}
})
.to_string(),
)
.unwrap();
let err = validate_config(Some(&project_path), Some(&schemas_dir())).unwrap_err();
assert!(
err.to_string().contains("no declared schema"),
"expected a hard error naming the missing declared schema, got: {}",
err
);
}
#[test]
fn validate_rl_job_configs_ok_when_no_jobs_directory_present() {
let dir = tempdir().unwrap();
assert!(validate_rl_job_configs(dir.path()).is_ok());
}
#[test]
fn validate_rl_job_configs_rejects_a_malformed_job_definition() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs/rl/jobs")).unwrap();
fs::write(
dir.path().join("configs/rl/jobs/broken.yml"),
"not: [valid, job, definition",
)
.unwrap();
assert!(validate_rl_job_configs(dir.path()).is_err());
}
fn valid_environment_json() -> serde_json::Value {
serde_json::json!({
"id": "basic_arena",
"version": "1.0.0",
"name": "Basic Arena",
"description": "A test arena",
"scene_path": "res://basic_arena.tscn",
"dimensions": {"width": 20.0, "length": 20.0, "height": 3.0, "unit": "meters"},
"features": {}
})
}
#[test]
fn validate_environment_configs_ok_when_no_environments_directory_present() {
let dir = tempdir().unwrap();
assert!(validate_environment_configs(dir.path(), Some(&schemas_dir())).is_ok());
}
#[test]
fn validate_environment_configs_accepts_a_valid_environment() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/basic_arena")).unwrap();
fs::write(
dir.path()
.join("configs/simulation/environments/basic_arena/environment.json"),
valid_environment_json().to_string(),
)
.unwrap();
assert!(validate_environment_configs(dir.path(), Some(&schemas_dir())).is_ok());
}
#[test]
fn validate_environment_configs_rejects_a_config_missing_required_fields() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/broken_env")).unwrap();
fs::write(
dir.path()
.join("configs/simulation/environments/broken_env/environment.json"),
serde_json::json!({"id": "broken_env"}).to_string(),
)
.unwrap();
let err = validate_environment_configs(dir.path(), Some(&schemas_dir())).unwrap_err();
assert!(err.to_string().contains("broken_env"), "{err}");
}
#[test]
fn validate_environment_configs_reports_every_invalid_environment_not_just_the_first() {
let dir = tempdir().unwrap();
for name in ["env_a", "env_b"] {
fs::create_dir_all(dir.path().join("configs/simulation/environments").join(name)).unwrap();
fs::write(
dir.path()
.join("configs/simulation/environments")
.join(name)
.join("environment.json"),
serde_json::json!({}).to_string(),
)
.unwrap();
}
let err = validate_environment_configs(dir.path(), Some(&schemas_dir()))
.unwrap_err()
.to_string();
assert!(err.contains("env_a"), "{err}");
assert!(err.contains("env_b"), "{err}");
}
#[test]
fn validate_environment_configs_rejects_a_config_missing_scene_path() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/broken_env")).unwrap();
let mut value = valid_environment_json();
value.as_object_mut().unwrap().remove("scene_path");
fs::write(
dir.path()
.join("configs/simulation/environments/broken_env/environment.json"),
value.to_string(),
)
.unwrap();
let err = validate_environment_configs(dir.path(), Some(&schemas_dir()))
.unwrap_err()
.to_string();
assert!(err.contains("scene_path"), "{err}");
}
#[test]
fn validate_environment_configs_rejects_a_config_missing_dimensions() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/broken_env")).unwrap();
let mut value = valid_environment_json();
value.as_object_mut().unwrap().remove("dimensions");
fs::write(
dir.path()
.join("configs/simulation/environments/broken_env/environment.json"),
value.to_string(),
)
.unwrap();
let err = validate_environment_configs(dir.path(), Some(&schemas_dir()))
.unwrap_err()
.to_string();
assert!(err.contains("dimensions"), "{err}");
}
#[test]
fn validate_environment_configs_rejects_a_malformed_features_subfield() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/broken_env")).unwrap();
let mut value = valid_environment_json();
value["features"]["floor"] = serde_json::json!({
"type": "flat",
"material": "concrete",
"friction": 0.8
});
fs::write(
dir.path()
.join("configs/simulation/environments/broken_env/environment.json"),
value.to_string(),
)
.unwrap();
let err = validate_environment_configs(dir.path(), Some(&schemas_dir()))
.unwrap_err()
.to_string();
assert!(err.contains("collision"), "{err}");
}
#[test]
fn validate_active_behavior_tree_config_ok_when_no_project_config_present() {
let dir = tempdir().unwrap();
assert!(validate_active_behavior_tree_config(dir.path()).is_ok());
}
#[test]
fn validate_active_behavior_tree_config_ok_when_project_declares_no_behaviors() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0"}).to_string(),
)
.unwrap();
assert!(validate_active_behavior_tree_config(dir.path()).is_ok());
}
#[test]
fn validate_active_behavior_tree_config_accepts_a_valid_active_behavior() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0", "behaviors": {"active": "patrol"}}).to_string(),
)
.unwrap();
fs::create_dir_all(dir.path().join("configs/behaviors/patrol")).unwrap();
fs::write(
dir.path().join("configs/behaviors/patrol/config.json"),
serde_json::json!({"name": "patrol", "root": {"type": "sequence", "children": []}}).to_string(),
)
.unwrap();
assert!(validate_active_behavior_tree_config(dir.path()).is_ok());
}
#[test]
fn validate_active_behavior_tree_config_rejects_an_invalid_active_behavior() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0", "behaviors": {"active": "patrol"}}).to_string(),
)
.unwrap();
fs::create_dir_all(dir.path().join("configs/behaviors/patrol")).unwrap();
fs::write(
dir.path().join("configs/behaviors/patrol/config.json"),
serde_json::json!({"root": {"type": "sequence", "children": []}}).to_string(),
)
.unwrap();
assert!(validate_active_behavior_tree_config(dir.path()).is_err());
}
#[test]
fn validate_active_behavior_tree_config_errors_when_active_behavior_resolves_to_nothing() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0", "behaviors": {"active": "does_not_exist"}})
.to_string(),
)
.unwrap();
assert!(validate_active_behavior_tree_config(dir.path()).is_err());
}
#[test]
fn validate_model_catalog_if_present_ok_when_absent() {
let dir = tempdir().unwrap();
assert!(validate_model_catalog_if_present(dir.path()).is_ok());
}
#[test]
fn validate_model_catalog_if_present_validates_a_catalog_under_configs() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("configs")).unwrap();
fs::write(
dir.path().join("configs/model_catalog.toml"),
"[[models]]\nname = \"bad\"\ndescription = \"missing required fields\"\n",
)
.unwrap();
assert!(validate_model_catalog_if_present(dir.path()).is_err());
}
#[test]
fn validate_configs_passes_for_a_minimal_valid_project() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0"}).to_string(),
)
.unwrap();
assert!(validate_configs(dir.path(), ConfigKind::ALL, Some(&schemas_dir())).is_ok());
}
#[test]
fn validate_configs_fails_cleanly_when_an_environment_config_is_invalid() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0"}).to_string(),
)
.unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/broken_env")).unwrap();
fs::write(
dir.path()
.join("configs/simulation/environments/broken_env/environment.json"),
serde_json::json!({}).to_string(),
)
.unwrap();
let err = validate_configs(dir.path(), ConfigKind::ALL, Some(&schemas_dir())).unwrap_err();
assert!(err.to_string().contains("broken_env"), "{err}");
}
#[test]
fn validate_configs_reports_failures_from_multiple_kinds_at_once() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0", "behaviors": {"active": "does_not_exist"}})
.to_string(),
)
.unwrap();
fs::create_dir_all(dir.path().join("configs/simulation/environments/broken_env")).unwrap();
fs::write(
dir.path()
.join("configs/simulation/environments/broken_env/environment.json"),
serde_json::json!({}).to_string(),
)
.unwrap();
fs::create_dir_all(dir.path().join("configs/rl/jobs")).unwrap();
fs::write(
dir.path().join("configs/rl/jobs/broken.yml"),
"not: [valid, job, definition",
)
.unwrap();
let err = validate_configs(dir.path(), ConfigKind::ALL, Some(&schemas_dir()))
.unwrap_err()
.to_string();
assert!(err.contains("3 problems"), "{err}");
assert!(err.contains("broken_env"), "{err}");
assert!(err.contains("does_not_exist"), "{err}");
}
#[test]
fn validate_configs_can_validate_a_narrower_subset() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("mecha10.json"),
serde_json::json!({"name": "proj", "version": "1.0.0", "behaviors": {"active": "does_not_exist"}})
.to_string(),
)
.unwrap();
assert!(validate_configs(
dir.path(),
&[ConfigKind::ProjectAndNodes, ConfigKind::SimulationEnvironments],
Some(&schemas_dir())
)
.is_ok());
}
}