rmux 0.10.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

static NEXT_PROJECT_ID: AtomicU64 = AtomicU64::new(0);

pub(crate) struct CompileFailCase {
    name: &'static str,
    source: String,
    error_code: &'static str,
    diagnostic: &'static str,
}

impl CompileFailCase {
    #[allow(dead_code)]
    pub(crate) fn enum_match(name: &'static str, source: impl Into<String>) -> Self {
        Self {
            name,
            source: source.into(),
            error_code: "error[E0004]",
            diagnostic: "is marked as non-exhaustive",
        }
    }

    #[allow(dead_code)]
    pub(crate) fn struct_literal(name: &'static str, source: impl Into<String>) -> Self {
        Self {
            name,
            source: source.into(),
            error_code: "error[E0639]",
            diagnostic: "non-exhaustive struct",
        }
    }
}

pub(crate) fn assert_compile_fail_cases(
    dependency: &str,
    manifest_dir: &Path,
    cases: &[CompileFailCase],
) {
    let project = ConsumerProject::new(dependency, manifest_dir, cases);
    let cargo_executable = std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"));

    for case in cases {
        let output = Command::new(&cargo_executable)
            .args([
                "check",
                "--offline",
                "--quiet",
                "--color=never",
                "--bin",
                case.name,
            ])
            .current_dir(&project.root)
            .env("CARGO_TARGET_DIR", project.root.join("target"))
            .env("CARGO_NET_OFFLINE", "true")
            .output()
            .unwrap_or_else(|error| {
                panic!(
                    "failed to launch cargo for compile-fail case `{}`: {error}",
                    case.name
                )
            });
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "consumer case `{}` compiled; its #[non_exhaustive] contract may have disappeared",
            case.name
        );
        assert!(
            stderr.contains(case.error_code) && stderr.contains(case.diagnostic),
            "consumer case `{}` failed for the wrong reason:\n{stderr}",
            case.name
        );
    }
}

struct ConsumerProject {
    root: PathBuf,
}

impl ConsumerProject {
    fn new(dependency: &str, manifest_dir: &Path, cases: &[CompileFailCase]) -> Self {
        let project_id = NEXT_PROJECT_ID.fetch_add(1, Ordering::Relaxed);
        let root = std::env::temp_dir().join(format!(
            "{dependency}-public-api-compile-fail-{}-{project_id}",
            std::process::id()
        ));
        let bin_dir = root.join("src").join("bin");
        fs::create_dir_all(&bin_dir).unwrap_or_else(|error| {
            panic!(
                "failed to create compile-fail project at {}: {error}",
                root.display()
            )
        });

        let dependency_path = toml_string(manifest_dir);
        let manifest = format!(
            "[package]\n\
             name = \"{dependency}-public-api-compile-fail\"\n\
             version = \"0.0.0\"\n\
             edition = \"2021\"\n\
             publish = false\n\
             \n\
             [workspace]\n\
             \n\
             [dependencies]\n\
             {dependency} = {{ path = \"{dependency_path}\" }}\n"
        );
        fs::write(root.join("Cargo.toml"), manifest).unwrap_or_else(|error| {
            panic!(
                "failed to write compile-fail manifest at {}: {error}",
                root.display()
            )
        });
        for case in cases {
            fs::write(bin_dir.join(format!("{}.rs", case.name)), &case.source).unwrap_or_else(
                |error| {
                    panic!(
                        "failed to write compile-fail case `{}` at {}: {error}",
                        case.name,
                        root.display()
                    )
                },
            );
        }

        Self { root }
    }
}

impl Drop for ConsumerProject {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

fn toml_string(path: &Path) -> String {
    path.to_string_lossy()
        .replace('\\', "\\\\")
        .replace('"', "\\\"")
}