ge_man/
filesystem.rs

1use std::io::Read;
2use std::os::unix::ffi::OsStrExt;
3use std::path::Path;
4use std::{fs, io};
5
6use anyhow::{anyhow, bail, Context};
7use ge_man_lib::archive;
8use ge_man_lib::config::{LutrisConfig, SteamConfig};
9use ge_man_lib::tag::TagKind;
10#[cfg(test)]
11use mockall::{automock, predicate::*};
12
13use crate::data::ManagedVersion;
14use crate::path::{
15    steam_path, xdg_config_home, xdg_data_home, PathConfiguration, LUTRIS_WINE_RUNNERS_DIR, STEAM_COMP_DIR,
16};
17use crate::version::{Version, Versioned};
18
19const USER_SETTINGS_PY: &str = "user_settings.py";
20const LUTRIS_INITIAL_WINE_RUNNER_CONFIG: &str = r#"
21wine:
22  version: VERSION
23"#;
24
25#[cfg_attr(test, automock)]
26pub trait FilesystemManager {
27    fn setup_version(&self, version: Version, compressed_tar: Box<dyn Read>) -> anyhow::Result<ManagedVersion>;
28    fn remove_version(&self, version: &ManagedVersion) -> anyhow::Result<()>;
29    fn migrate_folder(&self, version: Version, source_path: &Path) -> anyhow::Result<ManagedVersion>;
30    fn apply_to_app_config(&self, version: &ManagedVersion) -> anyhow::Result<()>;
31    fn copy_user_settings(&self, src_version: &ManagedVersion, dst_version: &ManagedVersion) -> anyhow::Result<()>;
32}
33
34/// Inside this struct it is assumed that all data passed to the methods of this struct contain valid data which
35/// passed clap's or the ui module's validations.
36pub struct FsMng<'a> {
37    path_config: &'a dyn PathConfiguration,
38}
39
40impl<'a> FsMng<'a> {
41    pub fn new(path_config: &'a dyn PathConfiguration) -> Self {
42        FsMng { path_config }
43    }
44
45    fn copy_directory(&self, src: &Path, dst: &Path) -> anyhow::Result<()> {
46        fs::create_dir_all(dst).unwrap();
47        for entry in src.read_dir()? {
48            let dir_entry = entry?;
49            let dst = dst.join(dir_entry.file_name());
50
51            if dir_entry.path().is_dir() {
52                self.copy_directory(&dir_entry.path(), &dst)?;
53            } else {
54                fs::copy(&dir_entry.path(), &dst)?;
55            }
56        }
57
58        Ok(())
59    }
60
61    fn move_or_copy_directory(&self, version: &ManagedVersion, src_path: &Path) -> anyhow::Result<()> {
62        let dst_path = match version.kind() {
63            TagKind::Proton => self.path_config.steam_compatibility_tools_dir(steam_path()),
64            TagKind::Wine { .. } => self.path_config.lutris_runners_dir(xdg_data_home()),
65        };
66        let dst_path = dst_path.join(version.directory_name());
67
68        // A rename is used here to move the directory into the destination folder. We could just copy the files but
69        // Proton GE releases tend to be 400 MB in size and Wine GE releases about 100 MB.
70        if let Err(err) = fs::rename(src_path, &dst_path) {
71            match err.raw_os_error() {
72                // Rename only works when the source and destination are on the same device. In the case that the
73                // destination is a different device the source must be copied to the destination.
74                Some(18) => {
75                    self.copy_directory(src_path, &dst_path).context(format!(
76                        "Failed to copy source to destination.\n\
77                                         Source: {}\n\
78                                         Destination: {}\n",
79                        src_path.display(),
80                        dst_path.display(),
81                    ))?;
82                }
83                _ => bail!(err),
84            }
85        }
86
87        Ok(())
88    }
89}
90
91impl<'a> FilesystemManager for FsMng<'a> {
92    fn setup_version(&self, version: Version, compressed_tar: Box<dyn Read>) -> anyhow::Result<ManagedVersion> {
93        let dst_path = match version.kind() {
94            TagKind::Proton => self.path_config.steam_compatibility_tools_dir(steam_path()),
95            TagKind::Wine { .. } => self.path_config.lutris_runners_dir(xdg_data_home()),
96        };
97        let extracted_location = archive::extract_compressed(version.kind(), compressed_tar, &dst_path)
98            .context("Failed to extract compressed archive")?;
99
100        let directory_name = String::from_utf8_lossy(extracted_location.file_name().unwrap().as_bytes()).into_owned();
101
102        let mut version = ManagedVersion::from(version);
103        version.set_directory_name(directory_name);
104
105        Ok(version)
106    }
107
108    fn remove_version(&self, version: &ManagedVersion) -> anyhow::Result<()> {
109        let path = match version.kind() {
110            TagKind::Proton => self.path_config.steam_compatibility_tools_dir(steam_path()),
111            TagKind::Wine { .. } => self.path_config.lutris_runners_dir(xdg_data_home()),
112        };
113        let path = path.join(version.directory_name());
114
115        fs::remove_dir_all(&path).context(format!("Could not remove directory '{}'", path.display()))
116    }
117
118    fn migrate_folder(&self, version: Version, source_path: &Path) -> anyhow::Result<ManagedVersion> {
119        let mut managed_version = ManagedVersion::from(version);
120        let dir_name = format!("GEH_{}_{}", managed_version.kind(), managed_version.tag());
121        managed_version.set_directory_name(dir_name);
122
123        match source_path.parent() {
124            Some(parent) => {
125                if parent.ends_with(STEAM_COMP_DIR) || parent.ends_with(LUTRIS_WINE_RUNNERS_DIR) {
126                    managed_version
127                        .set_directory_name(String::from_utf8_lossy(source_path.file_name().unwrap().as_bytes()));
128                    return Ok(managed_version);
129                } else {
130                    self.move_or_copy_directory(&managed_version, source_path)?;
131                }
132            }
133            None => self.move_or_copy_directory(&managed_version, source_path)?,
134        }
135
136        Ok(managed_version)
137    }
138
139    fn apply_to_app_config(&self, version: &ManagedVersion) -> anyhow::Result<()> {
140        match version.kind() {
141            TagKind::Proton => {
142                let steam_cfg_path = self.path_config.steam_config(steam_path());
143                let backup_path = self
144                    .path_config
145                    .app_config_backup_file(xdg_config_home(), version.kind());
146
147                fs::copy(&steam_cfg_path, &backup_path).context(format!(
148                    r#"Could not create backup of Steam config from "{}" to "{}" "#,
149                    steam_cfg_path.display(),
150                    backup_path.display()
151                ))?;
152
153                let mut config = SteamConfig::create_copy(&steam_cfg_path)?;
154                config.set_proton_version(version.directory_name());
155
156                let new_config: Vec<u8> = config.into();
157                fs::write(steam_cfg_path, new_config)?;
158            }
159            TagKind::Wine { .. } => {
160                let runner_cfg_path = self.path_config.lutris_wine_runner_config(xdg_config_home());
161                let backup_path = self
162                    .path_config
163                    .app_config_backup_file(xdg_config_home(), version.kind());
164
165                let copy_result = fs::copy(&runner_cfg_path, &backup_path);
166
167                if let Err(io_err) = copy_result {
168                    if let io::ErrorKind::NotFound = io_err.kind() {
169                        fs::write(
170                            runner_cfg_path,
171                            LUTRIS_INITIAL_WINE_RUNNER_CONFIG.replace("VERSION", version.directory_name()),
172                        )
173                        .context("Failed to create initial Wine runner configuration for Lutris")?;
174                    } else {
175                        return Err(anyhow!(io_err)).context(format!(
176                            r#"Could not create backup of Wine runner config from "{}" to "{}""#,
177                            runner_cfg_path.display(),
178                            backup_path.display()
179                        ));
180                    }
181                } else {
182                    let mut config = LutrisConfig::create_copy(&runner_cfg_path)?;
183                    config.set_wine_version(version.directory_name());
184
185                    let new_config: Vec<u8> = config.into();
186                    fs::write(runner_cfg_path, new_config)?;
187                };
188            }
189        }
190
191        Ok(())
192    }
193
194    fn copy_user_settings(&self, src_version: &ManagedVersion, dst_version: &ManagedVersion) -> anyhow::Result<()> {
195        let src_path = self
196            .path_config
197            .steam_compatibility_tools_dir(steam_path())
198            .join(src_version.directory_name())
199            .join(USER_SETTINGS_PY);
200        let dst_path = self
201            .path_config
202            .steam_compatibility_tools_dir(steam_path())
203            .join(dst_version.directory_name())
204            .join(USER_SETTINGS_PY);
205
206        fs::copy(src_path, dst_path).context(format!(
207            "Could not copy user_settings.py from {} to {}",
208            src_version, dst_version
209        ))?;
210        Ok(())
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use std::fs::File;
217    use std::io::BufReader;
218    use std::path::PathBuf;
219
220    use assert_fs::prelude::{PathAssert, PathChild};
221    use assert_fs::TempDir;
222    use ge_man_lib::tag::Tag;
223
224    use super::*;
225
226    struct MockPathConfig {
227        pub tmp_dir: PathBuf,
228    }
229
230    impl MockPathConfig {
231        pub fn new(tmp_dir: PathBuf) -> Self {
232            MockPathConfig { tmp_dir }
233        }
234    }
235
236    impl PathConfiguration for MockPathConfig {
237        fn xdg_data_dir(&self, _xdg_data_path: Option<String>) -> PathBuf {
238            self.tmp_dir.join(".local/share")
239        }
240
241        fn xdg_config_dir(&self, _xdg_config_path: Option<String>) -> PathBuf {
242            self.tmp_dir.join(".config")
243        }
244
245        fn steam(&self, _steam_root_path_override: Option<String>) -> PathBuf {
246            self.tmp_dir.join(".steam/root")
247        }
248    }
249
250    #[test]
251    fn setup_proton_version() {
252        let tag = String::from("6.20-GE-1");
253        let kind = TagKind::Proton;
254        let dir_name = "Proton-6.20-GE-1";
255
256        let tmp_dir = TempDir::new().unwrap();
257        let path_config = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
258        fs::create_dir_all(path_config.steam_compatibility_tools_dir(None)).unwrap();
259
260        let fs_manager = FsMng::new(&path_config);
261
262        let compressed_tar = BufReader::new(File::open("test_resources/assets/Proton-6.20-GE-1.tar.gz").unwrap());
263        let version = Version::new(tag.clone(), kind.clone());
264        let managed_version = fs_manager.setup_version(version, Box::new(compressed_tar)).unwrap();
265
266        assert_eq!(managed_version.tag(), &Tag::from(tag));
267        assert_eq!(managed_version.kind(), &kind);
268        assert_eq!(managed_version.directory_name(), &dir_name);
269        tmp_dir
270            .child(".steam/root/compatibilitytools.d")
271            .child(&dir_name)
272            .assert(predicates::path::exists());
273
274        drop(fs_manager);
275        tmp_dir.close().unwrap();
276    }
277
278    #[test]
279    fn setup_wine_version() {
280        let tag = String::from("6.20-GE-1");
281        let kind = TagKind::wine();
282        let dir_name = "Wine-6.20-GE-1";
283
284        let tmp_dir = TempDir::new().unwrap();
285        let path_config = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
286        fs::create_dir_all(path_config.lutris_runners_dir(None)).unwrap();
287
288        let fs_manager = FsMng::new(&path_config);
289
290        let compressed_tar = BufReader::new(File::open("test_resources/assets/Wine-6.20-GE-1.tar.xz").unwrap());
291        let version = Version::new(tag.clone(), kind.clone());
292        let managed_version = fs_manager.setup_version(version, Box::new(compressed_tar)).unwrap();
293
294        assert_eq!(managed_version.tag(), &Tag::from(tag));
295        assert_eq!(managed_version.kind(), &kind);
296        assert_eq!(managed_version.directory_name(), &dir_name);
297        tmp_dir
298            .child(".local/share/lutris/runners/wine")
299            .child(&dir_name)
300            .assert(predicates::path::exists());
301
302        drop(fs_manager);
303        tmp_dir.close().unwrap();
304    }
305
306    #[test]
307    fn setup_wine_lol_version() {
308        let tag = String::from("6.20-GE-1");
309        let kind = TagKind::lol();
310        let dir_name = "Wine-6.20-GE-1-LoL";
311
312        let tmp_dir = TempDir::new().unwrap();
313        let path_config = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
314        fs::create_dir_all(path_config.lutris_runners_dir(None)).unwrap();
315
316        let fs_manager = FsMng::new(&path_config);
317
318        let compressed_tar = BufReader::new(File::open("test_resources/assets/Wine-6.20-GE-1-LoL.tar.xz").unwrap());
319        let version = Version::new(tag.clone(), kind.clone());
320        let managed_version = fs_manager.setup_version(version, Box::new(compressed_tar)).unwrap();
321
322        assert_eq!(managed_version.tag(), &Tag::from(tag));
323        assert_eq!(managed_version.kind(), &kind);
324        assert_eq!(managed_version.directory_name(), &dir_name);
325        tmp_dir
326            .child(".local/share/lutris/runners/wine")
327            .child(&dir_name)
328            .assert(predicates::path::exists());
329
330        drop(fs_manager);
331        tmp_dir.close().unwrap();
332    }
333
334    #[test]
335    fn remove_proton_version() {
336        let tag = String::from("6.20-GE-1");
337        let dir_name = String::from("Proton-6.20-GE-1");
338        let kind = TagKind::Proton;
339
340        let tmp_dir = TempDir::new().unwrap();
341        let path_config = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
342        std::fs::create_dir_all(path_config.steam_compatibility_tools_dir(None).join(&dir_name)).unwrap();
343
344        let fs_manager = FsMng::new(&path_config);
345
346        let version = ManagedVersion::new(Tag::from(tag), kind, dir_name.clone());
347        fs_manager.remove_version(&version).unwrap();
348
349        tmp_dir
350            .child(".local/share/game-compatibility-manager/versions/proton-ge")
351            .child(&dir_name)
352            .assert(predicates::path::missing());
353
354        drop(fs_manager);
355        tmp_dir.close().unwrap();
356    }
357
358    #[test]
359    fn remove_wine_version() {
360        let tag = String::from("6.20-GE-1");
361        let dir_name = String::from("Wine-6.20-GE-1");
362        let kind = TagKind::wine();
363
364        let tmp_dir = TempDir::new().unwrap();
365        let path_config = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
366        std::fs::create_dir_all(path_config.lutris_runners_dir(None).join(&dir_name)).unwrap();
367
368        let fs_manager = FsMng::new(&path_config);
369
370        let version = ManagedVersion::new(Tag::from(tag), kind, dir_name.clone());
371        fs_manager.remove_version(&version).unwrap();
372
373        tmp_dir
374            .child(".local/share/game-compatibility-manager/versions/wine-ge")
375            .child(&dir_name)
376            .assert(predicates::path::missing());
377
378        drop(fs_manager);
379        tmp_dir.close().unwrap();
380    }
381
382    #[test]
383    fn remove_wine_lol_version() {
384        let tag = String::from("6.20-GE-1-LoL");
385        let dir_name = String::from("Wine-6.20-GE-1-LoL");
386        let kind = TagKind::lol();
387
388        let tmp_dir = TempDir::new().unwrap();
389        let path_config = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
390        std::fs::create_dir_all(path_config.lutris_runners_dir(None).join(&dir_name)).unwrap();
391
392        let fs_manager = FsMng::new(&path_config);
393
394        let version = ManagedVersion::new(Tag::from(tag), kind, dir_name.clone());
395        fs_manager.remove_version(&version).unwrap();
396
397        tmp_dir
398            .child(".local/share/game-compatibility-manager/versions/wine-ge")
399            .child(&dir_name)
400            .assert(predicates::path::missing());
401
402        drop(fs_manager);
403        tmp_dir.close().unwrap();
404    }
405
406    #[test]
407    fn migrate_proton_version_in_steam_dir() {
408        let tmp_dir = TempDir::new().unwrap();
409        let version = Version::new("6.20-GE-1", TagKind::Proton);
410        let source_path = PathBuf::from(tmp_dir.join(".local/share/Steam/compatibilitytools.d/Proton-6.20-GE-1"));
411        fs::create_dir_all(&source_path).unwrap();
412
413        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
414        let fs_mng = FsMng::new(&path_cfg);
415
416        let version = fs_mng.migrate_folder(version, &source_path).unwrap();
417        assert_eq!(version.tag(), &Tag::from("6.20-GE-1"));
418        assert_eq!(version.kind(), &TagKind::Proton);
419        assert_eq!(version.directory_name(), &String::from("Proton-6.20-GE-1"));
420
421        tmp_dir
422            .child(".local/share/Steam/compatibilitytools.d/GEH_PROTON_6.20-GE-1")
423            .assert(predicates::path::missing());
424        tmp_dir
425            .child(".local/share/Steam/compatibilitytools.d/Proton-6.20-GE-1")
426            .assert(predicates::path::exists());
427
428        drop(fs_mng);
429        tmp_dir.close().unwrap();
430    }
431
432    #[test]
433    fn migrate_proton_version_present_in_random_directory() {
434        let tmp_dir = TempDir::new().unwrap();
435        let source_path = PathBuf::from(tmp_dir.join("some/dir/Proton-6.20-GE-1"));
436        let version = Version::new("6.20-GE-1", TagKind::Proton);
437        fs::create_dir_all(&source_path).unwrap();
438        fs::create_dir_all(tmp_dir.join(".steam/root/compatibilitytools.d")).unwrap();
439
440        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
441        let fs_mng = FsMng::new(&path_cfg);
442
443        let version = fs_mng.migrate_folder(version, &source_path).unwrap();
444        assert_eq!(version.tag(), &Tag::from("6.20-GE-1"));
445        assert_eq!(version.kind(), &TagKind::Proton);
446        assert_eq!(version.directory_name(), &String::from("GEH_PROTON_6.20-GE-1"));
447
448        tmp_dir
449            .child(".steam/root/compatibilitytools.d/Proton-6.20-GE-1")
450            .assert(predicates::path::missing());
451        tmp_dir
452            .child(".steam/root/compatibilitytools.d/GEH_PROTON_6.20-GE-1")
453            .assert(predicates::path::exists());
454
455        drop(fs_mng);
456        tmp_dir.close().unwrap();
457    }
458
459    #[test]
460    fn migrate_wine_version_in_lutris_directory() {
461        let tmp_dir = TempDir::new().unwrap();
462        let source_path = PathBuf::from(tmp_dir.join(".local/share/lutris/runners/wine/Wine-6.20-GE-1"));
463        let version = Version::new("6.20-GE-1", TagKind::wine());
464        fs::create_dir_all(&source_path).unwrap();
465        fs::create_dir_all(tmp_dir.join(".local/share/lutris/runners/wine")).unwrap();
466
467        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
468        let fs_mng = FsMng::new(&path_cfg);
469
470        let version = fs_mng.migrate_folder(version, &source_path).unwrap();
471        assert_eq!(version.tag(), &Tag::from("6.20-GE-1"));
472        assert_eq!(version.kind(), &TagKind::wine());
473        assert_eq!(version.directory_name(), &String::from("Wine-6.20-GE-1"));
474
475        tmp_dir
476            .child(".local/share/lutris/runners/wine/GEH_Wine_6.20-GE-1")
477            .assert(predicates::path::missing());
478        tmp_dir
479            .child(".local/share/lutris/runners/wine/Wine-6.20-GE-1")
480            .assert(predicates::path::exists());
481
482        drop(fs_mng);
483        tmp_dir.close().unwrap();
484    }
485
486    #[test]
487    fn migrate_wine_version_in_random_directory() {
488        let tmp_dir = TempDir::new().unwrap();
489        let source_path = PathBuf::from(tmp_dir.join("some/dir/Wine-6.20-GE-1"));
490        let version = Version::new("6.20-GE-1", TagKind::wine());
491        fs::create_dir_all(&source_path).unwrap();
492        fs::create_dir_all(tmp_dir.join(".local/share/lutris/runners/wine")).unwrap();
493
494        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
495        let fs_mng = FsMng::new(&path_cfg);
496
497        let version = fs_mng.migrate_folder(version, &source_path).unwrap();
498        assert_eq!(version.tag(), &Tag::from("6.20-GE-1"));
499        assert_eq!(version.kind(), &TagKind::wine());
500        assert_eq!(version.directory_name(), &String::from("GEH_WINE_6.20-GE-1"));
501
502        tmp_dir
503            .child(".local/share/lutris/runners/wine/Wine-6.20-GE-1")
504            .assert(predicates::path::missing());
505        tmp_dir
506            .child(".local/share/lutris/runners/wine/GEH_WINE_6.20-GE-1")
507            .assert(predicates::path::exists());
508
509        drop(fs_mng);
510        tmp_dir.close().unwrap();
511    }
512
513    #[test]
514    fn migrate_lol_version_in_lutris_directory() {
515        let tmp_dir = TempDir::new().unwrap();
516        let source_path = PathBuf::from(tmp_dir.join(".local/share/lutris/runners/wine/Wine-LoL-6.20-GE-1"));
517        let version = Version::new("6.20-GE-1", TagKind::lol());
518        fs::create_dir_all(&source_path).unwrap();
519        fs::create_dir_all(tmp_dir.join(".local/share/lutris/runners/wine")).unwrap();
520
521        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
522        let fs_mng = FsMng::new(&path_cfg);
523
524        let version = fs_mng.migrate_folder(version, &source_path).unwrap();
525        assert_eq!(version.tag(), &Tag::from("6.20-GE-1"));
526        assert_eq!(version.kind(), &TagKind::lol());
527        assert_eq!(version.directory_name(), &String::from("Wine-LoL-6.20-GE-1"));
528
529        tmp_dir
530            .child(".local/share/lutris/runners/wine/GEH_LoL_Wine_6.20-GE-1")
531            .assert(predicates::path::missing());
532        tmp_dir
533            .child(".local/share/lutris/runners/wine/Wine-LoL-6.20-GE-1")
534            .assert(predicates::path::exists());
535
536        drop(fs_mng);
537        tmp_dir.close().unwrap();
538    }
539
540    #[test]
541    fn migrate_lol_version_in_random_directory() {
542        let tmp_dir = TempDir::new().unwrap();
543        let source_path = PathBuf::from(tmp_dir.join("some/dir/Wine-LoL-6.20-GE-1"));
544        let version = Version::new("6.20-GE-1", TagKind::lol());
545        fs::create_dir_all(&source_path).unwrap();
546        fs::create_dir_all(tmp_dir.join(".local/share/lutris/runners/wine")).unwrap();
547
548        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
549        let fs_mng = FsMng::new(&path_cfg);
550
551        let version = fs_mng.migrate_folder(version, &source_path).unwrap();
552        assert_eq!(version.tag(), &Tag::from("6.20-GE-1"));
553        assert_eq!(version.kind(), &TagKind::lol());
554        assert_eq!(version.directory_name(), &String::from("GEH_LOL_WINE_6.20-GE-1"));
555
556        tmp_dir
557            .child(".local/share/lutris/runners/wine/Wine-LoL-6.20-GE-1")
558            .assert(predicates::path::missing());
559        tmp_dir
560            .child(".local/share/lutris/runners/wine/GEH_LOL_WINE_6.20-GE-1")
561            .assert(predicates::path::exists());
562
563        drop(fs_mng);
564        tmp_dir.close().unwrap();
565    }
566
567    #[test]
568    fn apply_proton_ge_version_to_steam_config() {
569        let tmp_dir = TempDir::new().unwrap();
570        let steam_cfg_dir = tmp_dir.join(".steam/root/config");
571        let steam_cfg_file = steam_cfg_dir.join("config.vdf");
572        let proton_dir_name = "Proton-6.20-GE-1";
573        fs::create_dir_all(&steam_cfg_dir).unwrap();
574        fs::copy("test_resources/assets/config.vdf", &steam_cfg_file).unwrap();
575
576        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
577        fs::create_dir_all(
578            path_cfg
579                .app_config_backup_file(None, &TagKind::Proton)
580                .parent()
581                .unwrap(),
582        )
583        .unwrap();
584        let fs_mng = FsMng::new(&path_cfg);
585
586        let version = ManagedVersion::new("6.20-GE-1", TagKind::Proton, proton_dir_name);
587        fs_mng.apply_to_app_config(&version).unwrap();
588
589        let modified_config = SteamConfig::create_copy(&steam_cfg_file).unwrap();
590        assert_eq!(modified_config.proton_version(), proton_dir_name);
591
592        tmp_dir
593            .child(path_cfg.app_config_backup_file(None, &TagKind::Proton))
594            .assert(predicates::path::exists());
595
596        drop(fs_mng);
597        tmp_dir.close().unwrap();
598    }
599
600    #[test]
601    fn apply_wine_ge_version_to_lutris_config_when_runner_config_already_exists() {
602        let tmp_dir = TempDir::new().unwrap();
603        let cfg_dir = tmp_dir.join(".config/lutris/runners");
604        let cfg_file = cfg_dir.join("wine.yml");
605        let dir_name = "Wine-6.20-GE-1";
606        fs::create_dir_all(&cfg_dir).unwrap();
607        fs::copy("test_resources/assets/wine.yml", &cfg_file).unwrap();
608
609        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
610        fs::create_dir_all(
611            path_cfg
612                .app_config_backup_file(None, &TagKind::wine())
613                .parent()
614                .unwrap(),
615        )
616        .unwrap();
617        let fs_mng = FsMng::new(&path_cfg);
618
619        let version = ManagedVersion::new("6.20-GE-1", TagKind::wine(), dir_name);
620        fs_mng.apply_to_app_config(&version).unwrap();
621
622        let modified_config = LutrisConfig::create_copy(&cfg_file).unwrap();
623        assert_eq!(modified_config.wine_version(), dir_name);
624
625        tmp_dir
626            .child(path_cfg.app_config_backup_file(None, &TagKind::wine()))
627            .assert(predicates::path::exists());
628
629        drop(fs_mng);
630        tmp_dir.close().unwrap();
631    }
632
633    #[test]
634    fn apply_wine_ge_version_to_lutris_config_when_no_runner_config_exists() {
635        let tmp_dir = TempDir::new().unwrap();
636        let cfg_dir = tmp_dir.join(".config/lutris/runners");
637        let cfg_file = cfg_dir.join("wine.yml");
638        let dir_name = "Wine-6.21-GE-1";
639        fs::create_dir_all(&cfg_dir).unwrap();
640
641        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
642        fs::create_dir_all(
643            path_cfg
644                .app_config_backup_file(None, &TagKind::wine())
645                .parent()
646                .unwrap(),
647        )
648        .unwrap();
649        let fs_mng = FsMng::new(&path_cfg);
650
651        let version = ManagedVersion::new("6.21-GE-1", TagKind::wine(), dir_name);
652        fs_mng.apply_to_app_config(&version).unwrap();
653
654        let modified_config = LutrisConfig::create_copy(&cfg_file).unwrap();
655        assert_eq!(modified_config.wine_version(), dir_name);
656
657        tmp_dir
658            .child(path_cfg.app_config_backup_file(None, &TagKind::wine()))
659            .assert(predicates::path::missing());
660
661        drop(fs_mng);
662        tmp_dir.close().unwrap();
663    }
664
665    #[test]
666    fn copy_proton_settings() {
667        let tmp_dir = TempDir::new().unwrap();
668        fs::create_dir_all(tmp_dir.join(".steam/root/compatibilitytools.d")).unwrap();
669        fs::create_dir_all(tmp_dir.join(".steam/root/config")).unwrap();
670
671        let path_cfg = MockPathConfig::new(PathBuf::from(tmp_dir.path()));
672        let fs_mng = FsMng::new(&path_cfg);
673
674        let src = Version::new("6.19-GE-1", TagKind::Proton);
675        let src_tar = File::open("test_resources/assets/Proton-6.19-GE-1.tar.gz").unwrap();
676        let dst = Version::new("6.20-GE-2", TagKind::Proton);
677        let dst_tar = File::open("test_resources/assets/Proton-6.20-GE-2.tar.gz").unwrap();
678
679        let src = fs_mng.setup_version(src, Box::new(src_tar)).unwrap();
680        let dst = fs_mng.setup_version(dst, Box::new(dst_tar)).unwrap();
681
682        tmp_dir
683            .child(".steam/root/compatibilitytools.d/Proton-6.19-GE-1")
684            .assert(predicates::path::exists());
685        tmp_dir
686            .child(".steam/root/compatibilitytools.d/Proton-6.20-GE-2")
687            .assert(predicates::path::exists());
688
689        fs::copy(
690            tmp_dir.join(".steam/root/compatibilitytools.d/Proton-6.19-GE-1/hello-world.txt"),
691            tmp_dir.join(".steam/root/compatibilitytools.d/Proton-6.19-GE-1/user_settings.py"),
692        )
693        .unwrap();
694
695        tmp_dir
696            .child(".steam/root/compatibilitytools.d/Proton-6.19-GE-1/user_settings.py")
697            .assert(predicates::path::exists());
698
699        fs_mng.copy_user_settings(&src, &dst).unwrap();
700
701        tmp_dir
702            .child(".steam/root/compatibilitytools.d/Proton-6.20-GE-2/user_settings.py")
703            .assert(predicates::path::exists());
704
705        tmp_dir.close().unwrap();
706    }
707}