homeboy 0.69.0

CLI for multi-component deployment and development workflow automation
Documentation
use std::path::Path;

use super::fingerprint::FileFingerprint;
use crate::extension::TestMappingConfig;

/// Partition fingerprints into source files and test files based on the config.
pub fn partition_fingerprints<'a>(
    fingerprints: &[&'a FileFingerprint],
    config: &TestMappingConfig,
) -> (Vec<&'a FileFingerprint>, Vec<&'a FileFingerprint>) {
    let mut source = Vec::new();
    let mut test = Vec::new();

    for fp in fingerprints {
        if is_test_file(&fp.relative_path, config) {
            test.push(*fp);
        } else if is_source_file(&fp.relative_path, config) {
            source.push(*fp);
        }
    }

    (source, test)
}

/// Check if a file path is within one of the configured source directories.
pub fn is_source_file(path: &str, config: &TestMappingConfig) -> bool {
    config.source_dirs.iter().any(|dir| path.starts_with(dir)) || path.ends_with(".inc")
}

/// Check if a file path is within one of the configured test directories.
pub fn is_test_file(path: &str, config: &TestMappingConfig) -> bool {
    config.test_dirs.iter().any(|dir| path.starts_with(dir))
}

/// Convert a source file path to its expected test file path using the template.
///
/// Template variables: `{dir}` (relative dir within source_dir), `{name}` (stem), `{ext}` (extension).
pub fn source_to_test_path(source_path: &str, config: &TestMappingConfig) -> Option<String> {
    if source_path.ends_with(".inc") {
        return None;
    }

    let source_dir = config
        .source_dirs
        .iter()
        .find(|dir| source_path.starts_with(dir.as_str()))?;

    let relative = source_path.strip_prefix(source_dir)?;
    let relative = relative.strip_prefix('/').unwrap_or(relative);

    let path = Path::new(relative);
    let name = path.file_stem()?.to_str()?;
    let ext = path.extension()?.to_str()?;
    let dir = path
        .parent()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    let test_path = config
        .test_file_pattern
        .replace("{dir}", &dir)
        .replace("{name}", name)
        .replace("{ext}", ext);

    Some(test_path.replace("//", "/"))
}

/// Convert a test file path back to its expected source file path.
pub fn test_to_source_path(test_path: &str, config: &TestMappingConfig) -> Option<String> {
    let pattern = &config.test_file_pattern;
    let test_dir = config.test_dirs.first()?;

    let relative_in_test = if test_path.starts_with(test_dir.as_str()) {
        let stripped = test_path.strip_prefix(test_dir.as_str())?;
        stripped.strip_prefix('/').unwrap_or(stripped)
    } else {
        return None;
    };

    let pattern_after_test_dir = if pattern.starts_with(test_dir.as_str()) {
        let stripped = pattern.strip_prefix(test_dir.as_str())?;
        stripped.strip_prefix('/').unwrap_or(stripped)
    } else {
        pattern.as_str()
    };

    let name_pos = pattern_after_test_dir.find("{name}")?;
    let after_name = &pattern_after_test_dir[name_pos + 6..];

    let test_file_path = Path::new(relative_in_test);
    let test_ext = test_file_path.extension()?.to_str()?;
    let test_stem = test_file_path.file_stem()?.to_str()?;
    let test_dir_part = test_file_path
        .parent()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    let suffix_before_ext = after_name.strip_suffix(".{ext}").unwrap_or("");
    let source_name = test_stem.strip_suffix(suffix_before_ext)?;
    let source_dir = config.source_dirs.first()?;

    Some(if test_dir_part.is_empty() {
        format!("{}/{}.{}", source_dir, source_name, test_ext)
    } else {
        format!(
            "{}/{}/{}.{}",
            source_dir, test_dir_part, source_name, test_ext
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::extension::TestMappingConfig;

    fn make_config() -> TestMappingConfig {
        TestMappingConfig {
            source_dirs: vec!["src".to_string()],
            test_dirs: vec!["tests".to_string()],
            test_file_pattern: "tests/{dir}/{name}_test.{ext}".to_string(),
            method_prefix: "test_".to_string(),
            critical_patterns: vec![],
            inline_tests: true,
        }
    }

    #[test]
    fn source_to_test_path_basic() {
        let config = make_config();
        assert_eq!(
            source_to_test_path("src/core/audit.rs", &config),
            Some("tests/core/audit_test.rs".to_string())
        );
    }

    #[test]
    fn source_to_test_path_top_level() {
        let config = make_config();
        assert_eq!(
            source_to_test_path("src/main.rs", &config),
            Some("tests/main_test.rs".to_string())
        );
    }

    #[test]
    fn source_to_test_path_skips_inc_fragments() {
        let config = make_config();
        assert_eq!(
            source_to_test_path("src/core/deploy/types.inc", &config),
            None
        );
    }

    #[test]
    fn test_to_source_path_basic() {
        let config = make_config();
        assert_eq!(
            test_to_source_path("tests/core/audit_test.rs", &config),
            Some("src/core/audit.rs".to_string())
        );
    }
}