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 =
203 validate_workspace_context(Path::new("/workspace/package_a/src/lib.rs"), &fs).unwrap();
204
205 let result = detect_package(&context, &fs).unwrap();
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).unwrap();
216
217 let result = detect_package(&context, &fs).unwrap();
218
219 assert_eq!(result, DetectedPackage::Workspace);
220 }
221
222 #[test]
223 fn mock_nonexistent_file_error() {
224 let mut mock = MockFilesystem::new();
225
226 mock.expect_current_dir()
227 .returning(|| Ok(PathBuf::from("/workspace")));
228
229 mock.expect_cargo_toml_exists()
230 .returning(|dir| dir.to_string_lossy() == "/workspace");
231
232 mock.expect_read_cargo_toml().returning(|dir| {
233 if dir.to_string_lossy() == "/workspace" {
234 Ok("[workspace]\nmembers = []\n".to_string())
235 } else {
236 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
237 }
238 });
239
240 mock.expect_exists().returning(|path| {
241 let path_str = path.to_string_lossy();
242 path_str == "/workspace" || path_str == "/workspace/Cargo.toml"
243 });
244
245 mock.expect_canonicalize().returning(|path| {
246 let path_str = path.to_string_lossy();
247 if path_str.contains("nonexistent") {
248 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
249 } else {
250 Ok(path.to_path_buf())
251 }
252 });
253
254 let fs = FilesystemFacade::from_mock(mock);
255
256 let result =
257 validate_workspace_context(Path::new("/workspace/package_a/src/nonexistent.rs"), &fs);
258
259 assert!(result.is_err());
260 assert!(result.unwrap_err().to_string().contains("does not exist"));
261 }
262
263 #[test]
264 fn mock_file_disappears_during_package_detection() {
265 use std::sync::Arc;
268 use std::sync::atomic::{AtomicBool, Ordering};
269
270 let file_exists = Arc::new(AtomicBool::new(true));
271 let file_exists_clone = Arc::clone(&file_exists);
272
273 let mut mock = MockFilesystem::new();
274
275 mock.expect_current_dir()
276 .returning(|| Ok(PathBuf::from("/workspace")));
277
278 mock.expect_cargo_toml_exists().returning(move |dir| {
279 let path_str = dir.to_string_lossy();
280 if path_str == "/workspace/package_a" {
281 let exists = file_exists_clone.load(Ordering::SeqCst);
283 file_exists_clone.store(false, Ordering::SeqCst);
284 exists
285 } else {
286 path_str == "/workspace"
287 }
288 });
289
290 mock.expect_read_cargo_toml().returning(|dir| {
291 let path_str = dir.to_string_lossy();
292 if path_str == "/workspace" {
293 Ok("[workspace]\nmembers = [\"package_a\"]\n".to_string())
294 } else {
295 Err(io::Error::new(io::ErrorKind::NotFound, "file was deleted"))
297 }
298 });
299
300 mock.expect_is_file()
301 .returning(|path| path.to_string_lossy().ends_with(".rs"));
302
303 mock.expect_exists().returning(|_| true);
304
305 mock.expect_canonicalize()
306 .returning(|path| Ok(path.to_path_buf()));
307
308 let fs = FilesystemFacade::from_mock(mock);
309
310 let context = WorkspaceContext {
312 absolute_target_path: PathBuf::from("/workspace/package_a/src/lib.rs"),
313 workspace_root: PathBuf::from("/workspace"),
314 };
315
316 let result = detect_package(&context, &fs);
318
319 assert!(result.is_err());
320 assert!(result.unwrap_err().to_string().contains("deleted"));
321 }
322
323 #[test]
324 fn mock_invalid_toml_in_package() {
325 let mut mock = MockFilesystem::new();
326
327 mock.expect_current_dir()
328 .returning(|| Ok(PathBuf::from("/workspace")));
329
330 mock.expect_cargo_toml_exists().returning(|dir| {
331 let path_str = dir.to_string_lossy();
332 path_str == "/workspace" || path_str == "/workspace/bad_package"
333 });
334
335 mock.expect_read_cargo_toml().returning(|dir| {
336 let path_str = dir.to_string_lossy();
337 if path_str == "/workspace" {
338 Ok("[workspace]\nmembers = [\"bad_package\"]\n".to_string())
339 } else if path_str == "/workspace/bad_package" {
340 Ok("[package\nname = \"bad\"\n".to_string())
342 } else {
343 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
344 }
345 });
346
347 mock.expect_is_file()
348 .returning(|path| path.to_string_lossy().ends_with(".rs"));
349
350 mock.expect_exists().returning(|_| true);
351
352 mock.expect_canonicalize()
353 .returning(|path| Ok(path.to_path_buf()));
354
355 let fs = FilesystemFacade::from_mock(mock);
356
357 let context = WorkspaceContext {
358 absolute_target_path: PathBuf::from("/workspace/bad_package/src/lib.rs"),
359 workspace_root: PathBuf::from("/workspace"),
360 };
361
362 let result = detect_package(&context, &fs);
363
364 assert!(result.is_err());
365 let error_msg = result.unwrap_err().to_string();
366 assert!(
367 error_msg.contains("TOML") || error_msg.contains("parse"),
368 "Expected TOML parse error, got: {error_msg}"
369 );
370 }
371
372 #[test]
373 fn mock_package_without_name_field() {
374 let mut mock = MockFilesystem::new();
375
376 mock.expect_current_dir()
377 .returning(|| Ok(PathBuf::from("/workspace")));
378
379 mock.expect_cargo_toml_exists().returning(|dir| {
380 let path_str = dir.to_string_lossy();
381 path_str == "/workspace" || path_str == "/workspace/nameless"
382 });
383
384 mock.expect_read_cargo_toml().returning(|dir| {
385 let path_str = dir.to_string_lossy();
386 if path_str == "/workspace" {
387 Ok("[workspace]\nmembers = [\"nameless\"]\n".to_string())
388 } else if path_str == "/workspace/nameless" {
389 Ok("[package]\nversion = \"0.1.0\"\n".to_string())
391 } else {
392 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
393 }
394 });
395
396 mock.expect_is_file()
397 .returning(|path| path.to_string_lossy().ends_with(".rs"));
398
399 mock.expect_exists().returning(|_| true);
400
401 mock.expect_canonicalize()
402 .returning(|path| Ok(path.to_path_buf()));
403
404 let fs = FilesystemFacade::from_mock(mock);
405
406 let context = WorkspaceContext {
407 absolute_target_path: PathBuf::from("/workspace/nameless/src/lib.rs"),
408 workspace_root: PathBuf::from("/workspace"),
409 };
410
411 let result = detect_package(&context, &fs);
412
413 assert!(result.is_err());
414 assert!(
415 result
416 .unwrap_err()
417 .to_string()
418 .contains("Could not find package name")
419 );
420 }
421
422 #[test]
423 fn mock_nested_package_detection() {
424 let mut mock = MockFilesystem::new();
426
427 mock.expect_current_dir()
428 .returning(|| Ok(PathBuf::from("/workspace")));
429
430 mock.expect_cargo_toml_exists().returning(|dir| {
431 let path_str = dir.to_string_lossy();
432 path_str == "/workspace"
433 || path_str == "/workspace/crates/inner"
434 || path_str == "/workspace/crates"
435 });
436
437 mock.expect_read_cargo_toml().returning(|dir| {
438 let path_str = dir.to_string_lossy();
439 if path_str == "/workspace" {
440 Ok("[workspace]\nmembers = [\"crates/*\"]\n".to_string())
441 } else if path_str == "/workspace/crates/inner" {
442 Ok("[package]\nname = \"inner-package\"\nversion = \"0.1.0\"\n".to_string())
443 } else {
444 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
445 }
446 });
447
448 mock.expect_is_file()
449 .returning(|path| path.to_string_lossy().ends_with(".rs"));
450
451 mock.expect_exists().returning(|_| true);
452
453 mock.expect_canonicalize()
454 .returning(|path| Ok(path.to_path_buf()));
455
456 let fs = FilesystemFacade::from_mock(mock);
457
458 let context = WorkspaceContext {
459 absolute_target_path: PathBuf::from("/workspace/crates/inner/src/lib.rs"),
460 workspace_root: PathBuf::from("/workspace"),
461 };
462
463 let result = detect_package(&context, &fs).unwrap();
464
465 assert_eq!(
466 result,
467 DetectedPackage::Package("inner-package".to_string())
468 );
469 }
470
471 #[test]
472 fn mock_directory_target_instead_of_file() {
473 let mock = create_simple_workspace_mock();
474 let fs = FilesystemFacade::from_mock(mock);
475
476 let context = WorkspaceContext {
478 absolute_target_path: PathBuf::from("/workspace/package_a/src"),
479 workspace_root: PathBuf::from("/workspace"),
480 };
481
482 let result = detect_package(&context, &fs).unwrap();
483
484 assert_eq!(result, DetectedPackage::Package("package_a".to_string()));
485 }
486
487 #[test]
488 fn mock_current_dir_outside_workspace() {
489 let mut mock = MockFilesystem::new();
490
491 mock.expect_current_dir()
492 .returning(|| Ok(PathBuf::from("/some/random/path")));
493
494 mock.expect_cargo_toml_exists().returning(|_| false);
495
496 let fs = FilesystemFacade::from_mock(mock);
497
498 let result = validate_workspace_context(Path::new("file.rs"), &fs);
499
500 assert!(result.is_err());
501 assert!(
502 result
503 .unwrap_err()
504 .to_string()
505 .contains("not within a Cargo workspace")
506 );
507 }
508
509 #[test]
510 fn mock_current_dir_fails() {
511 let mut mock = MockFilesystem::new();
512
513 mock.expect_current_dir().returning(|| {
514 Err(io::Error::new(
515 io::ErrorKind::PermissionDenied,
516 "access denied",
517 ))
518 });
519
520 let fs = FilesystemFacade::from_mock(mock);
521
522 let result = validate_workspace_context(Path::new("file.rs"), &fs);
523
524 result.unwrap_err();
525 }
526
527 #[test]
528 fn mock_detect_package_reaches_filesystem_root() {
529 let mut mock = MockFilesystem::new();
533
534 mock.expect_is_file()
535 .returning(|path| path.to_string_lossy() == "/file.rs");
536
537 mock.expect_cargo_toml_exists()
539 .returning(|dir| dir.to_string_lossy() == "/");
540
541 mock.expect_read_cargo_toml().returning(|dir| {
542 if dir.to_string_lossy() == "/" {
543 Ok("[workspace]\nmembers = []\n".to_string())
544 } else {
545 Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
546 }
547 });
548
549 let fs = FilesystemFacade::from_mock(mock);
550
551 let context = WorkspaceContext {
554 absolute_target_path: PathBuf::from("/file.rs"),
555 workspace_root: PathBuf::from("/"),
556 };
557
558 let result = detect_package(&context, &fs).unwrap();
562
563 assert_eq!(result, DetectedPackage::Workspace);
564 }
565}