1mod app;
4mod error;
5mod models;
6mod plugin_source;
7mod registry_source;
8mod service;
9
10pub use app::App;
11pub use error::CoreError;
12pub use models::{
13 CurrentTool, InstalledVersion, MigrateSummary, PluginInfo, SelfUpgradeSummary, UninstallResult,
14 UseScope, VersionInfo,
15};
16pub use vs_installer::ProgressFn;
17
18#[cfg(test)]
19mod tests {
20 #[cfg(feature = "lua")]
21 use std::error::Error;
22 #[cfg(feature = "lua")]
23 use std::fs;
24
25 #[cfg(feature = "lua")]
26 use tempfile::TempDir;
27 #[cfg(feature = "lua")]
28 use vs_config::{
29 AppConfig, CacheConfig, HomeLayout, RegistryConfig, StorageConfig, write_app_config,
30 };
31 #[cfg(feature = "lua")]
32 use vs_plugin_api::PluginBackendKind;
33
34 #[cfg(feature = "lua")]
35 use crate::{App, UseScope};
36
37 #[cfg(feature = "lua")]
38 #[test]
39 fn use_tool_should_write_project_config() -> Result<(), Box<dyn Error>> {
40 let temp_dir = TempDir::new()?;
41 let home = temp_dir.path().join("home");
42 let cwd = temp_dir.path().join("project");
43 fs::create_dir_all(&cwd)?;
44 let app = App::new(
45 HomeLayout {
46 active_home: home,
47 migration_candidates: Vec::new(),
48 },
49 cwd.clone(),
50 Some(String::from("session")),
51 )?;
52
53 let source = temp_dir.path().join("nodejs-lua");
54 write_lua_fixture(&source);
55 app.add_plugin(
56 Some("nodejs"),
57 Some(source.display().to_string()),
58 Some(PluginBackendKind::Lua),
59 None,
60 )?;
61 app.install_plugin_version("nodejs", Some("20.11.1"), None)?;
62
63 app.use_tool("nodejs", "20.11.1", UseScope::Project, false)?;
64
65 let config = fs::read_to_string(cwd.join(".vs.toml"))?;
66 assert!(config.contains("nodejs = \"20.11.1\""));
67 Ok(())
68 }
69
70 #[cfg(feature = "lua")]
71 #[test]
72 fn available_plugins_should_bootstrap_registry_when_source_is_configured()
73 -> Result<(), Box<dyn Error>> {
74 let temp_dir = TempDir::new()?;
75 let home = temp_dir.path().join("home");
76 let cwd = temp_dir.path().join("project");
77 fs::create_dir_all(&cwd)?;
78 let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
79 .join("../../fixtures/registry/index.json");
80
81 write_app_config(
82 &home,
83 &AppConfig {
84 proxy: Default::default(),
85 storage: Default::default(),
86 registry: RegistryConfig {
87 address: registry_path.display().to_string(),
88 },
89 legacy_version_file: Default::default(),
90 cache: Default::default(),
91 },
92 )?;
93
94 let app = App::new(
95 HomeLayout {
96 active_home: home,
97 migration_candidates: Vec::new(),
98 },
99 cwd,
100 Some(String::from("session")),
101 )?;
102
103 let entries = app.available_plugins()?;
104 assert!(!entries.is_empty());
105 assert!(entries.iter().any(|entry| entry.name == "nodejs"));
106 Ok(())
107 }
108
109 #[cfg(feature = "lua")]
110 #[test]
111 fn available_plugins_should_fallback_to_cached_registry_when_refresh_fails()
112 -> Result<(), Box<dyn Error>> {
113 let temp_dir = TempDir::new()?;
114 let home = temp_dir.path().join("home");
115 let cwd = temp_dir.path().join("project");
116 fs::create_dir_all(&cwd)?;
117 let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
118 .join("../../fixtures/registry/index.json");
119
120 write_app_config(
121 &home,
122 &AppConfig {
123 proxy: Default::default(),
124 storage: Default::default(),
125 registry: RegistryConfig {
126 address: registry_path.display().to_string(),
127 },
128 legacy_version_file: Default::default(),
129 cache: Default::default(),
130 },
131 )?;
132
133 let app = App::new(
134 HomeLayout {
135 active_home: home.clone(),
136 migration_candidates: Vec::new(),
137 },
138 cwd.clone(),
139 Some(String::from("session")),
140 )?;
141 assert!(!app.available_plugins()?.is_empty());
142
143 write_app_config(
144 &home,
145 &AppConfig {
146 proxy: Default::default(),
147 storage: Default::default(),
148 registry: RegistryConfig {
149 address: temp_dir
150 .path()
151 .join("missing/index.json")
152 .display()
153 .to_string(),
154 },
155 legacy_version_file: Default::default(),
156 cache: Default::default(),
157 },
158 )?;
159
160 let fallback = App::new(
161 HomeLayout {
162 active_home: home,
163 migration_candidates: Vec::new(),
164 },
165 cwd,
166 Some(String::from("session")),
167 )?;
168 let entries = fallback.available_plugins()?;
169 assert!(entries.iter().any(|entry| entry.name == "nodejs"));
170 Ok(())
171 }
172
173 #[cfg(feature = "lua")]
174 #[test]
175 fn add_plugin_should_fallback_to_cached_registry_when_refresh_fails()
176 -> Result<(), Box<dyn Error>> {
177 let temp_dir = TempDir::new()?;
178 let home = temp_dir.path().join("home");
179 let cwd = temp_dir.path().join("project");
180 fs::create_dir_all(&cwd)?;
181 let registry_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
182 .join("../../fixtures/registry/index.json");
183
184 write_app_config(
185 &home,
186 &AppConfig {
187 proxy: Default::default(),
188 storage: Default::default(),
189 registry: RegistryConfig {
190 address: registry_path.display().to_string(),
191 },
192 legacy_version_file: Default::default(),
193 cache: Default::default(),
194 },
195 )?;
196
197 let app = App::new(
198 HomeLayout {
199 active_home: home.clone(),
200 migration_candidates: Vec::new(),
201 },
202 cwd.clone(),
203 Some(String::from("session")),
204 )?;
205 assert!(!app.available_plugins()?.is_empty());
206
207 write_app_config(
208 &home,
209 &AppConfig {
210 proxy: Default::default(),
211 storage: Default::default(),
212 registry: RegistryConfig {
213 address: temp_dir
214 .path()
215 .join("missing/index.json")
216 .display()
217 .to_string(),
218 },
219 legacy_version_file: Default::default(),
220 cache: Default::default(),
221 },
222 )?;
223
224 let fallback = App::new(
225 HomeLayout {
226 active_home: home,
227 migration_candidates: Vec::new(),
228 },
229 cwd,
230 Some(String::from("session")),
231 )?;
232 let entry = fallback.add_plugin(Some("nodejs"), None, None, None)?;
233 assert_eq!(entry.name, "nodejs");
234 Ok(())
235 }
236
237 #[cfg(feature = "lua")]
238 #[test]
239 fn storage_sdk_path_should_redirect_runtime_installs() -> Result<(), Box<dyn Error>> {
240 let temp_dir = TempDir::new()?;
241 let home = temp_dir.path().join("home");
242 let storage_root = temp_dir.path().join("runtime-root");
243 let cwd = temp_dir.path().join("project");
244 let default_runtime_root = home.join("cache");
245 fs::create_dir_all(&cwd)?;
246
247 write_app_config(
248 &home,
249 &AppConfig {
250 storage: StorageConfig {
251 sdk_path: storage_root.display().to_string(),
252 },
253 ..AppConfig::default()
254 },
255 )?;
256
257 let app = App::new(
258 HomeLayout {
259 active_home: home,
260 migration_candidates: Vec::new(),
261 },
262 cwd,
263 Some(String::from("session")),
264 )?;
265
266 let source = temp_dir.path().join("nodejs-lua");
267 write_lua_fixture(&source);
268 app.add_plugin(
269 Some("nodejs"),
270 Some(source.display().to_string()),
271 Some(PluginBackendKind::Lua),
272 None,
273 )?;
274 let installed = app.install_plugin_version("nodejs", Some("20.11.1"), None)?;
275
276 assert!(installed.install_dir.starts_with(&storage_root));
277 assert!(!installed.install_dir.starts_with(default_runtime_root));
278 Ok(())
279 }
280
281 #[cfg(feature = "lua")]
282 #[test]
283 fn project_tool_version_for_use_should_resolve_legacy_file() -> Result<(), Box<dyn Error>> {
284 let temp_dir = TempDir::new()?;
285 let home = temp_dir.path().join("home");
286 let cwd = temp_dir.path().join("project");
287 fs::create_dir_all(&cwd)?;
288 fs::write(cwd.join(".nvmrc"), "20.11.1\n")?;
289
290 let app = App::new(
291 HomeLayout {
292 active_home: home,
293 migration_candidates: Vec::new(),
294 },
295 cwd,
296 Some(String::from("session")),
297 )?;
298
299 let source = temp_dir.path().join("nodejs-lua");
300 write_lua_fixture(&source);
301 app.add_plugin(
302 Some("nodejs"),
303 Some(source.display().to_string()),
304 Some(PluginBackendKind::Lua),
305 None,
306 )?;
307
308 assert_eq!(
309 app.project_tool_version_for_use("nodejs")?,
310 Some(String::from("20.11.1"))
311 );
312 Ok(())
313 }
314
315 #[cfg(feature = "lua")]
316 #[test]
317 fn legacy_latest_installed_should_pick_the_newest_matching_runtime()
318 -> Result<(), Box<dyn Error>> {
319 let temp_dir = TempDir::new()?;
320 let home = temp_dir.path().join("home");
321 let cwd = temp_dir.path().join("project");
322 fs::create_dir_all(&cwd)?;
323 fs::write(cwd.join(".nvmrc"), "20\n")?;
324 write_app_config(
325 &home,
326 &AppConfig {
327 legacy_version_file: vs_config::LegacyVersionFileConfig {
328 enable: true,
329 strategy: String::from("latest_installed"),
330 },
331 ..AppConfig::default()
332 },
333 )?;
334
335 let app = App::new(
336 HomeLayout {
337 active_home: home,
338 migration_candidates: Vec::new(),
339 },
340 cwd,
341 Some(String::from("session")),
342 )?;
343
344 let source = temp_dir.path().join("nodejs-lua");
345 write_multi_version_lua_fixture(&source)?;
346 app.add_plugin(
347 Some("nodejs"),
348 Some(source.display().to_string()),
349 Some(PluginBackendKind::Lua),
350 None,
351 )?;
352 app.install_plugin_version("nodejs", Some("20.9.0"), None)?;
353 app.install_plugin_version("nodejs", Some("20.11.1"), None)?;
354
355 assert_eq!(
356 app.project_tool_version_for_use("nodejs")?,
357 Some(String::from("20.11.1"))
358 );
359 Ok(())
360 }
361
362 #[cfg(feature = "lua")]
363 #[test]
364 fn available_hook_cache_should_return_cached_versions_when_enabled()
365 -> Result<(), Box<dyn Error>> {
366 let temp_dir = TempDir::new()?;
367 let home = temp_dir.path().join("home");
368 let cwd = temp_dir.path().join("project");
369 fs::create_dir_all(&cwd)?;
370 write_app_config(
371 &home,
372 &AppConfig {
373 cache: CacheConfig {
374 available_hook_duration: String::from("12h"),
375 },
376 ..AppConfig::default()
377 },
378 )?;
379
380 let app = App::new(
381 HomeLayout {
382 active_home: home,
383 migration_candidates: Vec::new(),
384 },
385 cwd,
386 Some(String::from("session")),
387 )?;
388
389 let source = temp_dir.path().join("nodejs-lua");
390 write_multi_version_lua_fixture(&source)?;
391 app.add_plugin(
392 Some("nodejs"),
393 Some(source.display().to_string()),
394 Some(PluginBackendKind::Lua),
395 None,
396 )?;
397
398 let versions = app.search_versions("nodejs", &[])?;
399 fs::remove_dir_all(&source)?;
400 let cached = app.search_versions("nodejs", &[])?;
401
402 assert_eq!(versions, cached);
403 Ok(())
404 }
405
406 #[cfg(feature = "lua")]
407 fn write_lua_fixture(root: &std::path::Path) {
408 if let Err(error) = fs::create_dir_all(root.join("hooks")) {
409 panic!("failed to create hooks directory: {error}");
410 }
411 if let Err(error) = fs::create_dir_all(root.join("packages/20.11.1/bin")) {
412 panic!("failed to create package directory: {error}");
413 }
414 fs::write(
415 root.join("metadata.lua"),
416 "PLUGIN = {}\nPLUGIN.name = 'nodejs'\nPLUGIN.version = '0.1.0'\nPLUGIN.legacyFilenames = { '.nvmrc' }\n",
417 )
418 .unwrap_or_else(|error| panic!("failed to write metadata fixture: {error}"));
419 fs::write(
420 root.join("hooks/available.lua"),
421 "function PLUGIN:Available(ctx)\n return { { version = '20.11.1' } }\nend\n",
422 )
423 .unwrap_or_else(|error| panic!("failed to write available fixture: {error}"));
424 fs::write(
425 root.join("hooks/pre_install.lua"),
426 "function PLUGIN:PreInstall(ctx)\n return { version = '20.11.1', url = 'packages/20.11.1' }\nend\n",
427 )
428 .unwrap_or_else(|error| panic!("failed to write pre_install fixture: {error}"));
429 fs::write(
430 root.join("hooks/env_keys.lua"),
431 "function PLUGIN:EnvKeys(ctx)\n return { { key = 'NODEJS_HOME', value = ctx.path }, { key = 'PATH', value = ctx.path .. '/bin' } }\nend\n",
432 )
433 .unwrap_or_else(|error| panic!("failed to write env_keys fixture: {error}"));
434 }
435
436 #[cfg(feature = "lua")]
437 fn write_multi_version_lua_fixture(root: &std::path::Path) -> Result<(), Box<dyn Error>> {
438 fs::create_dir_all(root.join("hooks"))?;
439 for version in ["20.9.0", "20.11.1"] {
440 fs::create_dir_all(root.join(format!("packages/{version}/bin")))?;
441 }
442 fs::write(
443 root.join("metadata.lua"),
444 "PLUGIN = {}\nPLUGIN.name = 'nodejs'\nPLUGIN.version = '0.1.0'\nPLUGIN.legacyFilenames = { '.nvmrc' }\n",
445 )?;
446 fs::write(
447 root.join("hooks/available.lua"),
448 "function PLUGIN:Available(ctx)\n return { { version = '20.11.1' }, { version = '20.9.0' } }\nend\n",
449 )?;
450 fs::write(
451 root.join("hooks/pre_install.lua"),
452 "function PLUGIN:PreInstall(ctx)\n return { version = ctx.version, url = 'packages/' .. ctx.version }\nend\n",
453 )?;
454 fs::write(
455 root.join("hooks/env_keys.lua"),
456 "function PLUGIN:EnvKeys(ctx)\n return { { key = 'PATH', value = ctx.path .. '/bin' } }\nend\n",
457 )?;
458 Ok(())
459 }
460}