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, 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
39pub 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 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
88pub 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 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}