310 lines
12 KiB
Rust
310 lines
12 KiB
Rust
// file: crates/ksp-config-lib/unit_tests/document.rs
|
|
// version: 4
|
|
|
|
#[test]
|
|
fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
|
|
let workspace = workspace_root();
|
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
|
let registry = crate::ConfigFileRegistry::defaults();
|
|
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
|
assert!(bootstrap.is_ok(), "workspace Config paths should be valid: {bootstrap:?}");
|
|
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
|
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
|
|
if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) {
|
|
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
|
|
let document = engine.load_validated_document(&file_id);
|
|
assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}");
|
|
if let std::result::Result::Ok(document) = document {
|
|
assert_eq!(document.file_id(), &file_id);
|
|
assert_eq!(document.path(), workspace.join("config/std.logging.json").as_path());
|
|
let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str);
|
|
assert!(default_profile.is_some(), "validated Logging document should retain default_profile");
|
|
if let std::option::Option::Some(default_profile) = default_profile {
|
|
let profiles = document.value().get("profiles").and_then(serde_json::Value::as_array);
|
|
assert!(profiles.is_some(), "validated Logging document should retain profiles");
|
|
if let std::option::Option::Some(profiles) = profiles {
|
|
assert!(profiles.iter().any(|profile| -> bool {
|
|
return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some(default_profile);
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_document_is_reported_with_file_read_error() {
|
|
let fixture = fixture_roots("missing-document");
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::None, valid_minimal_logging_schema());
|
|
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_JSON_FILE_READ_FAILED);
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_json_is_reported_before_schema_validation() {
|
|
let fixture = fixture_roots("malformed-json");
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::Some("{ invalid-json"), valid_minimal_logging_schema());
|
|
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_JSON_SYNTAX_INVALID);
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_schema_document_is_reported_before_instance_validation() {
|
|
let fixture = fixture_roots("invalid-schema");
|
|
let schema = r#"{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "definitely-not-a-json-schema-type"
|
|
}"#;
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::Some("{}"), schema);
|
|
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_SCHEMA_INVALID);
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
#[test]
|
|
fn schema_violation_is_distinct_from_json_syntax_failure() {
|
|
let fixture = fixture_roots("schema-violation");
|
|
let schema = r#"{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"required": ["required_field"],
|
|
"properties": {
|
|
"required_field": {"type": "string"}
|
|
}
|
|
}"#;
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::Some("{}"), schema);
|
|
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_SCHEMA_VALIDATION_FAILED);
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
#[test]
|
|
fn schema_valid_logging_document_can_still_fail_ksp_semantics() {
|
|
let fixture = fixture_roots("semantic-invalid");
|
|
let workspace = workspace_root();
|
|
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
|
|
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
|
|
if let std::result::Result::Ok(schema_source) = schema_source {
|
|
let document = r#"{
|
|
"format_version": 1,
|
|
"logs_directory": "logs",
|
|
"default_profile": "duplicate-output",
|
|
"profiles": [
|
|
{
|
|
"profile_id": "duplicate-output",
|
|
"default_filter": "info",
|
|
"span_events": "off",
|
|
"console": {
|
|
"enabled": false,
|
|
"output": "stderr",
|
|
"ansi": false,
|
|
"format": "human",
|
|
"filter": {"level": "trace", "targets": ["*"], "domains": ["*"]}
|
|
},
|
|
"files": [
|
|
{
|
|
"output_id": "file.same",
|
|
"enabled": true,
|
|
"path": "first.log",
|
|
"rotation": "daily",
|
|
"format": "human",
|
|
"ansi": false,
|
|
"filter": {"level": "info", "targets": ["*"], "domains": ["*"]}
|
|
},
|
|
{
|
|
"output_id": "file.same",
|
|
"enabled": true,
|
|
"path": "second.log",
|
|
"rotation": "daily",
|
|
"format": "human",
|
|
"ansi": false,
|
|
"filter": {"level": "info", "targets": ["*"], "domains": ["*"]}
|
|
}
|
|
],
|
|
"target_filters": []
|
|
}
|
|
]
|
|
}"#;
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::Some(document), schema_source.as_str());
|
|
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
|
|
}
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_profile_ids_are_semantically_invalid() {
|
|
let fixture = fixture_roots("duplicate-profile-id");
|
|
let workspace = workspace_root();
|
|
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
|
|
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
|
|
if let std::result::Result::Ok(schema_source) = schema_source {
|
|
let source = valid_logging_source_with_profiles("first", "first", "first");
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str());
|
|
assert!(prepared.is_ok(), "duplicate profile fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
|
|
}
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_default_profile_target_is_semantically_invalid() {
|
|
let fixture = fixture_roots("missing-default-profile");
|
|
let workspace = workspace_root();
|
|
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
|
|
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
|
|
if let std::result::Result::Ok(schema_source) = schema_source {
|
|
let source = valid_logging_source_with_profiles("missing", "first", "second");
|
|
let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str());
|
|
assert!(prepared.is_ok(), "missing default profile fixture should be writable: {prepared:?}");
|
|
if prepared.is_ok() {
|
|
let result = load_fixture(&fixture);
|
|
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
|
|
}
|
|
}
|
|
cleanup_fixture(&fixture);
|
|
}
|
|
|
|
fn valid_logging_source_with_profiles(default_profile: &str, first_profile: &str, second_profile: &str) -> String {
|
|
return format!(
|
|
r#"{{
|
|
"format_version": 1,
|
|
"logs_directory": "logs",
|
|
"default_profile": "{default_profile}",
|
|
"profiles": [
|
|
{},
|
|
{}
|
|
]
|
|
}}"#,
|
|
valid_logging_profile(first_profile, "file.first"),
|
|
valid_logging_profile(second_profile, "file.second")
|
|
);
|
|
}
|
|
|
|
fn valid_logging_profile(profile_id: &str, output_id: &str) -> String {
|
|
return format!(
|
|
r#"{{
|
|
"profile_id": "{profile_id}",
|
|
"default_filter": "info",
|
|
"span_events": "off",
|
|
"console": {{
|
|
"enabled": false,
|
|
"output": "stderr",
|
|
"ansi": false,
|
|
"format": "human",
|
|
"filter": {{"level": "trace", "targets": ["*"], "domains": ["*"]}}
|
|
}},
|
|
"files": [{{
|
|
"output_id": "{output_id}",
|
|
"enabled": true,
|
|
"path": "output.log",
|
|
"rotation": "daily",
|
|
"format": "human",
|
|
"ansi": false,
|
|
"filter": {{"level": "info", "targets": ["*"], "domains": ["*"]}}
|
|
}}],
|
|
"target_filters": []
|
|
}}"#
|
|
);
|
|
}
|
|
|
|
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigJsonDocument> {
|
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
|
|
let bootstrap = match bootstrap {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let registry = crate::ConfigFileRegistry::defaults();
|
|
let registry = match registry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
|
let file_id = match file_id {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
|
|
return engine.load_validated_document(&file_id);
|
|
}
|
|
|
|
fn prepare_fixture(fixture: &FixtureRoots, document: std::option::Option<&str>, schema: &str) -> std::io::Result<()> {
|
|
cleanup_fixture(fixture);
|
|
let config = std::fs::create_dir_all(fixture.config.as_path());
|
|
if let std::result::Result::Err(error) = config {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let schemas = std::fs::create_dir_all(fixture.schemas.as_path());
|
|
if let std::result::Result::Err(error) = schemas {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let schema_write = std::fs::write(fixture.schemas.join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME), schema);
|
|
if let std::result::Result::Err(error) = schema_write {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if let std::option::Option::Some(document) = document {
|
|
let document_write = std::fs::write(fixture.config.join(crate::DEFAULT_STD_LOGGING_FILENAME), document);
|
|
if let std::result::Result::Err(error) = document_write {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn valid_minimal_logging_schema() -> &'static str {
|
|
return r#"{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object"
|
|
}"#;
|
|
}
|
|
|
|
fn assert_error_code(result: ksp_core_lib::Result<crate::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) {
|
|
assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}");
|
|
if let std::result::Result::Err(error) = result {
|
|
assert_eq!(error.code(), expected);
|
|
}
|
|
}
|
|
|
|
fn workspace_root() -> std::path::PathBuf {
|
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
|
}
|
|
|
|
struct FixtureRoots {
|
|
root: std::path::PathBuf,
|
|
config: std::path::PathBuf,
|
|
schemas: std::path::PathBuf,
|
|
}
|
|
|
|
fn fixture_roots(name: &str) -> FixtureRoots {
|
|
let mut root = std::env::temp_dir();
|
|
root.push(format!("ksp-config-lib-pre008-{name}-{}", std::process::id()));
|
|
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
|
|
}
|
|
|
|
fn cleanup_fixture(fixture: &FixtureRoots) {
|
|
let result = std::fs::remove_dir_all(fixture.root.as_path());
|
|
if let std::result::Result::Err(error) = result {
|
|
assert_eq!(error.kind(), std::io::ErrorKind::NotFound, "fixture cleanup should only ignore missing directories: {error}");
|
|
}
|
|
}
|