confget 5.0.1

Parse configuration files.
Documentation
/*
 * Copyright (c) 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
//! A trivial set of tests.
//!
//! For more complete testing of the `confget` executable, see the full
//! source distribution available from
//! <https://devel.ringlet.net/textproc/confget/>.

// This is a test module, right?
#![allow(clippy::panic_in_result_fn)]
#![allow(clippy::unwrap_used)]

use std::fs;
use std::path::Path;

use rstest::rstest;
use thiserror::Error;
use tracing::trace;
use tracing_test::traced_test;

use crate::defs::{ConfgetError, Config};
use crate::format;

#[derive(Debug, Error)]
enum Error {
    #[error("Could not read the {0} input file")]
    FileRead(String, #[source] ConfgetError),

    #[error("Could not filter vars from the {0} input file")]
    FilterVars(String, #[source] ConfgetError),

    #[error("Could not get the {0} section from the {1} input file")]
    NoSection(String, String),

    #[error("Could not get the {0}.{1} value from the {2} input file")]
    NoValue(String, String, String),
}

fn run(
    cfg: &Config,
    filename: &str,
    section: &str,
    key: &str,
    value: &str,
    expected: bool,
) -> Result<(), Error> {
    let (data, _) =
        super::read_ini_file(cfg).map_err(|err| Error::FileRead(filename.to_owned(), err))?;
    assert_eq!(
        data.get(section)
            .ok_or_else(|| Error::NoSection(filename.to_owned(), section.to_owned()))?
            .get(key)
            .ok_or_else(|| Error::NoValue(
                filename.to_owned(),
                section.to_owned(),
                key.to_owned()
            ))?,
        value
    );

    let vars = format::filter_vars(cfg, &data, section)
        .map_err(|err| Error::FilterVars(filename.to_owned(), err))?;
    trace!(?vars);
    if expected {
        assert!(!vars.is_empty());
        let found = vars
            .into_iter()
            .find(|var| var.name == key)
            .ok_or_else(|| {
                Error::NoValue(filename.to_owned(), section.to_owned(), key.to_owned())
            })?;
        assert_eq!(found.value, value);
    } else {
        assert!(vars.is_empty());
    }
    Ok(())
}

#[rstest]
#[case("t1.ini", "b sect", "key4", "v'alu'e4")]
#[case("t2.ini", "sec1", "key2", "4")]
#[case("t3.ini", "a", "aonly", "a")]
#[case("t4.ini", "x", "key8", "key9=key10=key11")]
#[traced_test]
fn test_parse_full(
    #[case] filename: &str,
    #[case] section: &str,
    #[case] key: &str,
    #[case] value: &str,
) -> Result<(), Error> {
    let cfg_list_all = Config {
        filename: Some(format!("test_data/{}", filename)),
        list_all: true,
        ..Config::default()
    };
    run(&cfg_list_all, filename, section, key, value, true)?;

    let cfg_single = Config {
        list_all: false,
        varnames: vec![key.to_owned()],
        ..cfg_list_all
    };
    run(&cfg_single, filename, section, key, value, true)?;

    let cfg_match_var_names = Config {
        match_var_names: true,
        varnames: vec![format!("{}*", key)],
        ..cfg_single
    };
    run(&cfg_match_var_names, filename, section, key, value, true)?;

    let cfg_match_var_names_fail = Config {
        match_var_names: true,
        varnames: vec![format!("{}x*", key)],
        ..cfg_match_var_names
    };
    run(
        &cfg_match_var_names_fail,
        filename,
        section,
        key,
        value,
        false,
    )?;

    let cfg_match_var_values = Config {
        list_all: true,
        match_var_names: false,
        match_var_values: Some(format!("{}*", value.to_owned())),
        varnames: vec![],
        ..cfg_match_var_names_fail
    };
    run(&cfg_match_var_values, filename, section, key, value, true)?;

    let cfg_match_var_values_fail = Config {
        match_var_values: Some(format!("{}x*", value.to_owned())),
        ..cfg_match_var_values
    };
    run(
        &cfg_match_var_values_fail,
        filename,
        section,
        key,
        value,
        false,
    )?;

    Ok(())
}

#[rstest]
#[case("t1.ini")]
#[case("t2.ini")]
#[case("t3.ini")]
#[case("t4.ini")]
#[traced_test]
fn test_data_files_same(#[case] filename: &str) {
    let up = Path::new("../t").join(filename);
    if !up.is_file() {
        trace!("No {} file, skipping", up.display());
        return;
    }
    let up_contents = fs::read_to_string(&up).unwrap();

    let ours = Path::new("test_data").join(filename);
    let ours_contents = fs::read_to_string(&ours).unwrap();

    assert_eq!(up_contents, ours_contents);
}