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)]
32pub enum TestProjectError {
33    Io(std::io::Error),
34    CommandFailed { command: String, stderr: String },
35}
36
37impl From<std::io::Error> for TestProjectError {
38    fn from(e: std::io::Error) -> Self {
39        TestProjectError::Io(e)
40    }
41}
42
43impl std::fmt::Display for TestProjectError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            TestProjectError::Io(e) => write!(f, "IO error: {e}"),
47            TestProjectError::CommandFailed { command, stderr } => {
48                write!(f, "Command '{command}' failed:\n{stderr}")
49            }
50        }
51    }
52}
53
54impl std::error::Error for TestProjectError {}
55
56/// A temporary Cargo project for testing.
57pub struct CargoProject {
58    temp_dir: TempDir,
59}
60
61impl TestProject for CargoProject {
62    fn root(&self) -> &Path {
63        self.temp_dir.path()
64    }
65}
66
67impl CargoProject {
68    /// Create a new minimal Cargo project.
69    pub fn new(name: &str) -> Result<Self, TestProjectError> {
70        let temp_dir = TempDir::new()?;
71        let project = Self { temp_dir };
72        project.init_cargo_toml(name)?;
73        project.init_src_dir()?;
74        Ok(project)
75    }
76
77    fn init_cargo_toml(&self, name: &str) -> Result<(), TestProjectError> {
78        let content = format!(
79            r#"[package]
80name = "{name}"
81version = "0.1.0"
82edition = "2021"
83"#
84        );
85        fs::write(self.root().join("Cargo.toml"), content)?;
86        Ok(())
87    }
88
89    fn init_src_dir(&self) -> Result<(), TestProjectError> {
90        let src_dir = self.root().join("src");
91        fs::create_dir_all(&src_dir)?;
92
93        let main_content = r#"fn main() {
94    println!("Hello, world!");
95}
96"#;
97        fs::write(src_dir.join("main.rs"), main_content)?;
98        Ok(())
99    }
100}
101
102/// A temporary Node.js/TypeScript project for testing.
103pub struct NodeProject {
104    temp_dir: TempDir,
105}
106
107impl TestProject for NodeProject {
108    fn root(&self) -> &Path {
109        self.temp_dir.path()
110    }
111}
112
113impl NodeProject {
114    /// Create a new minimal Node/TypeScript project.
115    ///
116    /// Runs `npm install typescript` so typescript-language-server can find tsserver.
117    pub fn new(name: &str) -> Result<Self, TestProjectError> {
118        let temp_dir = TempDir::new()?;
119        let project = Self { temp_dir };
120        project.init_package_json(name)?;
121        project.init_tsconfig()?;
122        project.init_src_dir()?;
123        project.install_typescript()?;
124        Ok(project)
125    }
126
127    fn init_package_json(&self, name: &str) -> Result<(), TestProjectError> {
128        let content = format!(
129            r#"{{
130  "name": "{name}",
131  "version": "0.1.0"
132}}"#
133        );
134        fs::write(self.root().join("package.json"), content)?;
135        Ok(())
136    }
137
138    fn init_tsconfig(&self) -> Result<(), TestProjectError> {
139        let content = r#"{
140  "compilerOptions": {
141    "strict": true,
142    "noEmit": true
143  }
144}"#;
145        fs::write(self.root().join("tsconfig.json"), content)?;
146        Ok(())
147    }
148
149    fn init_src_dir(&self) -> Result<(), TestProjectError> {
150        let src_dir = self.root().join("src");
151        fs::create_dir_all(&src_dir)?;
152        fs::write(src_dir.join("index.ts"), "")?;
153        Ok(())
154    }
155
156    fn install_typescript(&self) -> Result<(), TestProjectError> {
157        let output =
158            Command::new("npm").args(["install", "--save-dev", "typescript"]).current_dir(self.root()).output()?;
159
160        if !output.status.success() {
161            return Err(TestProjectError::CommandFailed {
162                command: "npm install --save-dev typescript".to_string(),
163                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
164            });
165        }
166        Ok(())
167    }
168}