Skip to main content

aether_lspd/
testing.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use tempfile::TempDir;
5
6use crate::uri::path_to_uri;
7
8#[doc = include_str!("docs/testing.md")]
9pub trait TestProject {
10    fn root(&self) -> &Path;
11
12    fn add_file(&self, relative_path: &str, content: &str) -> Result<PathBuf, TestProjectError> {
13        let path = self.root().join(relative_path);
14        if let Some(parent) = path.parent() {
15            fs::create_dir_all(parent)?;
16        }
17        fs::write(&path, content)?;
18        Ok(path)
19    }
20
21    fn file_uri(&self, relative_path: &str) -> lsp_types::Uri {
22        path_to_uri(&self.root().join(relative_path)).expect("Invalid file path")
23    }
24
25    fn file_path_str(&self, relative_path: &str) -> String {
26        self.root().join(relative_path).to_str().expect("Non-UTF8 path").to_string()
27    }
28}
29
30/// Error type for test project operations.
31#[derive(Debug, thiserror::Error)]
32pub enum TestProjectError {
33    #[error("IO error: {0}")]
34    Io(#[from] std::io::Error),
35    #[error("Command '{command}' failed:\n{stderr}")]
36    CommandFailed { command: String, stderr: String },
37}
38
39/// A temporary Cargo project for testing.
40pub struct CargoProject {
41    temp_dir: TempDir,
42}
43
44impl TestProject for CargoProject {
45    fn root(&self) -> &Path {
46        self.temp_dir.path()
47    }
48}
49
50impl CargoProject {
51    /// Create a new minimal Cargo project.
52    pub fn new(name: &str) -> Result<Self, TestProjectError> {
53        let temp_dir = TempDir::new()?;
54        let project = Self { temp_dir };
55        project.init_cargo_toml(name)?;
56        project.init_src_dir()?;
57        Ok(project)
58    }
59
60    fn init_cargo_toml(&self, name: &str) -> Result<(), TestProjectError> {
61        let content = format!(
62            r#"[package]
63name = "{name}"
64version = "0.1.0"
65edition = "2021"
66"#
67        );
68        fs::write(self.root().join("Cargo.toml"), content)?;
69        Ok(())
70    }
71
72    fn init_src_dir(&self) -> Result<(), TestProjectError> {
73        let src_dir = self.root().join("src");
74        fs::create_dir_all(&src_dir)?;
75
76        let main_content = r#"fn main() {
77    println!("Hello, world!");
78}
79"#;
80        fs::write(src_dir.join("main.rs"), main_content)?;
81        Ok(())
82    }
83}
84
85const TYPESCRIPT_PACKAGE: &str = "typescript@6.0.3";
86const TYPESCRIPT_LANGUAGE_SERVER_PACKAGE: &str = "typescript-language-server@5.2.0";
87
88/// A temporary Node.js/TypeScript project for testing.
89pub struct NodeProject {
90    temp_dir: TempDir,
91}
92
93impl TestProject for NodeProject {
94    fn root(&self) -> &Path {
95        self.temp_dir.path()
96    }
97}
98
99impl NodeProject {
100    /// Create a new minimal Node/TypeScript project.
101    ///
102    /// Installs pinned TypeScript tooling locally so tests do not depend on global Node tools.
103    pub fn new(name: &str) -> Result<Self, TestProjectError> {
104        let temp_dir = TempDir::new()?;
105        let project = Self { temp_dir };
106        project.init_package_json(name)?;
107        project.init_tsconfig()?;
108        project.init_src_dir()?;
109        project.install_typescript()?;
110        Ok(project)
111    }
112
113    fn init_package_json(&self, name: &str) -> Result<(), TestProjectError> {
114        let content = format!(
115            r#"{{
116  "name": "{name}",
117  "version": "0.1.0"
118}}"#
119        );
120        fs::write(self.root().join("package.json"), content)?;
121        Ok(())
122    }
123
124    fn init_tsconfig(&self) -> Result<(), TestProjectError> {
125        let content = r#"{
126  "compilerOptions": {
127    "strict": true,
128    "noEmit": true
129  }
130}"#;
131        fs::write(self.root().join("tsconfig.json"), content)?;
132        Ok(())
133    }
134
135    fn init_src_dir(&self) -> Result<(), TestProjectError> {
136        let src_dir = self.root().join("src");
137        fs::create_dir_all(&src_dir)?;
138        fs::write(src_dir.join("index.ts"), "")?;
139        Ok(())
140    }
141
142    fn install_typescript(&self) -> Result<(), TestProjectError> {
143        let args = ["install", "--save-dev", TYPESCRIPT_PACKAGE, TYPESCRIPT_LANGUAGE_SERVER_PACKAGE];
144        let output = Command::new("npm").args(args).current_dir(self.root()).output()?;
145
146        if !output.status.success() {
147            return Err(TestProjectError::CommandFailed {
148                command: format!("npm {}", args.join(" ")),
149
150                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
151            });
152        }
153        Ok(())
154    }
155}