ryo-symbol 0.1.0

Symbol system for Rust codebase - unique identifiers and file path management
Documentation
//! Test harness for creating realistic Cargo workspaces in tests.
//!
//! This module provides utilities for creating temporary Cargo workspaces
//! with proper Cargo.toml files and source structure.
//!
//! # Example
//! ```ignore
//! use ryo_symbol::TestWorkspace;
//!
//! let workspace = TestWorkspace::builder()
//!     .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
//!     .build();
//!
//! let path = workspace.resolve("mylib", "src/lib.rs");
//! assert_eq!(path.crate_name().as_str(), "mylib");
//! ```

use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use tempfile::TempDir;

use crate::file_path::WorkspaceFilePath;
use crate::resolver::WorkspacePathResolver;

/// A temporary Cargo workspace for testing.
///
/// Creates a real filesystem structure with Cargo.toml files,
/// allowing tests to work with realistic workspace configurations.
pub struct TestWorkspace {
    /// Temporary directory (dropped when TestWorkspace is dropped)
    _tempdir: TempDir,
    /// Workspace root path
    workspace_root: PathBuf,
    /// Crate name -> relative paths of source files
    crates: HashMap<String, Vec<String>>,
    /// Path resolver
    resolver: WorkspacePathResolver,
}

impl TestWorkspace {
    /// Create a new builder for constructing a test workspace.
    pub fn builder() -> TestWorkspaceBuilder {
        TestWorkspaceBuilder::new()
    }

    /// Get the workspace root path.
    pub fn workspace_root(&self) -> &Path {
        &self.workspace_root
    }

    /// Get the path resolver for this workspace.
    pub fn resolver(&self) -> &WorkspacePathResolver {
        &self.resolver
    }

    /// Resolve a file path within a crate to a WorkspaceFilePath.
    ///
    /// # Arguments
    /// * `crate_name` - The crate name (e.g., "mylib")
    /// * `relative_path` - Path relative to crate root (e.g., "src/lib.rs")
    pub fn resolve(&self, crate_name: &str, relative_path: &str) -> WorkspaceFilePath {
        use crate::crate_name::CrateName;
        // Path relative to workspace root: {crate_name}/{relative_path}
        let workspace_relative = Path::new(crate_name).join(relative_path);
        let crate_name = CrateName::new_unchecked(crate_name);
        self.resolver
            .resolve_relative_with_crate(workspace_relative, crate_name)
    }

    /// Get all WorkspaceFilePaths for a crate.
    pub fn files_in_crate(&self, crate_name: &str) -> Vec<WorkspaceFilePath> {
        self.crates
            .get(crate_name)
            .map(|paths| paths.iter().map(|p| self.resolve(crate_name, p)).collect())
            .unwrap_or_default()
    }
}

/// Builder for creating test workspaces.
pub struct TestWorkspaceBuilder {
    /// Crate definitions: name -> (relative_path -> source_content)
    crates: HashMap<String, HashMap<String, String>>,
}

impl TestWorkspaceBuilder {
    /// Create a new builder.
    pub fn new() -> Self {
        Self {
            crates: HashMap::new(),
        }
    }

    /// Add a crate with a single source file.
    ///
    /// # Arguments
    /// * `crate_name` - The crate name (will be used in Cargo.toml)
    /// * `relative_path` - Path relative to crate root (e.g., "src/lib.rs")
    /// * `source` - The source code content
    pub fn crate_with_source(
        mut self,
        crate_name: &str,
        relative_path: &str,
        source: &str,
    ) -> Self {
        let crate_files = self.crates.entry(crate_name.to_string()).or_default();
        crate_files.insert(relative_path.to_string(), source.to_string());
        self
    }

    /// Add a source file to an existing crate.
    pub fn add_file(mut self, crate_name: &str, relative_path: &str, source: &str) -> Self {
        let crate_files = self.crates.entry(crate_name.to_string()).or_default();
        crate_files.insert(relative_path.to_string(), source.to_string());
        self
    }

    /// Build the test workspace.
    ///
    /// Creates a temporary directory with:
    /// - A workspace Cargo.toml listing all crates as members
    /// - Individual crate directories with their own Cargo.toml
    /// - Source files as specified
    pub fn build(self) -> TestWorkspace {
        let tempdir = TempDir::new().expect("Failed to create temp directory");
        let workspace_root = tempdir.path().to_path_buf();

        // Create workspace Cargo.toml
        let members: Vec<String> = self.crates.keys().map(|s| format!("\"{}\"", s)).collect();
        let workspace_toml = format!(
            r#"[workspace]
members = [{}]
resolver = "2"
"#,
            members.join(", ")
        );
        fs::write(workspace_root.join("Cargo.toml"), workspace_toml)
            .expect("Failed to write workspace Cargo.toml");

        // Track created files
        let mut crate_paths: HashMap<String, Vec<String>> = HashMap::new();

        // Create each crate
        for (crate_name, files) in &self.crates {
            let crate_dir = workspace_root.join(crate_name);
            fs::create_dir_all(&crate_dir).expect("Failed to create crate directory");

            // Create crate Cargo.toml
            let crate_toml = format!(
                r#"[package]
name = "{}"
version = "0.1.0"
edition = "2021"
"#,
                crate_name
            );
            fs::write(crate_dir.join("Cargo.toml"), crate_toml)
                .expect("Failed to write crate Cargo.toml");

            // Create source files
            let mut paths = Vec::new();
            for (relative_path, source) in files {
                let file_path = crate_dir.join(relative_path);
                if let Some(parent) = file_path.parent() {
                    fs::create_dir_all(parent).expect("Failed to create source directory");
                }
                fs::write(&file_path, source).expect("Failed to write source file");
                paths.push(relative_path.clone());
            }
            crate_paths.insert(crate_name.clone(), paths);
        }

        let resolver = WorkspacePathResolver::new(workspace_root.clone());

        TestWorkspace {
            _tempdir: tempdir,
            workspace_root,
            crates: crate_paths,
            resolver,
        }
    }
}

impl Default for TestWorkspaceBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_single_crate_workspace() {
        let workspace = TestWorkspace::builder()
            .crate_with_source("mylib", "src/lib.rs", "pub fn hello() {}")
            .build();

        let path = workspace.resolve("mylib", "src/lib.rs");
        assert_eq!(path.crate_name().as_str(), "mylib");
        assert!(path.as_relative().ends_with("src/lib.rs"));
    }

    #[test]
    fn test_multi_crate_workspace() {
        let workspace = TestWorkspace::builder()
            .crate_with_source("crate_a", "src/lib.rs", "pub mod foo;")
            .add_file("crate_a", "src/foo.rs", "pub fn foo() {}")
            .crate_with_source("crate_b", "src/lib.rs", "pub fn bar() {}")
            .build();

        let path_a = workspace.resolve("crate_a", "src/lib.rs");
        let path_b = workspace.resolve("crate_b", "src/lib.rs");

        assert_eq!(path_a.crate_name().as_str(), "crate_a");
        assert_eq!(path_b.crate_name().as_str(), "crate_b");

        let files_a = workspace.files_in_crate("crate_a");
        assert_eq!(files_a.len(), 2);
    }
}