which_problem/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(unused_crate_dependencies)]
3#![warn(clippy::pedantic)]
4
5/// Get detailed diagnostic information about executable lookups
6///
7/// Example:
8///
9/// ```rust,no_run
10/// use std::process::Command;
11/// use which_problem::Which;
12///
13/// let program = "bundle";
14/// Command::new(program)
15///     .arg("install")
16///     .output()
17///     .map_err(|error| {
18///         eprintln!("Executing command '{program}' failed. Error: {error}");
19///
20///         match Which::new(program).diagnose() {
21///             Ok(details) => println!("Diagnostic info: {details}"),
22///             Err(error) => println!("Warning: Internal which_problem error: {error}"),
23///         }
24///         error
25///     })
26///     .unwrap();
27/// ```
28mod file_state;
29mod path_part;
30mod path_with_state;
31mod program;
32mod suggest;
33mod which;
34
35// Primary input interface
36pub use crate::which::Which;
37
38// Primary output interface
39pub use crate::program::Program;
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use crate::file_state::{file_state, FileState};
45    use crate::path_with_state::PathWithState;
46    use crate::which::Which;
47    use is_executable::IsExecutable;
48    use path_part::PartState;
49    use std::ffi::OsString;
50    use std::os::unix::fs::PermissionsExt;
51    use std::path::Path;
52
53    fn make_executable(file: &Path) {
54        let perms = std::fs::metadata(file).unwrap().permissions();
55        let mode = perms.mode() | 0o111;
56        std::fs::set_permissions(file, std::fs::Permissions::from_mode(mode)).unwrap();
57    }
58
59    #[test]
60    fn multiple_valid() {
61        let tmp_dir = tempfile::tempdir().unwrap();
62        let dir = tmp_dir.path().to_path_buf();
63        let file = dir.join("haha");
64
65        let tmp_dir_two = tempfile::tempdir().unwrap();
66        let dir_two = tmp_dir_two.path().to_path_buf();
67        let file_two = dir_two.join(file.file_name().unwrap());
68        let program = OsString::from(file.file_name().unwrap());
69
70        std::fs::write(&file, "contents").unwrap();
71        std::fs::write(&file_two, "contents").unwrap();
72        make_executable(&file);
73        make_executable(&file_two);
74
75        let program = Which {
76            program,
77            path_env: Some(vec![dir.as_os_str(), dir_two.as_os_str()].join(&OsString::from(":"))),
78            ..Which::default()
79        }
80        .diagnose()
81        .unwrap();
82
83        assert_eq!(
84            vec![
85                PathWithState {
86                    path: file,
87                    state: FileState::Valid,
88                },
89                PathWithState {
90                    path: file_two,
91                    state: FileState::Valid
92                }
93            ],
94            program.found_files
95        );
96    }
97
98    #[test]
99    fn check_executable_file() {
100        let tmp_dir = tempfile::tempdir().unwrap();
101        let dir = tmp_dir.path();
102        let file = dir.join("lol");
103        let name = OsString::from(file.file_name().unwrap());
104
105        std::fs::write(&file, "contents").unwrap();
106
107        let program = Which {
108            program: name.clone(),
109            path_env: Some(dir.as_os_str().into()),
110            ..Which::default()
111        }
112        .diagnose()
113        .unwrap();
114
115        assert_eq!(
116            vec![PathWithState {
117                path: file.clone(),
118                state: FileState::NotExecutable
119            }],
120            program.found_files
121        );
122
123        let perms = std::fs::metadata(&file).unwrap().permissions();
124        let mode = perms.mode() | 0o111;
125        std::fs::set_permissions(&file, std::fs::Permissions::from_mode(mode)).unwrap();
126
127        assert!(file.is_executable());
128
129        let program = Which {
130            program: name,
131            path_env: Some(dir.as_os_str().into()),
132            ..Which::default()
133        }
134        .diagnose()
135        .unwrap();
136
137        assert_eq!(
138            vec![PathWithState {
139                path: file,
140                state: FileState::Valid
141            }],
142            program.found_files
143        );
144    }
145
146    #[test]
147    fn check_symlink_file() {
148        let tmp_dir = tempfile::tempdir().unwrap();
149        let dir = tmp_dir.path();
150        let file = dir.join("lol");
151        let name = OsString::from(file.file_name().unwrap());
152
153        std::os::unix::fs::symlink(dir.join("nope"), &file).unwrap();
154
155        assert_eq!(FileState::BadSymlink, file_state(&file));
156
157        let program = Which {
158            program: name,
159            path_env: Some(dir.as_os_str().into()),
160            ..Which::default()
161        }
162        .diagnose()
163        .unwrap();
164
165        assert_eq!(
166            vec![PathWithState {
167                path: file,
168                state: FileState::BadSymlink
169            }],
170            program.found_files
171        );
172    }
173
174    #[test]
175    fn check_parts_are_dirs() {
176        let tmp_dir = tempfile::tempdir().unwrap();
177        let dir = tmp_dir.path();
178        let file = dir.join("lol");
179        let name = OsString::from(file.file_name().unwrap());
180        let expected = dir.join("nope");
181
182        std::fs::write(&expected, "lol").unwrap();
183        let program = Which {
184            program: name,
185            path_env: Some(vec![expected.as_os_str(), dir.as_os_str()].join(&OsString::from(":"))),
186            ..Which::default()
187        }
188        .diagnose()
189        .unwrap();
190
191        assert!(!program
192            .path_parts
193            .iter()
194            .any(|p| p.state == PartState::Missing));
195        assert!(program
196            .path_parts
197            .iter()
198            .any(|p| p.state == PartState::NotDir));
199    }
200
201    #[test]
202    fn check_path_parts_exist() {
203        let tmp_dir = tempfile::tempdir().unwrap();
204        let dir = tmp_dir.path();
205        let file = dir.join("lol");
206        let name = OsString::from(file.file_name().unwrap());
207        let expected = dir.join("nope");
208
209        let program = Which {
210            program: name,
211            path_env: Some(vec![expected.as_os_str(), dir.as_os_str()].join(&OsString::from(":"))),
212            ..Which::default()
213        }
214        .diagnose()
215        .unwrap();
216
217        assert!(program
218            .path_parts
219            .iter()
220            .any(|p| p.state == PartState::Missing));
221        assert!(!program
222            .path_parts
223            .iter()
224            .any(|p| p.state == PartState::NotDir));
225    }
226
227    #[test]
228    fn check_suggested_spelling() {
229        let tmp_dir = tempfile::tempdir().unwrap();
230        let dir = tmp_dir.path();
231        let file = dir.join("lol");
232        let name = OsString::from(file.file_name().unwrap());
233
234        let actual = dir.join("rofl");
235        std::fs::write(&actual, "contents").unwrap();
236        make_executable(&actual);
237
238        let program = Which {
239            program: name,
240            path_env: Some(dir.as_os_str().into()),
241            ..Which::default()
242        }
243        .diagnose()
244        .unwrap();
245
246        assert_eq!(
247            program.suggested.clone().unwrap(),
248            vec![actual.file_name().unwrap()]
249        );
250
251        assert_eq!(program.name, file.file_name().unwrap());
252    }
253}