jao 0.3.4

Discover and run workspace scripts from a simple CLI
use std::path::{Path, PathBuf};

use assert_cmd::Command;
use assert_fs::TempDir;
use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild};
use predicates::str::contains;

#[test]
fn dynamic_completion_follows_jaofolder_and_prefixes() {
    let workspace = TempDir::new().unwrap();
    workspace
        .child(Path::new("myapp").join(".jaofolder"))
        .touch()
        .unwrap();
    workspace
        .child(
            Path::new("myapp")
                .join("frontend")
                .join(".jaofolder"),
        )
        .touch()
        .unwrap();
    workspace
        .child(
            Path::new("myapp")
                .join("backend")
                .join(".jaofolder"),
        )
        .touch()
        .unwrap();
    workspace
        .child(script_rel_path(&["myapp", "frontend", "scripts"], "dev"))
        .write_str(script_contents())
        .unwrap();
    workspace
        .child(script_rel_path(&["myapp", "backend", "scripts"], "build"))
        .write_str(script_contents())
        .unwrap();

    assert_eq!(completion_lines(workspace.path(), 0, &["m"]), vec![String::from("myapp")]);
    assert_eq!(
        completion_lines(workspace.path(), 1, &["myapp", ""]),
        vec![String::from("backend"), String::from("frontend")]
    );
    assert_eq!(
        completion_lines(workspace.path(), 2, &["myapp", "backend", "b"]),
        vec![String::from("build")]
    );
    assert_eq!(
        completion_lines(
            workspace
                .child("myapp")
                .path(),
            0,
            &[""]
        ),
        vec![String::from("backend"), String::from("frontend")]
    );
}

#[test]
fn dynamic_completion_respects_recursive_jaoignore() {
    let workspace = TempDir::new().unwrap();
    workspace
        .child(".jaoignore")
        .write_str("scratch/\n")
        .unwrap();
    workspace
        .child(Path::new("myapp").join(".jaofolder"))
        .touch()
        .unwrap();
    workspace
        .child(
            Path::new("myapp")
                .join("backend")
                .join(".jaofolder"),
        )
        .touch()
        .unwrap();
    workspace
        .child(
            Path::new("myapp")
                .join("backend")
                .join(".jaoignore"),
        )
        .write_str(format!("seed.dev.{}\n", script_extension()).as_str())
        .unwrap();
    workspace
        .child(script_rel_path(&["myapp", "backend", "scripts"], "migrate.dev"))
        .write_str(script_contents())
        .unwrap();
    workspace
        .child(script_rel_path(&["myapp", "backend", "scripts"], "seed.dev"))
        .write_str(script_contents())
        .unwrap();
    workspace
        .child(script_rel_path(&["scratch", "scripts"], "one-off-fix"))
        .write_str(script_contents())
        .unwrap();

    let root_completions = completion_lines(workspace.path(), 0, &[""]);
    assert_eq!(root_completions, vec![String::from("myapp")]);

    let backend_completions = completion_lines(workspace.path(), 2, &["myapp", "backend", ""]);
    assert_eq!(backend_completions, vec![String::from("migrate")]);
}

#[test]
fn dynamic_completion_supports_options_and_fingerprint_context() {
    let workspace = TempDir::new().unwrap();
    let script_name = "completion-fingerprint-only";
    workspace
        .child(script_rel_path(&["scripts"], script_name))
        .write_str(script_contents())
        .unwrap();

    assert_eq!(
        completion_lines(workspace.path(), 0, &["--c"]),
        vec![String::from("--ci"), String::from("--completions")]
    );
    assert_eq!(completion_lines(workspace.path(), 1, &["--completions", "b"]), vec![String::from("bash")]);
    assert_eq!(
        completion_lines(workspace.path(), 1, &["--fingerprint", ""]),
        vec![script_name.to_string()]
    );
    assert!(completion_lines(workspace.path(), 2, &["--ci", "--require-fingerprint", "deadbeef"],).is_empty());
    assert_eq!(
        completion_lines(workspace.path(), 3, &["--ci", "--require-fingerprint", "deadbeef", ""],),
        vec![script_name.to_string()]
    );
}

#[test]
fn completion_scripts_invoke_internal_protocol() {
    let workspace = TempDir::new().unwrap();

    command_for(workspace.path())
        .args(["--completions", "bash"])
        .assert()
        .success()
        .stdout(contains("jao __complete --index"));

    command_for(workspace.path())
        .args(["--completions", "zsh"])
        .assert()
        .success()
        .stdout(contains("jao __complete --index"));
}

fn completion_lines(cwd: &Path, current_index: usize, words: &[&str]) -> Vec<String> {
    let mut command = command_for(cwd);
    command
        .arg("__complete")
        .arg("--index")
        .arg(current_index.to_string())
        .arg("--")
        .args(words);
    let output = command
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    String::from_utf8(output)
        .unwrap()
        .lines()
        .map(str::to_string)
        .collect()
}

fn command_for(cwd: &Path) -> Command {
    let mut command = Command::cargo_bin("jao").unwrap();
    command.current_dir(cwd);
    command
}

fn script_rel_path(directories: &[&str], stem: &str) -> PathBuf {
    let mut path = PathBuf::new();
    for directory in directories {
        path.push(directory);
    }
    path.push(format!("{stem}.{}", script_extension()));
    path
}

fn script_extension() -> &'static str {
    #[cfg(windows)]
    return "bat";
    #[cfg(unix)]
    return "sh";
}

fn script_contents() -> &'static str {
    #[cfg(windows)]
    return "@echo off\r\n";
    #[cfg(unix)]
    return "#!/bin/sh\n";
}