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#[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
56pub 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 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
102pub 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 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}