Expand test coverage + emit log-path-auto-updated event

- error.rs: 3 tests for the account-specific / transient-network /
    file-rejected classifiers
  - throttle.rs: 2 tests for unlimited passthrough + rate updates
  - folder_monitor.rs: 4 tests for extension parsing + include/exclude
    filter + empty-list behavior
  - updater.rs: 3 tests for semver compare edge cases
  - upload_log: now also emits log-path-auto-updated after persisting
    a working fallback so the renderer's input field updates live.

Test count: 3 → 15 (all pass). Live smoke test: cold + warm start
both land at 28 MB RAM with clean shutdown (0 orphans).
This commit is contained in:
Claude
2026-04-20 18:57:02 +02:00
parent 2958dca282
commit c2d706f6c9
7 changed files with 238 additions and 4 deletions
+33
View File
@@ -152,3 +152,36 @@ impl Serialize for AppError {
}
pub type AppResult<T> = Result<T, AppError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_account_specific() {
assert!(AppError::BadCredentials.is_account_specific());
assert!(AppError::HosterError("voe".into(), "too many requests".into()).is_account_specific());
assert!(AppError::HosterError("byse".into(), "quota exceeded".into()).is_account_specific());
assert!(AppError::HosterError("dood".into(), "CSRF-Token nicht gefunden".into()).is_account_specific());
assert!(AppError::Other("HTTP 429".into()).is_account_specific());
assert!(!AppError::Other("ENOTFOUND foo".into()).is_account_specific());
}
#[test]
fn classify_transient_network() {
assert!(AppError::Other("getaddrinfo ENOTFOUND s1055.filemoon".into()).is_transient_network());
assert!(AppError::Other("ECONNRESET".into()).is_transient_network());
assert!(AppError::Other("socket hang up".into()).is_transient_network());
assert!(!AppError::Other("quota exceeded".into()).is_transient_network());
assert!(!AppError::BadCredentials.is_transient_network());
}
#[test]
fn classify_file_rejected() {
assert!(AppError::FileRejected("Not video file format".into()).is_file_rejected());
assert!(AppError::HosterError("byse".into(), "Byse lehnte Datei ab: Not video file format".into()).is_file_rejected());
assert!(AppError::Other("Duplicate detected".into()).is_file_rejected());
assert!(!AppError::Other("quota".into()).is_file_rejected());
}
}
+34
View File
@@ -171,6 +171,40 @@ fn path_matches(p: &Path, extensions: &[String], include: bool) -> bool {
if include { listed } else { !listed }
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn parse_extensions_strips_dots_and_whitespace() {
assert_eq!(parse_extensions(".mp4, mkv, .avi"), vec!["mp4", "mkv", "avi"]);
assert_eq!(parse_extensions(""), Vec::<String>::new());
assert_eq!(parse_extensions(", ,"), Vec::<String>::new());
}
#[test]
fn include_filter_accepts_listed() {
let p = PathBuf::from("video.mp4");
assert!(path_matches(&p, &vec!["mp4".into(), "mkv".into()], true));
assert!(!path_matches(&p, &vec!["avi".into()], true));
}
#[test]
fn exclude_filter_rejects_listed() {
let p = PathBuf::from("video.mp4");
assert!(!path_matches(&p, &vec!["mp4".into()], false));
assert!(path_matches(&p, &vec!["avi".into()], false));
}
#[test]
fn empty_extensions_accepts_everything() {
let p = PathBuf::from("foo.anything");
assert!(path_matches(&p, &[], true));
assert!(path_matches(&p, &[], false));
}
}
fn walk_collect(dir: &Path, recursive: bool, out: &mut HashSet<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for entry in rd.flatten() {
+25
View File
@@ -36,7 +36,13 @@ impl Throttle {
}
}
#[cfg(test)]
pub fn max_bps(&self) -> u64 { self.inner.lock().max_bps }
/// Block until `bytes` worth of tokens are available.
#[cfg(test)]
pub fn available_tokens(&self) -> f64 { self.inner.lock().tokens }
pub async fn consume(&self, mut bytes: u64) {
loop {
let (take, remaining) = {
@@ -57,3 +63,22 @@ impl Throttle {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unlimited_is_instant() {
let t = Throttle::new(0);
assert_eq!(t.max_bps(), 0);
}
#[test]
fn set_rate_updates_limit() {
let t = Throttle::new(1000);
t.set_rate(500);
assert_eq!(t.max_bps(), 500);
assert!(t.available_tokens() <= 500.0);
}
}
+25
View File
@@ -79,6 +79,31 @@ pub async fn download_and_launch(app: tauri::AppHandle) -> Result<(), String> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semver_compare_triggers_update() {
let newer = Version::parse("2.1.0").unwrap();
let current = Version::parse("2.0.0").unwrap();
assert!(newer > current);
}
#[test]
fn semver_equal_does_not_trigger() {
let v1 = Version::parse("2.0.0").unwrap();
let v2 = Version::parse("2.0.0").unwrap();
assert!(!(v1 > v2));
}
#[test]
fn semver_strips_v_prefix() {
assert!(Version::parse("2.0.0".trim_start_matches('v')).is_ok());
assert!(Version::parse("v2.0.0".trim_start_matches('v')).is_ok());
}
}
pub async fn check() -> UpdateCheck {
let current = env!("CARGO_PKG_VERSION").to_string();
let mut out = UpdateCheck {
+5 -2
View File
@@ -137,17 +137,20 @@ impl UploadLogWriter {
if let Some(state) = self.app.try_state::<crate::commands::AppState>() {
let store = state.config.clone();
let to_save = path.to_path_buf();
let app = self.app.clone();
tauri::async_runtime::spawn(async move {
if let Ok(cfg) = store.load() {
let mut gs = cfg.global_settings.clone();
// Strip daily suffix when daily-log is active — same as v1.
let save_path = if gs.session_log {
strip_daily_suffix(&to_save)
} else {
to_save.clone()
};
gs.log_file_path = save_path.display().to_string();
let _ = store.save_global(gs).await;
if store.save_global(gs).await.is_ok() {
let _ = app.emit("log-path-auto-updated",
serde_json::json!({ "logFilePath": save_path.display().to_string() }));
}
}
});
}
+8 -2
View File
@@ -28,7 +28,10 @@
},
"bundle": {
"active": true,
"targets": ["nsis", "msi"],
"targets": [
"nsis",
"msi"
],
"publisher": "xrangerde",
"shortDescription": "Multi-Hoster file uploader",
"longDescription": "Upload files to multiple video hosters with fallback accounts, retry logic and progress tracking.",
@@ -41,7 +44,10 @@
"nsis": {
"installMode": "perMachine",
"installerIcon": "icons/icon.ico",
"languages": ["German", "English"]
"languages": [
"German",
"English"
]
}
}
}