update_kit/detection/
brew.rs1use crate::types::{Channel, Confidence, Evidence, InstallDetection};
2use crate::utils::process::CommandRunner;
3
4const BREW_PATH_PATTERNS: &[&str] = &[
6 "/opt/homebrew/",
7 "/usr/local/Caskroom/",
8 "/usr/local/Cellar/",
9 "/home/linuxbrew/",
10];
11
12pub async fn detect_from_brew(
20 exec_path: &str,
21 brew_cask_name: Option<&str>,
22 cmd: &dyn CommandRunner,
23) -> Option<InstallDetection> {
24 let matching_pattern = BREW_PATH_PATTERNS
25 .iter()
26 .find(|pattern| exec_path.contains(*pattern));
27
28 let pattern = matching_pattern?;
29 let mut evidence = vec![Evidence {
30 source: "brew-path".into(),
31 detail: format!("path contains brew pattern '{}'", pattern),
32 }];
33
34 if let Some(cask_name) = brew_cask_name {
36 match cmd.run("brew", &["list", "--cask", cask_name]).await {
37 Ok(output) if output.success() => {
38 evidence.push(Evidence {
39 source: "brew-verify".into(),
40 detail: format!("brew list --cask {} succeeded", cask_name),
41 });
42 return Some(InstallDetection {
43 channel: Channel::BrewCask,
44 confidence: Confidence::High,
45 evidence,
46 });
47 }
48 _ => {
49 }
51 }
52 }
53
54 Some(InstallDetection {
55 channel: Channel::BrewCask,
56 confidence: Confidence::Medium,
57 evidence,
58 })
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::utils::process::TokioCommandRunner;
65
66 #[tokio::test]
67 async fn brew_path_detected_without_cask_name() {
68 let cmd = TokioCommandRunner;
69 let result = detect_from_brew("/opt/homebrew/bin/my-app", None, &cmd).await;
70 assert!(result.is_some());
71 let detection = result.unwrap();
72 assert_eq!(detection.channel, Channel::BrewCask);
73 assert_eq!(detection.confidence, Confidence::Medium);
74 }
75
76 #[tokio::test]
77 async fn non_brew_path_returns_none() {
78 let cmd = TokioCommandRunner;
79 let result = detect_from_brew("/usr/bin/my-app", None, &cmd).await;
80 assert!(result.is_none());
81 }
82
83 #[tokio::test]
84 async fn cellar_path_detected() {
85 let cmd = TokioCommandRunner;
86 let result =
87 detect_from_brew("/usr/local/Cellar/my-app/1.0/bin/my-app", None, &cmd).await;
88 assert!(result.is_some());
89 let detection = result.unwrap();
90 assert_eq!(detection.channel, Channel::BrewCask);
91 }
92
93 #[tokio::test]
94 async fn caskroom_path_detected() {
95 let cmd = TokioCommandRunner;
96 let result = detect_from_brew(
97 "/usr/local/Caskroom/my-app/1.0/my-app.app/bin/my-app",
98 None,
99 &cmd,
100 )
101 .await;
102 assert!(result.is_some());
103 }
104
105 #[tokio::test]
106 async fn linuxbrew_path_detected() {
107 let cmd = TokioCommandRunner;
108 let result = detect_from_brew("/home/linuxbrew/.linuxbrew/bin/my-app", None, &cmd).await;
109 assert!(result.is_some());
110 }
111
112 use crate::test_utils::MockCommandRunner;
115
116 #[tokio::test]
117 async fn verified_cask_high_confidence() {
118 let cmd = MockCommandRunner::new();
119 cmd.on(
120 "brew list --cask my-cask",
121 Ok(MockCommandRunner::success_output("my-cask")),
122 );
123
124 let result = detect_from_brew("/opt/homebrew/bin/my-app", Some("my-cask"), &cmd).await;
125 let detection = result.unwrap();
126 assert_eq!(detection.channel, Channel::BrewCask);
127 assert_eq!(detection.confidence, Confidence::High);
128 assert!(detection.evidence.len() >= 2); }
130
131 #[tokio::test]
132 async fn failed_verification_medium_confidence() {
133 let cmd = MockCommandRunner::new();
134 cmd.on(
135 "brew list --cask my-cask",
136 Ok(MockCommandRunner::failure_output("Error")),
137 );
138
139 let result = detect_from_brew("/opt/homebrew/bin/my-app", Some("my-cask"), &cmd).await;
140 let detection = result.unwrap();
141 assert_eq!(detection.channel, Channel::BrewCask);
142 assert_eq!(detection.confidence, Confidence::Medium);
143 assert_eq!(detection.evidence.len(), 1); }
145
146 #[tokio::test]
147 async fn brew_command_not_found_medium_confidence() {
148 let cmd = MockCommandRunner::new();
149 let result = detect_from_brew("/opt/homebrew/bin/my-app", Some("my-cask"), &cmd).await;
152 let detection = result.unwrap();
153 assert_eq!(detection.confidence, Confidence::Medium);
154 }
155
156 #[tokio::test]
157 async fn evidence_source_is_brew_path() {
158 let cmd = MockCommandRunner::new();
159 let result = detect_from_brew("/opt/homebrew/bin/my-app", None, &cmd).await;
160 let detection = result.unwrap();
161 assert!(detection.evidence.iter().any(|e| e.source == "brew-path"));
162 }
163
164 #[tokio::test]
165 async fn evidence_has_verify_on_success() {
166 let cmd = MockCommandRunner::new();
167 cmd.on(
168 "brew list --cask my-cask",
169 Ok(MockCommandRunner::success_output("")),
170 );
171
172 let result = detect_from_brew("/opt/homebrew/bin/my-app", Some("my-cask"), &cmd).await;
173 let detection = result.unwrap();
174 assert!(detection
175 .evidence
176 .iter()
177 .any(|e| e.source == "brew-verify"));
178 }
179
180 #[tokio::test]
181 async fn usr_local_caskroom_detected() {
182 let cmd = MockCommandRunner::new();
183 let result =
184 detect_from_brew("/usr/local/Caskroom/my-app/1.0/bin/my-app", None, &cmd).await;
185 assert!(result.is_some());
186 assert_eq!(result.unwrap().channel, Channel::BrewCask);
187 }
188
189 #[tokio::test]
190 async fn non_brew_path_with_cask_name_returns_none() {
191 let cmd = MockCommandRunner::new();
193 let result = detect_from_brew("/usr/bin/my-app", Some("my-cask"), &cmd).await;
194 assert!(result.is_none());
195 }
196}