use std::path::Path;
use std::process::{Command, Output};
fn mnemo(home: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mnemo"));
cmd.env("HOME", home)
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("XDG_DATA_HOME", home.join(".local/share"));
cmd
}
fn run(home: &Path, args: &[&str]) -> Output {
mnemo(home).args(args).output().unwrap()
}
fn stdout(out: &Output) -> String {
String::from_utf8(out.stdout.clone()).unwrap()
}
fn init_git_repo(dir: &Path, branch: &str) -> bool {
let ok = |o: &Output| o.status.success();
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
};
match git(&["init", "-b", branch]) {
Ok(o) if ok(&o) => {}
_ => return false,
}
let _ = git(&["config", "user.email", "test@example.com"]);
let _ = git(&["config", "user.name", "Test"]);
let _ = git(&[
"remote",
"add",
"origin",
"https://example.com/acme/demo.git",
]);
true
}
#[test]
fn migrate_sur_base_neuve_est_idempotent() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
let out = run(home, &["migrate"]);
assert!(out.status.success());
let s = stdout(&out);
assert!(s.contains("Schéma"), "sortie migrate inattendue : {s:?}");
let out2 = run(home, &["migrate"]);
assert!(out2.status.success());
assert!(stdout(&out2).contains("déjà à jour"));
}
#[test]
fn doctor_affiche_la_version_de_schema() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
let out = run(home, &["doctor"]);
assert_eq!(out.status.code(), Some(0));
let s = stdout(&out);
assert!(
s.contains("Schéma SQLite"),
"doctor doit afficher le schéma : {s:?}"
);
assert!(s.contains("v2"), "doctor doit afficher la version : {s:?}");
}
#[test]
fn add_hors_git_fonctionne() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
let add = run(home, &["add", "--cmd", "echo hors-git", "--cwd", "/tmp"]);
assert!(add.status.success());
let out = run(home, &["search", "hors-git", "--print"]);
assert!(stdout(&out).contains("echo hors-git"));
}
#[test]
fn add_dans_git_remplit_le_contexte() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let repo = home.join("workspace").join("demo");
std::fs::create_dir_all(&repo).unwrap();
if !init_git_repo(&repo, "main") {
eprintln!("git indisponible : test ignoré");
return;
}
assert!(run(home, &["init"]).status.success());
let repo_str = repo.to_str().unwrap();
let add = run(
home,
&["add", "--cmd", "cargo build --release", "--cwd", repo_str],
);
assert!(add.status.success());
let by_project = run(home, &["search", "cargo", "--print", "--project", "demo"]);
assert!(
stdout(&by_project).contains("cargo build --release"),
"filtre --project doit retrouver la commande : {:?}",
stdout(&by_project)
);
let by_branch = run(home, &["search", "cargo", "--print", "--branch", "main"]);
assert!(
stdout(&by_branch).contains("cargo build --release"),
"filtre --branch doit retrouver la commande : {:?}",
stdout(&by_branch)
);
let none = run(
home,
&["search", "cargo", "--print", "--project", "inexistant"],
);
assert!(!stdout(&none).contains("cargo build"));
}
#[test]
fn stats_sur_base_vide_et_remplie() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
let empty = run(home, &["stats"]);
assert!(empty.status.success());
let es = stdout(&empty);
assert!(es.contains("Commandes enregistrées : 0"));
for args in [
vec!["add", "--cmd", "cargo build", "--cwd", "/tmp"],
vec![
"add",
"--cmd",
"cargo test",
"--cwd",
"/tmp",
"--exit-code",
"1",
],
vec!["add", "--cmd", "git status", "--cwd", "/tmp"],
] {
assert!(run(home, &args).status.success());
}
let filled = run(home, &["stats"]);
assert!(filled.status.success());
let fs = stdout(&filled);
assert!(fs.contains("Commandes enregistrées : 3"), "{fs:?}");
assert!(fs.contains("Commandes en échec : 1"), "{fs:?}");
assert!(fs.contains("Top commandes"));
assert!(fs.contains("cargo"));
}
#[test]
fn stats_ne_montre_plus_les_tokens_parasites() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
for args in [
vec!["add", "--cmd", "sudo apt update", "--cwd", "/tmp"],
vec![
"add",
"--cmd",
"env RUST_LOG=debug cargo test",
"--cwd",
"/tmp",
],
vec!["add", "--cmd", "/usr/bin/git status", "--cwd", "/tmp"],
] {
assert!(run(home, &args).status.success());
}
let out = run(home, &["stats"]);
assert!(out.status.success());
let s = stdout(&out);
assert!(s.contains("apt"), "{s:?}");
assert!(s.contains("cargo"), "{s:?}");
assert!(s.contains("git"), "{s:?}");
for junk in [" sudo", " env", " |", " -\n", "/usr/bin/git"] {
assert!(
!s.contains(junk),
"token parasite trouvé ({junk:?}) : {s:?}"
);
}
}
#[test]
fn stats_json_est_valide_et_filtre() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let repo = home.join("workspace").join("demo");
std::fs::create_dir_all(&repo).unwrap();
let has_git = init_git_repo(&repo, "main");
assert!(run(home, &["init"]).status.success());
let repo_str = repo.to_str().unwrap();
assert!(
run(home, &["add", "--cmd", "cargo build", "--cwd", repo_str])
.status
.success()
);
let js = run(home, &["stats", "--json"]);
assert!(js.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout(&js)).unwrap();
assert!(v["total_commands"].is_number());
assert!(v["top_commands"].is_array());
assert!(v["filters"].is_object());
if has_git {
let by_proj = run(home, &["stats", "--project", "demo"]);
assert!(by_proj.status.success());
assert!(stdout(&by_proj).contains("cargo"));
let pj = run(home, &["stats", "--project", "demo", "--json"]);
let pv: serde_json::Value = serde_json::from_str(&stdout(&pj)).unwrap();
assert_eq!(pv["filters"]["project"], "demo");
let by_branch = run(home, &["stats", "--branch", "main"]);
assert!(by_branch.status.success());
assert!(stdout(&by_branch).contains("cargo"));
let none = run(home, &["stats", "--project", "inexistant"]);
assert!(none.status.success());
assert!(stdout(&none).contains("Aucune commande trouvée pour ces filtres."));
}
}
#[test]
fn init_cree_la_section_stats() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
let cfg = std::fs::read_to_string(home.join(".config/mnemo/config.toml")).unwrap();
assert!(cfg.contains("[stats]"), "{cfg:?}");
assert!(cfg.contains("ignored_commands"), "{cfg:?}");
}
#[test]
fn config_sans_section_stats_reste_compatible() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
let cfg_dir = home.join(".config/mnemo");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join("config.toml"),
"sensitive_keywords = [\"password\"]\nignore_prefixes = [\"mnemo\"]\nsearch_limit = 5000\n",
)
.unwrap();
let out = run(home, &["config", "stats-ignore", "list"]);
assert!(out.status.success(), "{:?}", out);
assert!(stdout(&out).contains("Aucune commande ignorée configurée."));
}
#[test]
fn config_stats_ignore_add_remove_list() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
let list0 = run(home, &["config", "stats-ignore", "list"]);
assert!(stdout(&list0).contains("Aucune commande ignorée configurée."));
let add = run(home, &["config", "stats-ignore", "add", "create_dir"]);
assert!(add.status.success());
assert!(stdout(&add).contains("Commande ignorée ajoutée : create_dir"));
let add2 = run(home, &["config", "stats-ignore", "add", "create_dir"]);
assert!(stdout(&add2).contains("Commande déjà présente : create_dir"));
let list1 = run(home, &["config", "stats-ignore", "list"]);
assert!(stdout(&list1).contains("create_dir"));
let cfg = std::fs::read_to_string(home.join(".config/mnemo/config.toml")).unwrap();
assert_eq!(cfg.matches("create_dir").count(), 1, "{cfg:?}");
let rm = run(home, &["config", "stats-ignore", "remove", "create_dir"]);
assert!(stdout(&rm).contains("Commande retirée : create_dir"));
let rm2 = run(home, &["config", "stats-ignore", "remove", "create_dir"]);
assert!(stdout(&rm2).contains("Commande absente : create_dir"));
}
#[test]
fn stats_respecte_la_config_ignored_commands() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(run(home, &["init"]).status.success());
for args in [
vec!["add", "--cmd", "create_dir foo", "--cwd", "/tmp"],
vec!["add", "--cmd", "create_dir bar", "--cwd", "/tmp"],
vec!["add", "--cmd", "cargo build", "--cwd", "/tmp"],
] {
assert!(run(home, &args).status.success());
}
assert!(stdout(&run(home, &["stats"])).contains("create_dir"));
assert!(run(home, &["config", "stats-ignore", "add", "create_dir"])
.status
.success());
let out = run(home, &["stats"]);
assert!(out.status.success());
let s = stdout(&out);
assert!(!s.contains(" create_dir"), "{s:?}");
assert!(s.contains("cargo"), "{s:?}");
assert!(s.contains("Commandes enregistrées : 3"), "{s:?}");
assert!(
s.contains("Entrées ignorées dans le Top commandes : 2"),
"{s:?}"
);
let js = run(home, &["stats", "--json"]);
let v: serde_json::Value = serde_json::from_str(&stdout(&js)).unwrap();
assert_eq!(v["ignored_commands_config"][0], "create_dir");
assert_eq!(v["ignored_for_top_commands"], 2);
let doc = run(home, &["doctor"]);
assert!(stdout(&doc).contains("Commandes ignorées dans stats : create_dir"));
}