1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2
3mod detection;
10mod execution;
11mod pal;
12mod types;
13mod workspace;
14
15use detection::{DetectedPackage, detect_package};
16use execution::{execute_with_cargo_args, execute_with_env_var};
17use pal::{Filesystem, FilesystemFacade};
18pub use types::*;
19use workspace::validate_workspace_context;
20
21#[doc(hidden)]
26pub fn run(input: &RunInput) -> Result<RunOutcome, RunError> {
27 run_with_filesystem(input, &FilesystemFacade::target())
28}
29
30fn run_with_filesystem(input: &RunInput, fs: &impl Filesystem) -> Result<RunOutcome, RunError> {
34 let workspace_context = validate_workspace_context(&input.path, fs)
37 .map_err(|e| RunError::WorkspaceValidation(e.to_string()))?;
38
39 let detected_package = detect_package(&workspace_context, fs)
40 .map_err(|e| RunError::PackageDetection(e.to_string()))?;
41
42 match (&detected_package, &input.outside_package) {
44 (DetectedPackage::Workspace, OutsidePackageAction::Ignore) => {
45 println!("Path is not in any package, ignoring as requested");
46 return Ok(RunOutcome::Ignored);
47 }
48 (DetectedPackage::Workspace, OutsidePackageAction::Error) => {
49 return Err(RunError::OutsidePackage);
50 }
51 (DetectedPackage::Package(name), _) => {
52 println!("Detected package: {name}");
53 }
54 (DetectedPackage::Workspace, OutsidePackageAction::Workspace) => {
55 println!("Path is not in any package, using workspace scope");
56 }
57 }
58
59 assert_early_exit_cases_handled(&detected_package, &input.outside_package);
60
61 let exit_status = match &input.via_env {
62 Some(env_var) => execute_with_env_var(
63 &workspace_context.workspace_root,
64 env_var,
65 &detected_package,
66 &input.subcommand,
67 ),
68 None => execute_with_cargo_args(
69 &workspace_context.workspace_root,
70 &detected_package,
71 &input.subcommand,
72 ),
73 }
74 .map_err(RunError::CommandExecution)?;
75
76 let subcommand_succeeded = exit_status.success();
77
78 match detected_package {
79 DetectedPackage::Package(package_name) => Ok(RunOutcome::PackageDetected {
80 package_name,
81 subcommand_succeeded,
82 }),
83 DetectedPackage::Workspace => Ok(RunOutcome::WorkspaceScope {
84 subcommand_succeeded,
85 }),
86 }
87}
88
89#[cfg_attr(test, mutants::skip)]
97#[cfg_attr(coverage_nightly, coverage(off))]
98fn assert_early_exit_cases_handled(
99 detected_package: &DetectedPackage,
100 outside_package: &OutsidePackageAction,
101) {
102 let is_early_exit_case = matches!(
103 (detected_package, outside_package),
104 (
105 DetectedPackage::Workspace,
106 OutsidePackageAction::Ignore | OutsidePackageAction::Error
107 )
108 );
109 assert!(
110 !is_early_exit_case,
111 "Logic error: the match block above must return early for Ignore/Error cases"
112 );
113}
114
115#[cfg(test)]
119#[cfg_attr(coverage_nightly, coverage(off))]
120mod mock_tests {
121 use std::io;
122 use std::path::{Path, PathBuf};
123
124 use super::*;
125 use crate::detection::WorkspaceContext;
126 use crate::pal::{FilesystemFacade, MockFilesystem};
127
128 fn create_simple_workspace_mock() -> MockFilesystem {
141 let mut mock = MockFilesystem::new();
142
143 mock.expect_current_dir()
145 .returning(|| Ok(PathBuf::from("/workspace")));
146
147 mock.expect_cargo_toml_exists().returning(|dir| {
149 let path_str = dir.to_string_lossy();
150 path_str == "/workspace" || path_str == "/workspace/package_a"
151 });
152
153 mock.expect_read_cargo_toml().returning(|dir| {
155 let path_str = dir.to_string_lossy();
156 if path_str == "/workspace" {
157 Ok(r#"[workspace]
158members = ["package_a"]
159"#
160 .to_string())
161 } else if path_str == "/workspace/package_a" {
162 Ok(r#"[package]
163name = "package_a"
164version = "0.1.0"
165"#
166 .to_string())
167 } else {
168 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
169 }
170 });
171
172 mock.expect_is_file().returning(|path| {
174 let path_str = path.to_string_lossy();
175 path_str.ends_with(".rs") || path_str.ends_with(".md")
176 });
177
178 mock.expect_exists().returning(|path| {
180 let path_str = path.to_string_lossy();
181 path_str == "/workspace"
182 || path_str == "/workspace/Cargo.toml"
183 || path_str == "/workspace/package_a"
184 || path_str == "/workspace/package_a/Cargo.toml"
185 || path_str == "/workspace/package_a/src"
186 || path_str == "/workspace/package_a/src/lib.rs"
187 || path_str == "/workspace/README.md"
188 });
189
190 mock.expect_canonicalize()
192 .returning(|path| Ok(path.to_path_buf()));
193
194 mock
195 }
196
197 #[test]
198 fn mock_package_detection_in_simple_workspace() {
199 let mock = create_simple_workspace_mock();
200 let fs = FilesystemFacade::from_mock(mock);
201
202 let context = validate_workspace_context(Path::new("/workspace/package_a/src/lib.rs"), &fs)
203 .expect("workspace validation should succeed");
204
205 let result = detect_package(&context, &fs).expect("package detection should succeed");
206
207 assert_eq!(result, DetectedPackage::Package("package_a".to_string()));
208 }
209
210 #[test]
211 fn mock_workspace_root_file_fallback() {
212 let mock = create_simple_workspace_mock();
213 let fs = FilesystemFacade::from_mock(mock);
214
215 let context = validate_workspace_context(Path::new("/workspace/README.md"), &fs)
216 .expect("workspace validation should succeed");
217
218 let result = detect_package(&context, &fs).expect("package detection should succeed");
219
220 assert_eq!(result, DetectedPackage::Workspace);
221 }
222
223 #[test]
224 fn mock_nonexistent_file_error() {
225 let mut mock = MockFilesystem::new();
226
227 mock.expect_current_dir()
228 .returning(|| Ok(PathBuf::from("/workspace")));
229
230 mock.expect_cargo_toml_exists()
231 .returning(|dir| dir.to_string_lossy() == "/workspace");
232
233 mock.expect_read_cargo_toml().returning(|dir| {
234 if dir.to_string_lossy() == "/workspace" {
235 Ok("[workspace]\nmembers = []\n".to_string())
236 } else {
237 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
238 }
239 });
240
241 mock.expect_exists().returning(|path| {
242 let path_str = path.to_string_lossy();
243 path_str == "/workspace" || path_str == "/workspace/Cargo.toml"
244 });
245
246 mock.expect_canonicalize().returning(|path| {
247 let path_str = path.to_string_lossy();
248 if path_str.contains("nonexistent") {
249 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
250 } else {
251 Ok(path.to_path_buf())
252 }
253 });
254
255 let fs = FilesystemFacade::from_mock(mock);
256
257 let result =
258 validate_workspace_context(Path::new("/workspace/package_a/src/nonexistent.rs"), &fs);
259
260 assert!(result.is_err());
261 assert!(result.unwrap_err().to_string().contains("does not exist"));
262 }
263
264 #[test]
265 fn mock_file_disappears_during_package_detection() {
266 use std::sync::Arc;
269 use std::sync::atomic::{AtomicBool, Ordering};
270
271 let file_exists = Arc::new(AtomicBool::new(true));
272 let file_exists_clone = Arc::clone(&file_exists);
273
274 let mut mock = MockFilesystem::new();
275
276 mock.expect_current_dir()
277 .returning(|| Ok(PathBuf::from("/workspace")));
278
279 mock.expect_cargo_toml_exists().returning(move |dir| {
280 let path_str = dir.to_string_lossy();
281 if path_str == "/workspace/package_a" {
282 let exists = file_exists_clone.load(Ordering::SeqCst);
284 file_exists_clone.store(false, Ordering::SeqCst);
285 exists
286 } else {
287 path_str == "/workspace"
288 }
289 });
290
291 mock.expect_read_cargo_toml().returning(|dir| {
292 let path_str = dir.to_string_lossy();
293 if path_str == "/workspace" {
294 Ok("[workspace]\nmembers = [\"package_a\"]\n".to_string())
295 } else {
296 Err(io::Error::new(io::ErrorKind::NotFound, "file was deleted"))
298 }
299 });
300
301 mock.expect_is_file()
302 .returning(|path| path.to_string_lossy().ends_with(".rs"));
303
304 mock.expect_exists().returning(|_| true);
305
306 mock.expect_canonicalize()
307 .returning(|path| Ok(path.to_path_buf()));
308
309 let fs = FilesystemFacade::from_mock(mock);
310
311 let context = WorkspaceContext {
313 absolute_target_path: PathBuf::from("/workspace/package_a/src/lib.rs"),
314 workspace_root: PathBuf::from("/workspace"),
315 };
316
317 let result = detect_package(&context, &fs);
319
320 assert!(result.is_err());
321 assert!(result.unwrap_err().to_string().contains("deleted"));
322 }
323
324 #[test]
325 fn mock_invalid_toml_in_package() {
326 let mut mock = MockFilesystem::new();
327
328 mock.expect_current_dir()
329 .returning(|| Ok(PathBuf::from("/workspace")));
330
331 mock.expect_cargo_toml_exists().returning(|dir| {
332 let path_str = dir.to_string_lossy();
333 path_str == "/workspace" || path_str == "/workspace/bad_package"
334 });
335
336 mock.expect_read_cargo_toml().returning(|dir| {
337 let path_str = dir.to_string_lossy();
338 if path_str == "/workspace" {
339 Ok("[workspace]\nmembers = [\"bad_package\"]\n".to_string())
340 } else if path_str == "/workspace/bad_package" {
341 Ok("[package\nname = \"bad\"\n".to_string())
343 } else {
344 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
345 }
346 });
347
348 mock.expect_is_file()
349 .returning(|path| path.to_string_lossy().ends_with(".rs"));
350
351 mock.expect_exists().returning(|_| true);
352
353 mock.expect_canonicalize()
354 .returning(|path| Ok(path.to_path_buf()));
355
356 let fs = FilesystemFacade::from_mock(mock);
357
358 let context = WorkspaceContext {
359 absolute_target_path: PathBuf::from("/workspace/bad_package/src/lib.rs"),
360 workspace_root: PathBuf::from("/workspace"),
361 };
362
363 let result = detect_package(&context, &fs);
364
365 assert!(result.is_err());
366 let error_msg = result.unwrap_err().to_string();
367 assert!(
368 error_msg.contains("TOML") || error_msg.contains("parse"),
369 "Expected TOML parse error, got: {error_msg}"
370 );
371 }
372
373 #[test]
374 fn mock_package_without_name_field() {
375 let mut mock = MockFilesystem::new();
376
377 mock.expect_current_dir()
378 .returning(|| Ok(PathBuf::from("/workspace")));
379
380 mock.expect_cargo_toml_exists().returning(|dir| {
381 let path_str = dir.to_string_lossy();
382 path_str == "/workspace" || path_str == "/workspace/nameless"
383 });
384
385 mock.expect_read_cargo_toml().returning(|dir| {
386 let path_str = dir.to_string_lossy();
387 if path_str == "/workspace" {
388 Ok("[workspace]\nmembers = [\"nameless\"]\n".to_string())
389 } else if path_str == "/workspace/nameless" {
390 Ok("[package]\nversion = \"0.1.0\"\n".to_string())
392 } else {
393 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
394 }
395 });
396
397 mock.expect_is_file()
398 .returning(|path| path.to_string_lossy().ends_with(".rs"));
399
400 mock.expect_exists().returning(|_| true);
401
402 mock.expect_canonicalize()
403 .returning(|path| Ok(path.to_path_buf()));
404
405 let fs = FilesystemFacade::from_mock(mock);
406
407 let context = WorkspaceContext {
408 absolute_target_path: PathBuf::from("/workspace/nameless/src/lib.rs"),
409 workspace_root: PathBuf::from("/workspace"),
410 };
411
412 let result = detect_package(&context, &fs);
413
414 assert!(result.is_err());
415 assert!(
416 result
417 .unwrap_err()
418 .to_string()
419 .contains("Could not find package name")
420 );
421 }
422
423 #[test]
424 fn mock_nested_package_detection() {
425 let mut mock = MockFilesystem::new();
427
428 mock.expect_current_dir()
429 .returning(|| Ok(PathBuf::from("/workspace")));
430
431 mock.expect_cargo_toml_exists().returning(|dir| {
432 let path_str = dir.to_string_lossy();
433 path_str == "/workspace"
434 || path_str == "/workspace/crates/inner"
435 || path_str == "/workspace/crates"
436 });
437
438 mock.expect_read_cargo_toml().returning(|dir| {
439 let path_str = dir.to_string_lossy();
440 if path_str == "/workspace" {
441 Ok("[workspace]\nmembers = [\"crates/*\"]\n".to_string())
442 } else if path_str == "/workspace/crates/inner" {
443 Ok("[package]\nname = \"inner-package\"\nversion = \"0.1.0\"\n".to_string())
444 } else {
445 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
446 }
447 });
448
449 mock.expect_is_file()
450 .returning(|path| path.to_string_lossy().ends_with(".rs"));
451
452 mock.expect_exists().returning(|_| true);
453
454 mock.expect_canonicalize()
455 .returning(|path| Ok(path.to_path_buf()));
456
457 let fs = FilesystemFacade::from_mock(mock);
458
459 let context = WorkspaceContext {
460 absolute_target_path: PathBuf::from("/workspace/crates/inner/src/lib.rs"),
461 workspace_root: PathBuf::from("/workspace"),
462 };
463
464 let result = detect_package(&context, &fs).expect("package detection should succeed");
465
466 assert_eq!(
467 result,
468 DetectedPackage::Package("inner-package".to_string())
469 );
470 }
471
472 #[test]
473 fn mock_directory_target_instead_of_file() {
474 let mock = create_simple_workspace_mock();
475 let fs = FilesystemFacade::from_mock(mock);
476
477 let context = WorkspaceContext {
479 absolute_target_path: PathBuf::from("/workspace/package_a/src"),
480 workspace_root: PathBuf::from("/workspace"),
481 };
482
483 let result = detect_package(&context, &fs).expect("package detection should succeed");
484
485 assert_eq!(result, DetectedPackage::Package("package_a".to_string()));
486 }
487
488 #[test]
489 fn mock_current_dir_outside_workspace() {
490 let mut mock = MockFilesystem::new();
491
492 mock.expect_current_dir()
493 .returning(|| Ok(PathBuf::from("/some/random/path")));
494
495 mock.expect_cargo_toml_exists().returning(|_| false);
496
497 let fs = FilesystemFacade::from_mock(mock);
498
499 let result = validate_workspace_context(Path::new("file.rs"), &fs);
500
501 assert!(result.is_err());
502 assert!(
503 result
504 .unwrap_err()
505 .to_string()
506 .contains("not within a Cargo workspace")
507 );
508 }
509
510 #[test]
511 fn mock_current_dir_fails() {
512 let mut mock = MockFilesystem::new();
513
514 mock.expect_current_dir().returning(|| {
515 Err(io::Error::new(
516 io::ErrorKind::PermissionDenied,
517 "access denied",
518 ))
519 });
520
521 let fs = FilesystemFacade::from_mock(mock);
522
523 let result = validate_workspace_context(Path::new("file.rs"), &fs);
524
525 result.unwrap_err();
526 }
527
528 #[test]
529 fn mock_detect_package_reaches_filesystem_root() {
530 let mut mock = MockFilesystem::new();
534
535 mock.expect_is_file()
536 .returning(|path| path.to_string_lossy() == "/file.rs");
537
538 mock.expect_cargo_toml_exists()
540 .returning(|dir| dir.to_string_lossy() == "/");
541
542 mock.expect_read_cargo_toml().returning(|dir| {
543 if dir.to_string_lossy() == "/" {
544 Ok("[workspace]\nmembers = []\n".to_string())
545 } else {
546 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
547 }
548 });
549
550 let fs = FilesystemFacade::from_mock(mock);
551
552 let context = WorkspaceContext {
555 absolute_target_path: PathBuf::from("/file.rs"),
556 workspace_root: PathBuf::from("/"),
557 };
558
559 let result = detect_package(&context, &fs).expect("detection should succeed");
563
564 assert_eq!(result, DetectedPackage::Workspace);
565 }
566}