check.rs 104 lignes · 3514 octets
//! Contrôle statique du réalisé : une route du todo est « faite » si un
//! `.route("<chemin>")` (chaîne exacte) apparaît dans les sources du projet.

use std::path::Path;

use crate::todo::TodoRow;

/// (pur) Marque `done` selon la présence de `"<route>"` dans `haystack`.
pub fn mark(rows: &mut [TodoRow], haystack: &str) {
    for r in rows.iter_mut() {
        // chaîne littérale exacte, guillemet fermant inclus → évite les
        // faux positifs de préfixe (/settings vs /settings/sessions).
        let needle = format!("\"{}\"", r.route);
        r.done = haystack.contains(&needle);
    }
}

/// (I/O) Concatène le contenu des `.rs` sous `root`, en ignorant `target/`
/// et les crates vendored `rustwarden-*`.
pub fn collect_sources(root: &Path) -> String {
    let mut out = String::new();
    walk(root, &mut out);
    out
}

fn walk(dir: &Path, out: &mut String) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if path.is_dir() {
            if name == "target" || name.starts_with("rustwarden-") {
                continue;
            }
            walk(&path, out);
        } else if path.extension().is_some_and(|e| e == "rs") {
            if let Ok(s) = std::fs::read_to_string(&path) {
                out.push_str(&s);
                out.push('\n');
            }
        }
    }
}

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

    fn row(route: &str) -> TodoRow {
        TodoRow {
            done: false,
            functionality: "F".into(),
            screen: "S".into(),
            route: route.into(),
            channel: "browser".into(),
            protected: true,
            methods: "".into(),
            io: "".into(),
        }
    }

    #[test]
    fn mark_detecte_route_presente() {
        let mut rows = vec![row("/profile"), row("/settings/sessions")];
        let haystack = r#"router.route("/profile", get(profile))"#;
        mark(&mut rows, haystack);
        assert!(rows[0].done, "/profile trouvée");
        assert!(!rows[1].done, "/settings/sessions absente");
    }

    #[test]
    fn mark_evite_les_faux_positifs_de_prefixe() {
        // "/settings" ne doit PAS matcher "/settings/sessions"
        let mut rows = vec![row("/settings")];
        let haystack = r#".route("/settings/sessions", get(h))"#;
        mark(&mut rows, haystack);
        assert!(!rows[0].done);
    }

    #[test]
    fn collect_sources_ignore_target_et_vendored() {
        let base = std::env::temp_dir().join(format!("scrsel_{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&base);
        std::fs::create_dir_all(base.join("src")).unwrap();
        std::fs::create_dir_all(base.join("crates/rustwarden-core/src")).unwrap();
        std::fs::create_dir_all(base.join("target")).unwrap();
        std::fs::write(base.join("src/app.rs"), r#".route("/mine", get(h))"#).unwrap();
        std::fs::write(
            base.join("crates/rustwarden-core/src/x.rs"),
            r#".route("/vendored", get(h))"#,
        )
        .unwrap();
        std::fs::write(base.join("target/y.rs"), r#".route("/built", get(h))"#).unwrap();

        let hay = collect_sources(&base);
        assert!(hay.contains("/mine"));
        assert!(!hay.contains("/vendored"));
        assert!(!hay.contains("/built"));
        let _ = std::fs::remove_dir_all(&base);
    }
}