echoes 0.10.2

Find duplicated Rust doctests, functions and contexts
Documentation
//! `.echoes.toml` parameters. A missing file is a no-op.

use crate::{ContextScope, DependencySearch};
use std::path::Path;

#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
/// Parameters from a `.echoes.toml` file.
pub(crate) struct Config {
    /// Report groups with at least this many sites.
    pub(crate) threshold: Option<usize>,
    /// Skip blocks with fewer tokens than this.
    #[serde(rename = "min-tokens")]
    pub(crate) min_tokens: Option<usize>,
    /// Whether the duplicate function check runs.
    pub(crate) functions: Option<bool>,
    /// Skip functions whose body counts fewer tokens than this.
    #[serde(rename = "fn-min-tokens")]
    pub(crate) fn_min_tokens: Option<usize>,
    /// Whether the context-block check runs.
    #[serde(rename = "context-blocks")]
    pub(crate) context_blocks: Option<bool>,
    /// Skip context blocks whose canonical form counts fewer tokens than this.
    #[serde(rename = "context-min-tokens")]
    pub(crate) context_min_tokens: Option<usize>,
    /// Local context source selection.
    #[serde(rename = "context-select")]
    pub(crate) context_select: Option<ContextScope>,
    /// Workspace context comparison corpus.
    #[serde(rename = "context-search")]
    pub(crate) context_search: Option<ContextScope>,
    /// Dependency context discovery parameters.
    #[serde(default, rename = "context-dependencies")]
    pub(crate) context_dependencies: DependencySearch,
    /// Scan files marked generated as well.
    #[serde(rename = "scan-generated")]
    pub(crate) scan_generated: Option<bool>,
    /// More phrases that mark a file generated in its first lines.
    #[serde(rename = "generated-markers")]
    pub(crate) generated_markers: Option<alloc::vec::Vec<alloc::string::String>>,
    /// Gitignore-style globs of the files the scan leaves out.
    pub(crate) exclude: Option<alloc::vec::Vec<alloc::string::String>>,
}

/// Load `path`, or the default config when the file is absent.
///
/// # Errors
///
/// Fails when the file exists but cannot be read or is not valid TOML.
pub(crate) fn load(path: &Path) -> crate::Result<Config> {
    if !path.exists() {
        return Ok(Config::default());
    }
    let text = std::fs::read_to_string(path)?;
    Ok(toml::from_str(&text)?)
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::string::ToString;

    #[test]
    fn missing_file_is_default() {
        let cfg = load(Path::new("/nonexistent/.echoes.toml")).unwrap();
        assert_eq!(cfg, Config::default());
    }

    #[test]
    fn loads_parameters() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".echoes.toml");
        std::fs::write(
            &path,
            "threshold = 3\nmin-tokens = 4\nfunctions = false\nfn-min-tokens = 50\ncontext-blocks = true\ncontext-min-tokens = 30\nscan-generated = true\ngenerated-markers = [\"by rust-bindgen\"]\n",
        )
        .unwrap();
        let cfg = load(&path).unwrap();
        assert_eq!(cfg.threshold, Some(3));
        assert_eq!(cfg.min_tokens, Some(4));
        assert_eq!(cfg.functions, Some(false));
        assert_eq!(cfg.fn_min_tokens, Some(50));
        assert_eq!(cfg.context_blocks, Some(true));
        assert_eq!(cfg.context_min_tokens, Some(30));
        assert_eq!(cfg.scan_generated, Some(true));
        assert_eq!(
            cfg.generated_markers,
            Some(alloc::vec!["by rust-bindgen".to_string()])
        );
    }

    #[test]
    fn partial_file_keeps_defaults_for_absent_keys() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".echoes.toml");
        std::fs::write(&path, "threshold = 3\n").unwrap();
        let cfg = load(&path).unwrap();
        assert_eq!(cfg.threshold, Some(3));
        assert_eq!(cfg.min_tokens, None);
    }

    #[test]
    fn unknown_field_is_rejected() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".echoes.toml");
        std::fs::write(&path, "bogus = 1\n").unwrap();
        assert!(load(&path).is_err());
    }

    #[test]
    fn invalid_toml_is_an_error() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".echoes.toml");
        std::fs::write(&path, "threshold = ").unwrap();
        assert!(load(&path).is_err());
    }
}