carte 0.6.0

Runs your apps' processes on demand behind *.localhost hostnames, one stack per git worktree
Documentation
#![allow(dead_code)]

use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;

pub fn fixture() -> &'static Path {
    static PATH: OnceLock<PathBuf> = OnceLock::new();
    PATH.get_or_init(|| {
        let exe = std::env::current_exe().unwrap();
        let dir = exe.parent().unwrap().parent().unwrap();
        match std::env::var("NEXTEST_RUN_ID") {
            Ok(run) => build_fixture_once(dir, &run),
            Err(_) => build_fixture(),
        }
        dir.join("fixture-app")
    })
}

fn build_fixture() {
    let status = Command::new(env!("CARGO"))
        .args(["build", "--quiet", "-p", "fixture-app"])
        .status()
        .expect("cargo build fixture-app");
    assert!(status.success());
}

/// Builds the fixture app once per nextest run, which starts a process for each
/// test: every `cargo build` replaces the binary, and macOS kills a process that
/// starts it meanwhile.
fn build_fixture_once(dir: &Path, run: &str) {
    let lock = std::fs::File::create(dir.join("fixture-app.lock")).unwrap();
    lock.lock().unwrap();
    let built = dir.join("fixture-app.run");
    if std::fs::read_to_string(&built).ok().as_deref() != Some(run) {
        build_fixture();
        std::fs::write(&built, run).unwrap();
    }
}

/// The process fixture apps exit with, as `FIXTURE_LIFELINE_PID`: the test
/// process, or the one named in its own env.
pub fn lifeline() -> String {
    std::env::var("FIXTURE_LIFELINE_PID").unwrap_or_else(|_| std::process::id().to_string())
}

/// SIGKILLs `pid` if the lifeline exits first.
pub fn guard(pid: u32) {
    let mut guard = Command::new(fixture())
        .env_clear()
        .env("FIXTURE_LIFELINE_PID", lifeline())
        .env("FIXTURE_GUARD_PID", pid.to_string())
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn guard");
    std::thread::spawn(move || guard.wait());
}

pub fn git(dir: &Path, args: &[&str]) {
    let status = Command::new("git")
        .args(args)
        .current_dir(dir)
        .env("GIT_AUTHOR_NAME", "carte")
        .env("GIT_AUTHOR_EMAIL", "carte@example.com")
        .env("GIT_COMMITTER_NAME", "carte")
        .env("GIT_COMMITTER_EMAIL", "carte@example.com")
        .status()
        .unwrap();
    assert!(status.success(), "git {args:?}");
}

/// A git repo with a committed Procfile.dev.
pub fn repo(dir: &Path, procfile: &str) {
    std::fs::create_dir_all(dir).unwrap();
    git(dir, &["init", "-q", "-b", "main"]);
    std::fs::write(dir.join("Procfile.dev"), procfile).unwrap();
    git(dir, &["add", "."]);
    git(dir, &["commit", "-q", "-m", "init"]);
}

/// A plain HTTP/1.0 GET against loopback, returning the body.
pub fn http_get(port: u16, path: &str) -> String {
    let mut stream = TcpStream::connect(("127.0.0.1", port))
        .or_else(|_| TcpStream::connect(("::1", port)))
        .unwrap();
    write!(stream, "GET {path} HTTP/1.0\r\nHost: localhost\r\n\r\n").unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    response
        .split_once("\r\n\r\n")
        .map(|(_, body)| body.to_string())
        .unwrap_or_default()
}