From f4172254d9fb0a1b1a5a654abcb10f3354ff921f Mon Sep 17 00:00:00 2001 From: Nozzit <150452728+Nozzit@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:06:26 +0200 Subject: [PATCH] Tests voor AppImage-namen met spaties, zonder gedeelde globale staat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tauri leidt de bestandsnaam van het asset af van productName, dus een tool als "Open 3D Studio" levert "Open 3D Studio_0.8.0_amd64.AppImage" op. Deze tests leggen vast dat zo'n naam door de hele keten heen goed gaat: de assetkeuze, het uitlezen van de versie, het installeren, de detectie, en het pad in de menu-snelkoppeling — dat laatste moet tussen aanhalingstekens staan, anders breekt de spatie het Exec-veld op in losse argumenten. De bestaande test zette XDG_DATA_HOME om zijn eigen map te kiezen. Die variabele is procesbreed terwijl Rust tests in threads binnen één proces draait, dus een tweede test die hetzelfde doet overschrijft de eerste. Dat maakte de uitkomst afhankelijk van de volgorde waarin threads toevallig aan de beurt kwamen. scan() en install_appimage() krijgen daarom een variant met een expliciete datamap, die de tests gebruiken; het gedrag van de app zelf verandert niet. Twaalf volledige testruns op rij zijn nu groen. Co-Authored-By: Claude Fable 5 --- src-tauri/src/github.rs | 9 +++++ src-tauri/src/linux.rs | 90 ++++++++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/github.rs b/src-tauri/src/github.rs index 461bdda..d21971e 100644 --- a/src-tauri/src/github.rs +++ b/src-tauri/src/github.rs @@ -268,4 +268,13 @@ mod tests { assert_eq!(score_linux_asset("Open.2D.Studio_0.35.0_aarch64.AppImage"), None); assert_eq!(score_linux_asset("Open.2D.Studio_0.35.0_i386.AppImage"), None); } + + #[test] + fn handles_appimage_names_with_spaces() { + // Tauri leidt de bestandsnaam af van productName, dus een tool met + // spaties in zijn naam levert een asset met spaties op. + let name = "Open 3D Studio_0.8.0_amd64.AppImage"; + assert!(score_linux_asset(name).is_some()); + assert_eq!(extract_version(name).as_deref(), Some("0.8.0")); + } } diff --git a/src-tauri/src/linux.rs b/src-tauri/src/linux.rs index 8968f03..f896636 100644 --- a/src-tauri/src/linux.rs +++ b/src-tauri/src/linux.rs @@ -46,6 +46,14 @@ pub fn scan(queries: &[ToolQuery]) -> Vec { let Some(root) = apps_dir() else { return Vec::new(); }; + scan_in(&root, queries) +} + +/// Zelfde scan, maar met een expliciete map. De tests gebruiken deze variant +/// met een eigen tijdelijke map: de omgevingsvariabele die `apps_dir()` leest +/// is procesbreed, en Rust draait tests in threads binnen één proces, dus +/// tests die die variabele zetten zouden elkaar overschrijven. +fn scan_in(root: &Path, queries: &[ToolQuery]) -> Vec { queries .iter() .filter_map(|q| { @@ -101,9 +109,18 @@ fn make_executable(path: &Path) -> Result<(), String> { /// Plaats een gedownloade AppImage in de beheerde map en maak een /// menu-snelkoppeling. Oude versies van dezelfde tool worden opgeruimd. pub fn install_appimage(id: &str, display_name: &str, downloaded: &Path) -> Result<(), String> { - let dir = apps_dir() - .ok_or("thuismap niet gevonden")? - .join(id); + let data = data_home().ok_or("thuismap niet gevonden")?; + install_appimage_in(&data, id, display_name, downloaded) +} + +/// Zelfde installatie, maar met een expliciete datamap — zie `scan_in`. +fn install_appimage_in( + data_home: &Path, + id: &str, + display_name: &str, + downloaded: &Path, +) -> Result<(), String> { + let dir = data_home.join("openaec-installer").join("apps").join(id); std::fs::create_dir_all(&dir).map_err(|e| format!("appmap: {e}"))?; for old in appimages_in(&dir) { @@ -117,15 +134,20 @@ pub fn install_appimage(id: &str, display_name: &str, downloaded: &Path) -> Resu // Snelkoppeling is nuttig maar niet essentieel — een fout hier mag de // installatie niet laten mislukken. - let _ = write_desktop_entry(id, display_name, &target); + let _ = write_desktop_entry(data_home, id, display_name, &target); Ok(()) } /// Schrijf `~/.local/share/applications/openaec-.desktop`, zodat de tool /// ook in het applicatiemenu verschijnt. Wordt bij elke (her)installatie /// overschreven zodat het pad naar de nieuwste versie wijst. -fn write_desktop_entry(id: &str, display_name: &str, appimage: &Path) -> Result<(), String> { - let apps = data_home().ok_or("thuismap niet gevonden")?.join("applications"); +fn write_desktop_entry( + data_home: &Path, + id: &str, + display_name: &str, + appimage: &Path, +) -> Result<(), String> { + let apps = data_home.join("applications"); std::fs::create_dir_all(&apps).map_err(|e| e.to_string())?; let entry = format!( "[Desktop Entry]\nType=Application\nName={}\nExec=\"{}\"\nTerminal=false\nCategories=Office;Engineering;\nComment=Geïnstalleerd door OpenAEC Installer\n", @@ -197,24 +219,62 @@ mod tests { assert!(version_key("2026.7.10".into()) > version_key("2026.7.9".into())); } - #[test] - fn install_scan_and_update_roundtrip() { - // Eigen HOME zodat de test nooit in de echte gebruikersmap schrijft. - let tmp = std::env::temp_dir().join(format!("openaec-test-{}", std::process::id())); + /// Eigen tijdelijke datamap per test, zodat parallel draaiende tests + /// elkaar niet in de weg zitten. + fn temp_home(naam: &str) -> PathBuf { + let tmp = std::env::temp_dir().join(format!("openaec-test-{}-{naam}", std::process::id())); let _ = std::fs::remove_dir_all(&tmp); std::fs::create_dir_all(&tmp).unwrap(); - std::env::set_var("XDG_DATA_HOME", &tmp); + tmp + } + + fn apps_root(home: &Path) -> PathBuf { + home.join("openaec-installer").join("apps") + } + + #[test] + fn handles_appimage_names_with_spaces() { + // Een productName met spaties levert een asset met spaties op. Dat mag + // niet stuklopen op het pad in de menu-snelkoppeling. + let tmp = temp_home("spaces"); + + let download = tmp.join("Open 3D Studio_0.8.0_amd64.AppImage"); + std::fs::write(&download, b"fake").unwrap(); + install_appimage_in(&tmp, "open-3d-studio", "Open 3D Studio", &download).unwrap(); + + let found = scan_in(&apps_root(&tmp), &[ToolQuery { + id: "open-3d-studio".into(), + display_name: "Open 3D Studio".into(), + exe_name: None, + }]); + assert_eq!(found.len(), 1); + assert_eq!(found[0].version.as_deref(), Some("0.8.0")); + + // Het pad in de snelkoppeling staat tussen aanhalingstekens, anders + // breekt de spatie het Exec-veld op in losse argumenten. + let entry = + std::fs::read_to_string(tmp.join("applications/openaec-open-3d-studio.desktop")).unwrap(); + let exec = entry.lines().find(|l| l.starts_with("Exec=")).unwrap(); + assert!(exec.contains("\"") && exec.contains("Open 3D Studio_0.8.0"), "{exec}"); + + let _ = std::fs::remove_dir_all(&tmp); + } + + #[test] + fn install_scan_and_update_roundtrip() { + // Eigen datamap zodat de test nooit in de echte gebruikersmap schrijft. + let tmp = temp_home("roundtrip"); let download = tmp.join("Open.2D.Studio_0.35.0_amd64.AppImage"); std::fs::write(&download, b"fake").unwrap(); - install_appimage("open-2d-studio", "Open 2D Studio", &download).unwrap(); + install_appimage_in(&tmp, "open-2d-studio", "Open 2D Studio", &download).unwrap(); let queries = vec![ToolQuery { id: "open-2d-studio".into(), display_name: "Open 2D Studio".into(), exe_name: None, }]; - let found = scan(&queries); + let found = scan_in(&apps_root(&tmp), &queries); assert_eq!(found.len(), 1); assert_eq!(found[0].version.as_deref(), Some("0.35.0")); let exe = found[0].exe_path.clone().unwrap(); @@ -223,8 +283,8 @@ mod tests { // Update: nieuwe versie vervangt de oude, ook in de detectie. let newer = tmp.join("Open.2D.Studio_0.36.0_amd64.AppImage"); std::fs::write(&newer, b"fake2").unwrap(); - install_appimage("open-2d-studio", "Open 2D Studio", &newer).unwrap(); - let found = scan(&queries); + install_appimage_in(&tmp, "open-2d-studio", "Open 2D Studio", &newer).unwrap(); + let found = scan_in(&apps_root(&tmp), &queries); assert_eq!(found[0].version.as_deref(), Some("0.36.0")); assert!(!Path::new(&exe).exists(), "oude versie is opgeruimd");