vmn 0.1.2

Fast local Python virtual environment manager with zsh/fzf integration
Documentation
use std::fs;
use std::path::Path;
use std::process::Command;

use assert_cmd::prelude::*;
use predicates::prelude::*;
use tempfile::TempDir;

struct TestState {
    _temp: TempDir,
    config_dir: std::path::PathBuf,
    data_dir: std::path::PathBuf,
}

impl TestState {
    fn new() -> Self {
        let temp = tempfile::tempdir().unwrap();
        let config_dir = temp.path().join("config");
        let data_dir = temp.path().join("data");
        Self {
            _temp: temp,
            config_dir,
            data_dir,
        }
    }

    fn cmd(&self) -> Command {
        let mut cmd = Command::cargo_bin("vmn").unwrap();
        cmd.env("VMN_CONFIG_DIR", &self.config_dir)
            .env("VMN_DATA_DIR", &self.data_dir);
        cmd
    }
}

fn fake_env(path: &Path) {
    fs::create_dir_all(path.join("bin")).unwrap();
    fs::write(path.join("pyvenv.cfg"), "home = /usr/bin\n").unwrap();
    fs::write(
        path.join("bin").join("activate"),
        format!(
            "VIRTUAL_ENV='{}'\nexport VIRTUAL_ENV\nPATH=\"$VIRTUAL_ENV/bin:$PATH\"\nexport PATH\n",
            path.display()
        ),
    )
    .unwrap();
}

fn current_python_major_minor() -> String {
    let output = Command::new("python3").arg("--version").output().unwrap();
    assert!(output.status.success());
    let version = String::from_utf8_lossy(&output.stdout);
    let version = version.trim().strip_prefix("Python ").unwrap();
    let mut parts = version.split('.');
    format!("{}.{}", parts.next().unwrap(), parts.next().unwrap())
}

#[test]
fn init_prints_zsh_snippet_and_creates_state() {
    let state = TestState::new();

    state
        .cmd()
        .args(["init", "--shell", "zsh"])
        .assert()
        .success()
        .stdout(predicate::str::contains("VMN zsh integration"))
        .stdout(predicate::str::contains("vmn activate <name-or-id>"))
        .stdout(predicate::str::contains("vmn list --fzf"));

    assert!(state.config_dir.join("config.toml").is_file());
    assert!(state.data_dir.join("vmn.db").is_file());
}

#[test]
fn init_prints_bash_snippet() {
    let state = TestState::new();

    state
        .cmd()
        .args(["init", "--shell", "bash"])
        .assert()
        .success()
        .stdout(predicate::str::contains("VMN bash integration"))
        .stdout(predicate::str::contains("bind -x"))
        .stdout(predicate::str::contains("vmn activate <name-or-id>"))
        .stdout(predicate::str::contains("vmn list --fzf"));
}

#[test]
fn pythons_lists_interpreters() {
    let state = TestState::new();

    state
        .cmd()
        .args(["pythons", "--json"])
        .assert()
        .success()
        .stdout(predicate::str::contains("python"));
}

#[test]
fn scan_lists_and_activates_fake_project_env() {
    let state = TestState::new();
    let project = state._temp.path().join("project");
    fake_env(&project.join(".venv"));

    state
        .cmd()
        .arg("scan")
        .arg(&project)
        .assert()
        .success()
        .stdout(predicate::str::contains("registered 1 environment"));

    state
        .cmd()
        .args(["list", "--fzf"])
        .assert()
        .success()
        .stdout(predicate::str::contains("project"))
        .stdout(predicate::str::contains("active"));

    state
        .cmd()
        .args(["activate-path", "project"])
        .assert()
        .success()
        .stdout(predicate::str::contains(".venv/bin/activate"));

    state
        .cmd()
        .args(["project-dir", "project"])
        .assert()
        .success()
        .stdout(predicate::str::contains(project.to_string_lossy().as_ref()));

    state
        .cmd()
        .args(["activate", "project"])
        .env("SHELL", "/bin/sh")
        .env("VMN_ACTIVATE_TEST_NO_EXEC", "1")
        .assert()
        .success()
        .stdout(predicate::str::contains(
            project.join(".venv").to_string_lossy().as_ref(),
        ))
        .stdout(predicate::str::contains(project.to_string_lossy().as_ref()));
}

#[test]
fn create_info_refresh_and_remove_managed_env() {
    let state = TestState::new();
    let python_version = current_python_major_minor();

    let output = state
        .cmd()
        .args(["create", "demo", "--python", &python_version])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let activate_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
    assert!(activate_path.ends_with("/bin/activate"));
    assert!(Path::new(&activate_path).is_file());

    state
        .cmd()
        .args(["info", "demo"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Name: demo"))
        .stdout(predicate::str::contains("Python: Python"));

    state
        .cmd()
        .args(["refresh", "demo"])
        .assert()
        .success()
        .stdout(predicate::str::contains("refreshed 1 environment"));

    state
        .cmd()
        .args(["remove", "demo", "--delete-files", "--yes"])
        .assert()
        .success()
        .stdout(predicate::str::contains("removed"));

    assert!(!Path::new(&activate_path).exists());
}