chore(rust): clear the clippy backlog and finish the poison-tolerant lock sweep
`cargo clippy --all-targets` went from 51 warnings (23 in the lib) to zero. Most were mechanical — needless borrows, `assert_eq!` against a bool literal, `vec!` where an array does, `or_insert_with(Vec::new)`, a loop index used only to index — and were applied with `clippy --fix`, then reviewed line by line. That review caught one auto-fix that was *not* semantically neutral: dropping the redundant `use hostname;` left its `#[cfg(target_os = "linux")]` orphaned directly above `SERVICE_NAME`, which would have silently cfg'd the constant out of every non-Linux build. Removed the stray attribute with the import. Where a lint asked for a risky change rather than a better one, it is suppressed with a comment saying why: - `too_many_arguments` on five `#[tauri::command]` handlers and `ThumbnailCache::save_thumbnail` — most of the arity is `State<'_, _>` injection, and a parameter struct would change the IPC contract and the generated TypeScript for no readability gain. - `large_enum_variant` on `PlayerStatusEvent` and `AutoplayDecision` — both are serde + specta wire types emitted a handful of times a second, never bulk allocated; boxing would have to stay invisible to the generated bindings while every match arm gained a deref. - `await_holding_lock` on the `hybrid`/`offline` test modules — the guard is a test-only serialisation lock for the process-global `INCLUDE_CATALOG_BROWSE` flag, and the await it spans *is* the critical section. Each `#[tokio::test]` gets its own single-threaded runtime, so this is not the production deadlock class the lint targets; restructuring would reintroduce the flag race. Real fixes elsewhere: `JellyfinItem::to_media_item` takes `self` by value, so it is now `into_media_item`; the five-tuple episode row in the download commands has a named `EpisodeRow` alias; the mpv `PropertyChange` arm matches `name: "pause"` instead of guarding on it. Also converted the last 27 raw `.lock().unwrap()` call sites to `lock_safe()`, completing the `MutexSafe`/`RwLockSafe` convention. All of them turned out to be in test modules — production code was already clean — so this is consistency rather than a fix. The two raw locks in `utils/lock.rs` stay raw on purpose: those tests deliberately poison a mutex to prove the helpers recover from it. Pure refactoring: all 698 tests still pass.
This commit is contained in:
@@ -418,13 +418,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_auth_manager_wrapper_structure() {
|
||||
// Verify wrapper type exists and has correct structure
|
||||
assert_eq!(std::mem::size_of::<AuthManagerWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<AuthManagerWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_verifier_wrapper_structure() {
|
||||
// Verify wrapper type exists and has correct structure
|
||||
assert_eq!(std::mem::size_of::<SessionVerifierWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<SessionVerifierWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -496,7 +496,7 @@ pub(crate) async fn requeue_mistyped_video_downloads(
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let query = Query::new(&format!(
|
||||
let query = Query::new(format!(
|
||||
"UPDATE downloads
|
||||
SET status = 'pending', stream_url = NULL, progress = 0,
|
||||
bytes_downloaded = 0, started_at = NULL, completed_at = NULL
|
||||
@@ -563,7 +563,7 @@ where
|
||||
),
|
||||
None => String::new(),
|
||||
};
|
||||
let rows_query = Query::new(&format!(
|
||||
let rows_query = Query::new(format!(
|
||||
"SELECT d.id, d.item_id,
|
||||
COALESCE(
|
||||
d.media_type,
|
||||
@@ -753,6 +753,7 @@ pub async fn resume_queued_downloads(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::storage::db_service::RusqliteService;
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -1012,14 +1013,14 @@ mod tests {
|
||||
resolve_pending_download_urls(&db, "/data", None, move |item_id, media_type, _q| {
|
||||
let seen = Arc::clone(&seen_c);
|
||||
async move {
|
||||
seen.lock().unwrap().push((item_id.clone(), media_type));
|
||||
seen.lock_safe().push((item_id.clone(), media_type));
|
||||
Some(format!("http://resolved/{item_id}"))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let seen = seen.lock().unwrap().clone();
|
||||
let seen = seen.lock_safe().clone();
|
||||
let of = |id: &str| {
|
||||
seen.iter()
|
||||
.find(|(i, _)| i == id)
|
||||
@@ -1045,14 +1046,14 @@ mod tests {
|
||||
resolve_pending_download_urls(&db, "/data", None, move |_id, media_type, _q| {
|
||||
let seen = Arc::clone(&seen_c);
|
||||
async move {
|
||||
*seen.lock().unwrap() = media_type;
|
||||
*seen.lock_safe() = media_type;
|
||||
Some("http://x".to_string())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*seen.lock().unwrap(), "audio");
|
||||
assert_eq!(*seen.lock_safe(), "audio");
|
||||
}
|
||||
|
||||
/// An explicit `media_type` on the row always wins over the item's type.
|
||||
@@ -1069,14 +1070,14 @@ mod tests {
|
||||
resolve_pending_download_urls(&db, "/data", None, move |_id, media_type, _q| {
|
||||
let seen = Arc::clone(&seen_c);
|
||||
async move {
|
||||
*seen.lock().unwrap() = media_type;
|
||||
*seen.lock_safe() = media_type;
|
||||
Some("http://x".to_string())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*seen.lock().unwrap(), "video");
|
||||
assert_eq!(*seen.lock_safe(), "video");
|
||||
}
|
||||
|
||||
/// Rows already downloaded under the audio default hold an audio-only
|
||||
|
||||
@@ -97,6 +97,6 @@ mod tests {
|
||||
// due to its dependencies, so we just test the wrapper type structure
|
||||
|
||||
// This verifies the wrapper type exists and can hold Arc<Mutex>
|
||||
assert_eq!(std::mem::size_of::<ConnectivityMonitorWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<ConnectivityMonitorWrapper>() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ mod smart_cache;
|
||||
pub use pinning::*;
|
||||
pub use smart_cache::*;
|
||||
|
||||
/// One row of the series episode listing used when queueing a whole series:
|
||||
/// `(id, name, season_name, index_number, parent_index_number)`.
|
||||
type EpisodeRow = (String, String, Option<String>, Option<i32>, Option<i32>);
|
||||
|
||||
/// Wrapper for DownloadManager to be used as Tauri state
|
||||
pub struct DownloadManagerWrapper(pub Mutex<DownloadManager>);
|
||||
|
||||
@@ -596,6 +600,10 @@ pub(crate) async fn queue_album_tracks(
|
||||
/// TRACES: UR-018, UR-055 | DR-173 | UT-170
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
// Three of the eight arguments are Tauri `State<'_, _>` injections plus the
|
||||
// `AppHandle`, not caller input. Folding the rest into a struct would change the
|
||||
// IPC contract and the generated TypeScript for no readability gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn download_album(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
repository: State<'_, crate::commands::repository::RepositoryManagerWrapper>,
|
||||
@@ -807,7 +815,7 @@ pub async fn download_series(
|
||||
vec![QueryParam::String(series_id)],
|
||||
);
|
||||
|
||||
let episodes: Vec<(String, String, Option<String>, Option<i32>, Option<i32>)> = db_service
|
||||
let episodes: Vec<EpisodeRow> = db_service
|
||||
.query_many(episodes_query, |row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
@@ -912,6 +920,10 @@ pub async fn download_series(
|
||||
/// Queue all episodes of a specific season for download
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
// One of the eight arguments is a Tauri `State<'_, _>` injection; the rest are
|
||||
// the season's identifying fields. Folding them into a struct would change the
|
||||
// IPC contract and the generated TypeScript for no readability gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn download_season(
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
season_id: String,
|
||||
@@ -2307,7 +2319,7 @@ pub async fn delete_downloads_under(
|
||||
)";
|
||||
|
||||
let file_query = Query::with_params(
|
||||
&format!("SELECT d.file_path FROM downloads d WHERE {SCOPE}"),
|
||||
format!("SELECT d.file_path FROM downloads d WHERE {SCOPE}"),
|
||||
vec![
|
||||
QueryParam::String(user_id.clone()),
|
||||
QueryParam::String(item_id.clone()),
|
||||
@@ -2323,7 +2335,7 @@ pub async fn delete_downloads_under(
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let delete_query = Query::with_params(
|
||||
&format!("DELETE FROM downloads WHERE id IN (SELECT d.id FROM downloads d WHERE {SCOPE})"),
|
||||
format!("DELETE FROM downloads WHERE id IN (SELECT d.id FROM downloads d WHERE {SCOPE})"),
|
||||
vec![
|
||||
QueryParam::String(user_id),
|
||||
QueryParam::String(item_id.clone()),
|
||||
|
||||
@@ -186,6 +186,7 @@ async fn run_drain(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -211,7 +212,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn calls(&self) -> Vec<(String, bool)> {
|
||||
let mut calls = self.calls.lock().unwrap().clone();
|
||||
let mut calls = self.calls.lock_safe().clone();
|
||||
calls.sort();
|
||||
calls
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_playback_reporter_wrapper_structure() {
|
||||
// Verify wrapper type can hold Arc<TokioMutex<Option<T>>>
|
||||
assert_eq!(std::mem::size_of::<PlaybackReporterWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<PlaybackReporterWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1480,6 +1480,10 @@ pub async fn player_seek_video(
|
||||
/// Note: Frontend should handle saving series preferences after this command succeeds
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
// Two of the eight arguments are Tauri `State<'_, _>` injections, not caller
|
||||
// input. Folding the rest into a struct would change the IPC contract and the
|
||||
// generated TypeScript for no readability gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn player_switch_audio_track(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
repository_manager: State<'_, super::repository::RepositoryManagerWrapper>,
|
||||
@@ -1559,6 +1563,10 @@ pub async fn player_switch_audio_track(
|
||||
/// TRACES: UR-074 | DR-162
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
// Three of the nine arguments are Tauri `State<'_, _>` injections, not caller
|
||||
// input. Folding the rest into a struct would change the IPC contract and the
|
||||
// generated TypeScript for no readability gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn player_set_stream_quality(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
repository_manager: State<'_, super::repository::RepositoryManagerWrapper>,
|
||||
@@ -1784,7 +1792,7 @@ pub async fn player_get_status(
|
||||
let local_media = {
|
||||
let queue_arc = controller.queue();
|
||||
let queue = queue_arc.lock().map_err(|e| e.to_string())?;
|
||||
queue.current().map(|item| MergedMediaItem::from(item))
|
||||
queue.current().map(MergedMediaItem::from)
|
||||
};
|
||||
|
||||
let local_is_playing = status.state.is_playing();
|
||||
@@ -1808,10 +1816,7 @@ pub async fn player_get_status(
|
||||
log::info!("[PlayerCommands] Merging remote session state");
|
||||
|
||||
// Merge media item
|
||||
status.merged_media = session
|
||||
.now_playing_item
|
||||
.as_ref()
|
||||
.map(|item| MergedMediaItem::from(item));
|
||||
status.merged_media = session.now_playing_item.as_ref().map(MergedMediaItem::from);
|
||||
|
||||
// Merge isPlaying (NOT isPaused!)
|
||||
status.merged_is_playing = session
|
||||
@@ -2741,6 +2746,8 @@ pub async fn player_disable_jellyfin(player: State<'_, PlayerStateWrapper>) -> R
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::lock::MutexSafe;
|
||||
|
||||
/// The subtitle list the frontend resolved must survive the IPC hop and end
|
||||
/// up on the `MediaItem` the native backend loads.
|
||||
///
|
||||
@@ -3063,7 +3070,7 @@ mod tests {
|
||||
let database = Database::open_in_memory().unwrap();
|
||||
{
|
||||
let conn = database.connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
let conn = conn.lock_safe();
|
||||
conn.execute_batch(&format!(
|
||||
r#"
|
||||
INSERT INTO servers (id, name, url) VALUES ('srv', 'Test', 'http://test');
|
||||
@@ -3119,7 +3126,7 @@ mod tests {
|
||||
assert_eq!(switched, 1, "only the download whose file exists switches");
|
||||
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
match &queue_lock.items()[0].source {
|
||||
MediaSource::Local {
|
||||
file_path,
|
||||
@@ -3148,7 +3155,7 @@ mod tests {
|
||||
index_number: Option<i32>,
|
||||
}
|
||||
|
||||
let mut tracks = vec![
|
||||
let mut tracks = [
|
||||
MockTrack {
|
||||
id: "track1".to_string(),
|
||||
name: "Song 1".to_string(),
|
||||
@@ -3241,7 +3248,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Create tracks in random order (not sorted)
|
||||
let mut tracks = vec![
|
||||
let mut tracks = [
|
||||
MockTrack {
|
||||
id: "id5".to_string(),
|
||||
name: "Track 5".to_string(),
|
||||
|
||||
@@ -67,6 +67,10 @@ pub struct RepositoryManagerWrapper(pub RepositoryManager);
|
||||
/// Returns a handle (UUID) for accessing the repository
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
// Four of the eight arguments are Tauri `State<'_, _>` injections, not caller
|
||||
// input. Folding the remaining four into a struct would change the IPC contract
|
||||
// and the generated TypeScript for no readability gain.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn repository_create(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
player: State<'_, crate::commands::player::PlayerStateWrapper>,
|
||||
@@ -1091,7 +1095,6 @@ mod tests {
|
||||
let handle = format!("{}", uuid);
|
||||
// UUID should convert to a non-empty string
|
||||
assert!(!handle.is_empty());
|
||||
assert!(handle.len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -89,7 +89,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_poller_wrapper_structure() {
|
||||
// Test that wrapper type structure is correct
|
||||
assert_eq!(std::mem::size_of::<SessionPollerWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<SessionPollerWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1658,19 +1658,19 @@ mod tests {
|
||||
#[test]
|
||||
fn test_database_wrapper_structure() {
|
||||
// Verify DatabaseWrapper can be created and holds Mutex<Database>
|
||||
assert_eq!(std::mem::size_of::<DatabaseWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<DatabaseWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_store_wrapper_structure() {
|
||||
// Verify CredentialStoreWrapper can be created
|
||||
assert_eq!(std::mem::size_of::<CredentialStoreWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<CredentialStoreWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thumbnail_cache_wrapper_structure() {
|
||||
// Verify ThumbnailCacheWrapper holds Arc<ThumbnailCache>
|
||||
assert_eq!(std::mem::size_of::<ThumbnailCacheWrapper>() > 0, true);
|
||||
assert!(std::mem::size_of::<ThumbnailCacheWrapper>() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -481,6 +481,7 @@ pub async fn sync_process_pending(app: tauri::AppHandle) -> Result<DrainReport,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -517,7 +518,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn calls(&self) -> Vec<QueuedOp> {
|
||||
self.calls.lock().unwrap().clone()
|
||||
self.calls.lock_safe().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,7 +528,7 @@ mod tests {
|
||||
if let Some(err) = &self.fail_with {
|
||||
return Err(err.clone());
|
||||
}
|
||||
self.calls.lock().unwrap().push(op.clone());
|
||||
self.calls.lock_safe().push(op.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user