openmw-config 1.0.2

A library for interacting with the Openmw Configuration system.
Documentation
mod common;

use common::{temp_dir, write_cfg};
use openmw_config::OpenMWConfiguration;
use proptest::prelude::*;
use std::collections::HashSet;
use std::fmt::Write as _;

fn unique_preserve_order(values: Vec<String>) -> Vec<String> {
    let mut seen = HashSet::new();
    let mut out = Vec::new();

    for value in values {
        if seen.insert(value.clone()) {
            out.push(value);
        }
    }

    out
}

fn load_cfg(tag: &str, cfg: &str) -> OpenMWConfiguration {
    let dir = temp_dir(tag);
    write_cfg(&dir, cfg);
    OpenMWConfiguration::new(Some(dir)).unwrap()
}

fn content_values(config: &OpenMWConfiguration) -> Vec<String> {
    config
        .content_files_iter()
        .map(|f| f.value().clone())
        .collect()
}

fn archive_values(config: &OpenMWConfiguration) -> Vec<String> {
    config
        .fallback_archives_iter()
        .map(|f| f.value().clone())
        .collect()
}

fn groundcover_values(config: &OpenMWConfiguration) -> Vec<String> {
    config
        .groundcover_iter()
        .map(|f| f.value().clone())
        .collect()
}

fn fallback_map(config: &OpenMWConfiguration) -> std::collections::HashMap<String, String> {
    let mut map = std::collections::HashMap::new();
    for setting in config.game_settings() {
        map.insert(setting.key().clone(), setting.value().to_string());
    }
    map
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(48))]

    #[test]
    fn prop_replace_content_resets_prior_content_only(
        pre_content in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esm", 0..12),
        post_content in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esm", 0..12),
        archives in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.bsa", 0..12),
        ground in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esp", 0..12),
        fallback in prop::collection::vec(("[A-Za-z][A-Za-z0-9_]{0,20}", "[A-Za-z][A-Za-z0-9]{0,6}"), 0..12),
    ) {
        let pre_content = unique_preserve_order(pre_content);
        let post_content = unique_preserve_order(post_content);
        let archives = unique_preserve_order(archives);
        let ground = unique_preserve_order(ground);

        let mut cfg = String::new();
        for item in &pre_content {
            writeln!(&mut cfg, "content={item}").expect("writing to String cannot fail");
        }
        for item in &archives {
            writeln!(&mut cfg, "fallback-archive={item}").expect("writing to String cannot fail");
        }
        for item in &ground {
            writeln!(&mut cfg, "groundcover={item}").expect("writing to String cannot fail");
        }
        for (k, v) in &fallback {
            writeln!(&mut cfg, "fallback={k},{v}").expect("writing to String cannot fail");
        }

        cfg.push_str("replace=content\n");
        for item in &post_content {
            writeln!(&mut cfg, "content={item}").expect("writing to String cannot fail");
        }

        let loaded = load_cfg("prop_replace_content", &cfg);

        prop_assert_eq!(content_values(&loaded), post_content);
        prop_assert_eq!(archive_values(&loaded), archives);
        prop_assert_eq!(groundcover_values(&loaded), ground);
    }

    #[test]
    fn prop_replace_fallback_resets_prior_fallback_only(
        pre_fallback in prop::collection::vec(("[A-Za-z][A-Za-z0-9_]{0,20}", "[A-Za-z][A-Za-z0-9]{0,6}"), 0..12),
        post_fallback in prop::collection::vec(("[A-Za-z][A-Za-z0-9_]{0,20}", "[A-Za-z][A-Za-z0-9]{0,6}"), 0..12),
        content in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esm", 0..12),
        archives in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.bsa", 0..12),
    ) {
        let content = unique_preserve_order(content);
        let archives = unique_preserve_order(archives);

        let mut cfg = String::new();
        for item in &content {
            writeln!(&mut cfg, "content={item}").expect("writing to String cannot fail");
        }
        for item in &archives {
            writeln!(&mut cfg, "fallback-archive={item}").expect("writing to String cannot fail");
        }
        for (k, v) in &pre_fallback {
            writeln!(&mut cfg, "fallback={k},{v}").expect("writing to String cannot fail");
        }

        cfg.push_str("replace=fallback\n");
        for (k, v) in &post_fallback {
            writeln!(&mut cfg, "fallback={k},{v}").expect("writing to String cannot fail");
        }

        let loaded = load_cfg("prop_replace_fallback", &cfg);

        let mut expected = std::collections::HashMap::new();
        for (k, v) in &post_fallback {
            expected.insert(k.clone(), v.clone());
        }

        prop_assert_eq!(fallback_map(&loaded), expected);
        prop_assert_eq!(content_values(&loaded), content);
        prop_assert_eq!(archive_values(&loaded), archives);
    }

    #[test]
    fn prop_replace_groundcover_resets_prior_groundcover_only(
        pre_ground in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esp", 0..12),
        post_ground in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esp", 0..12),
        content in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esm", 0..12),
    ) {
        let pre_ground = unique_preserve_order(pre_ground);
        let post_ground = unique_preserve_order(post_ground);
        let content = unique_preserve_order(content);

        let mut cfg = String::new();
        for item in &content {
            writeln!(&mut cfg, "content={item}").expect("writing to String cannot fail");
        }
        for item in &pre_ground {
            writeln!(&mut cfg, "groundcover={item}").expect("writing to String cannot fail");
        }
        cfg.push_str("replace=groundcover\n");
        for item in &post_ground {
            writeln!(&mut cfg, "groundcover={item}").expect("writing to String cannot fail");
        }

        let loaded = load_cfg("prop_replace_ground", &cfg);
        prop_assert_eq!(groundcover_values(&loaded), post_ground);
        prop_assert_eq!(content_values(&loaded), content);
    }

    #[test]
    fn prop_replace_fallback_archives_resets_prior_archives_only(
        pre_archives in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.bsa", 0..12),
        post_archives in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.bsa", 0..12),
        content in prop::collection::vec("[A-Za-z0-9_-]{1,24}\\.esm", 0..12),
    ) {
        let pre_archives = unique_preserve_order(pre_archives);
        let post_archives = unique_preserve_order(post_archives);
        let content = unique_preserve_order(content);

        let mut cfg = String::new();
        for item in &content {
            writeln!(&mut cfg, "content={item}").expect("writing to String cannot fail");
        }
        for item in &pre_archives {
            writeln!(&mut cfg, "fallback-archive={item}").expect("writing to String cannot fail");
        }
        cfg.push_str("replace=fallback-archives\n");
        for item in &post_archives {
            writeln!(&mut cfg, "fallback-archive={item}").expect("writing to String cannot fail");
        }

        let loaded = load_cfg("prop_replace_archives", &cfg);
        prop_assert_eq!(archive_values(&loaded), post_archives);
        prop_assert_eq!(content_values(&loaded), content);
    }
}