cargo-focus 0.1.0

A Cargo subcommand for managing `workspace.default-members`
use assert_cmd::Command;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command as StdCommand;
use tempfile::TempDir;

pub struct WorkspaceFixture {
    _temp_dir: TempDir,
    manifest_path: PathBuf,
}

impl WorkspaceFixture {
    pub fn manifest_path(&self) -> &Path {
        &self.manifest_path
    }
}

pub fn cargo_focus() -> Command {
    assert_cmd::cargo::cargo_bin_cmd!("cargo-focus")
}

pub fn cargo_focus_for_manifest(fixture: &WorkspaceFixture) -> Command {
    let mut cmd = cargo_focus();
    cmd.arg("--manifest-path")
        .arg(fixture.manifest_path().to_str().expect("manifest path"));
    cmd
}

pub fn create_workspace_fixture(with_presets: bool) -> WorkspaceFixture {
    create_workspace_fixture_with_layout(
        with_presets,
        &[
            ("foo", "foo"),
            ("bar", "bar"),
            ("baz", "baz"),
            ("ambiguous", "ambiguous"),
        ],
    )
}

#[allow(dead_code)]
pub fn create_nested_workspace_fixture(with_presets: bool) -> WorkspaceFixture {
    create_workspace_fixture_with_layout(
        with_presets,
        &[
            ("foo", "foo"),
            ("bar", "crates/bar"),
            ("baz", "crates/baz"),
            ("ambiguous", "ambiguous"),
        ],
    )
}

#[allow(dead_code)]
pub fn create_mismatched_member_path_fixture(with_presets: bool) -> WorkspaceFixture {
    create_workspace_fixture_with_layout(
        with_presets,
        &[
            ("foo", "foo"),
            ("sp-core", "substrate/primitives/core"),
            ("bar", "bar"),
        ],
    )
}

fn create_workspace_fixture_with_layout(
    with_presets: bool,
    members: &[(&str, &str)],
) -> WorkspaceFixture {
    let temp_dir = TempDir::new().expect("temp dir");
    let root = temp_dir.path().to_path_buf();

    write_file(
        &root.join("Cargo.toml"),
        &workspace_manifest(with_presets, members),
    );
    for (name, path) in members {
        write_member_crate(&root, name, path);
    }
    generate_lockfile(&root.join("Cargo.toml"));

    WorkspaceFixture {
        _temp_dir: temp_dir,
        manifest_path: root.join("Cargo.toml"),
    }
}

fn generate_lockfile(manifest_path: &Path) {
    let status = StdCommand::new("cargo")
        .args(["generate-lockfile", "--manifest-path"])
        .arg(manifest_path)
        .status()
        .expect("run cargo generate-lockfile");
    assert!(status.success(), "cargo generate-lockfile failed");
}

fn workspace_manifest(with_presets: bool, members: &[(&str, &str)]) -> String {
    let members_list = members
        .iter()
        .map(|(_, path)| format!("\"{path}\""))
        .collect::<Vec<_>>()
        .join(", ");
    let default_member = members.first().map(|(_, path)| *path).unwrap_or("foo");
    let mut manifest = format!(
        "[workspace]\nmembers = [{members_list}]\ndefault-members = [\"{default_member}\"]\nresolver = \"3\"\n"
    );

    if with_presets {
        manifest.push_str(
            r#"
[workspace.metadata.cargo-focus]
schema = "1"

[workspace.metadata.cargo-focus.presets.dev]
description = "dev preset"
members = ["bar", "baz"]

[workspace.metadata.cargo-focus.presets.ambiguous]
description = "name collides with crate"
members = ["bar"]
"#,
        );
    }

    manifest
}

fn write_member_crate(root: &Path, name: &str, rel_path: &str) {
    write_file(
        &root.join(rel_path).join("Cargo.toml"),
        &format!(
            r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"
"#
        ),
    );
    write_file(
        &root.join(rel_path).join("src").join("lib.rs"),
        "pub fn marker() {}\n",
    );
}

fn write_file(path: &Path, content: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent dirs");
    }
    fs::write(path, content).expect("write file");
}