manasight_parser/log/
discovery.rs1use std::path::{Path, PathBuf};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct LogPaths {
34 player_log: PathBuf,
36 player_prev_log: PathBuf,
38}
39
40impl LogPaths {
41 pub fn player_log(&self) -> &Path {
43 &self.player_log
44 }
45
46 pub fn player_prev_log(&self) -> &Path {
48 &self.player_prev_log
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
58pub enum DiscoveryError {
59 #[error("could not resolve platform log directory")]
65 BaseDirNotFound,
66
67 #[error("log file not found at {path}", path = path.display())]
72 LogFileMissing {
73 path: PathBuf,
75 },
76
77 #[error("unsupported platform for log file discovery")]
81 UnsupportedPlatform,
82}
83
84const MTGA_LOG_DIR: &[&str] = &["Wizards Of The Coast", "MTGA"];
90
91const PLAYER_LOG: &str = "Player.log";
93
94const PLAYER_PREV_LOG: &str = "Player-prev.log";
96
97#[cfg(target_os = "windows")]
105fn resolve_base_dir() -> Result<PathBuf, DiscoveryError> {
106 known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppDataLow)
107 .ok_or(DiscoveryError::BaseDirNotFound)
108}
109
110#[cfg(target_os = "macos")]
114fn resolve_base_dir() -> Result<PathBuf, DiscoveryError> {
115 std::env::var("HOME")
116 .ok()
117 .map(|home| PathBuf::from(home).join("Library").join("Logs"))
118 .ok_or(DiscoveryError::BaseDirNotFound)
119}
120
121#[cfg(not(any(target_os = "windows", target_os = "macos")))]
123fn resolve_base_dir() -> Result<PathBuf, DiscoveryError> {
124 Err(DiscoveryError::UnsupportedPlatform)
125}
126
127fn build_log_paths(base_dir: PathBuf) -> LogPaths {
135 let mut mtga_dir = base_dir;
136 for component in MTGA_LOG_DIR {
137 mtga_dir.push(component);
138 }
139 LogPaths {
140 player_log: mtga_dir.join(PLAYER_LOG),
141 player_prev_log: mtga_dir.join(PLAYER_PREV_LOG),
142 }
143}
144
145fn check_existence(paths: LogPaths) -> Result<LogPaths, DiscoveryError> {
150 if paths.player_log.exists() {
151 ::log::info!("discovered log file: {}", paths.player_log.display());
152 Ok(paths)
153 } else {
154 ::log::warn!("log file not found: {}", paths.player_log.display());
155 Err(DiscoveryError::LogFileMissing {
156 path: paths.player_log,
157 })
158 }
159}
160
161pub fn resolve_log_paths() -> Result<LogPaths, DiscoveryError> {
178 let base_dir = resolve_base_dir()?;
179 Ok(build_log_paths(base_dir))
180}
181
182pub fn discover_log_file() -> Result<LogPaths, DiscoveryError> {
197 let paths = resolve_log_paths()?;
198 check_existence(paths)
199}
200
201#[cfg(test)]
206mod tests {
207 use super::*;
208 use std::fs;
209
210 type TestResult = Result<(), Box<dyn std::error::Error>>;
211
212 #[test]
215 fn test_build_log_paths_appends_mtga_components() {
216 let base = PathBuf::from("/some/base");
217 let paths = build_log_paths(base);
218 assert_eq!(
219 paths.player_log(),
220 Path::new("/some/base/Wizards Of The Coast/MTGA/Player.log")
221 );
222 assert_eq!(
223 paths.player_prev_log(),
224 Path::new("/some/base/Wizards Of The Coast/MTGA/Player-prev.log")
225 );
226 }
227
228 #[test]
229 fn test_build_log_paths_windows_style_path() {
230 let base = PathBuf::from(r"C:\Users\User\AppData\LocalLow");
231 let paths = build_log_paths(base);
232
233 let log_str = paths.player_log().to_string_lossy();
236 assert!(log_str.contains("Wizards Of The Coast"));
237 assert!(log_str.contains("MTGA"));
238 assert!(log_str.ends_with("Player.log"));
239 }
240
241 #[test]
242 fn test_build_log_paths_macos_style_path() {
243 let base = PathBuf::from("/Users/player/Library/Logs");
244 let paths = build_log_paths(base);
245 assert_eq!(
246 paths.player_log(),
247 Path::new("/Users/player/Library/Logs/Wizards Of The Coast/MTGA/Player.log")
248 );
249 }
250
251 #[test]
252 fn test_build_log_paths_both_files_share_directory() {
253 let paths = build_log_paths(PathBuf::from("/base"));
254 assert_eq!(
255 paths.player_log().parent(),
256 paths.player_prev_log().parent()
257 );
258 }
259
260 #[test]
261 fn test_build_log_paths_player_prev_log_correct_name() {
262 let paths = build_log_paths(PathBuf::from("/base"));
263 let filename = paths
264 .player_prev_log()
265 .file_name()
266 .map(|f| f.to_string_lossy().into_owned())
267 .unwrap_or_default();
268 assert_eq!(filename, "Player-prev.log");
269 }
270
271 #[test]
272 fn test_build_log_paths_player_log_correct_name() {
273 let paths = build_log_paths(PathBuf::from("/base"));
274 let filename = paths
275 .player_log()
276 .file_name()
277 .map(|f| f.to_string_lossy().into_owned())
278 .unwrap_or_default();
279 assert_eq!(filename, "Player.log");
280 }
281
282 #[test]
285 fn test_log_paths_clone_is_equal() {
286 let paths = build_log_paths(PathBuf::from("/base"));
287 let cloned = paths.clone();
288 assert_eq!(paths, cloned);
289 }
290
291 #[test]
294 fn test_check_existence_found_returns_ok() -> TestResult {
295 let dir = tempfile::tempdir()?;
296 let mtga_dir = dir.path().join("Wizards Of The Coast").join("MTGA");
297 fs::create_dir_all(&mtga_dir)?;
298 fs::write(mtga_dir.join("Player.log"), "test log data")?;
299
300 let paths = build_log_paths(dir.path().to_path_buf());
301 let result = check_existence(paths);
302 assert!(result.is_ok());
303 Ok(())
304 }
305
306 #[test]
307 fn test_check_existence_found_returns_correct_paths() -> TestResult {
308 let dir = tempfile::tempdir()?;
309 let mtga_dir = dir.path().join("Wizards Of The Coast").join("MTGA");
310 fs::create_dir_all(&mtga_dir)?;
311 fs::write(mtga_dir.join("Player.log"), "data")?;
312
313 let paths = build_log_paths(dir.path().to_path_buf());
314 let found = check_existence(paths)?;
315 assert_eq!(found.player_log(), mtga_dir.join("Player.log"));
316 assert_eq!(found.player_prev_log(), mtga_dir.join("Player-prev.log"));
317 Ok(())
318 }
319
320 #[test]
321 fn test_check_existence_missing_returns_log_file_missing() -> TestResult {
322 let dir = tempfile::tempdir()?;
323 let paths = build_log_paths(dir.path().to_path_buf());
325 let result = check_existence(paths);
326 assert!(matches!(result, Err(DiscoveryError::LogFileMissing { .. })));
327 Ok(())
328 }
329
330 #[test]
331 fn test_check_existence_missing_error_contains_expected_path() -> TestResult {
332 let dir = tempfile::tempdir()?;
333 let paths = build_log_paths(dir.path().to_path_buf());
334 let expected_path = paths.player_log().to_path_buf();
335
336 match check_existence(paths) {
337 Err(DiscoveryError::LogFileMissing { path }) => {
338 assert_eq!(path, expected_path);
339 }
340 other => return Err(format!("expected LogFileMissing, got: {other:?}").into()),
341 }
342 Ok(())
343 }
344
345 #[test]
346 fn test_check_existence_directory_exists_but_no_file() -> TestResult {
347 let dir = tempfile::tempdir()?;
348 let mtga_dir = dir.path().join("Wizards Of The Coast").join("MTGA");
349 fs::create_dir_all(&mtga_dir)?;
350 let paths = build_log_paths(dir.path().to_path_buf());
353 let result = check_existence(paths);
354 assert!(matches!(result, Err(DiscoveryError::LogFileMissing { .. })));
355 Ok(())
356 }
357
358 #[test]
361 fn test_discovery_error_base_dir_not_found_display() {
362 let err = DiscoveryError::BaseDirNotFound;
363 assert_eq!(err.to_string(), "could not resolve platform log directory");
364 }
365
366 #[test]
367 fn test_discovery_error_unsupported_platform_display() {
368 let err = DiscoveryError::UnsupportedPlatform;
369 assert_eq!(
370 err.to_string(),
371 "unsupported platform for log file discovery"
372 );
373 }
374
375 #[test]
376 fn test_discovery_error_log_file_missing_display() {
377 let err = DiscoveryError::LogFileMissing {
378 path: PathBuf::from("/some/path/Player.log"),
379 };
380 let display = err.to_string();
381 assert!(display.contains("/some/path/Player.log"));
382 assert!(display.contains("log file not found"));
383 }
384
385 #[test]
388 fn test_discovery_error_clone_is_equal() {
389 let err = DiscoveryError::LogFileMissing {
390 path: PathBuf::from("/test"),
391 };
392 let cloned = err.clone();
393 assert_eq!(err, cloned);
394 }
395
396 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
399 #[test]
400 fn test_resolve_log_paths_unsupported_platform() {
401 let result = resolve_log_paths();
402 assert!(matches!(result, Err(DiscoveryError::UnsupportedPlatform)));
403 }
404
405 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
406 #[test]
407 fn test_discover_log_file_unsupported_platform() {
408 let result = discover_log_file();
409 assert!(matches!(result, Err(DiscoveryError::UnsupportedPlatform)));
410 }
411
412 #[cfg(target_os = "windows")]
413 #[test]
414 fn test_resolve_log_paths_windows_contains_locallow() -> TestResult {
415 let paths = resolve_log_paths()?;
416 let log_str = paths.player_log().to_string_lossy();
417 assert!(
418 log_str.contains("LocalLow"),
419 "Windows path should contain LocalLow: {log_str}"
420 );
421 Ok(())
422 }
423
424 #[cfg(target_os = "macos")]
425 #[test]
426 fn test_resolve_log_paths_macos_contains_library_logs() -> TestResult {
427 let paths = resolve_log_paths()?;
428 let log_str = paths.player_log().to_string_lossy();
429 assert!(
430 log_str.contains("Library/Logs"),
431 "macOS path should contain Library/Logs: {log_str}"
432 );
433 Ok(())
434 }
435
436 #[test]
439 fn test_discover_log_file_returns_error_in_ci() {
440 let result = discover_log_file();
444 assert!(result.is_err());
445 }
446}