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