merge: clear the clippy backlog and unify lock helpers (D1-warnings, D3)

51 clippy warnings -> 0, with 8 justified #[allow]s (IPC arity, specta wire
types, and the 9 test-only await-holding-lock sites). 27 raw lock calls moved to
the poison-tolerant helpers - all of them test code; production was already
clean.

Caught a non-neutral clippy --fix: removing the redundant 'use hostname;' in
credentials.rs orphaned its #[cfg(target_os = "linux")] onto SERVICE_NAME,
which would have cfg'd the constant out of every non-Linux build. Compiles clean
on Linux, so only Windows/macOS CI would have caught it.
This commit is contained in:
2026-08-16 23:06:33 +02:00
27 changed files with 173 additions and 109 deletions
+6
View File
@@ -6,6 +6,12 @@ use serde::{Deserialize, Serialize};
/// Autoplay decision result - determines what happens after playback ends
#[derive(specta::Type, Debug, Clone, Serialize)]
#[serde(tag = "action", rename_all = "camelCase")]
// `ShowNextEpisodePopup` carries two `MediaItem`s, so it dwarfs the unit
// variants. Boxing them is not worth it here: this enum is constructed once per
// end-of-item (never in a hot loop or a large collection), and it is an IPC type
// — the indirection would have to stay invisible to serde/specta while every
// match arm gained a deref, for no measurable gain.
#[allow(clippy::large_enum_variant)]
pub enum AutoplayDecision {
/// Stop playback (no next item or timer expired)
Stop,
+6
View File
@@ -30,6 +30,12 @@ use super::{MediaSessionType, SleepTimerMode};
// queue_changed never reach the frontend, so the mini player never appears).
// Keep serde and specta agreeing: snake_case fields, snake_case variant tags.
#[serde(tag = "type", rename_all = "snake_case")]
// `ShowNextEpisodePopup` carries two `MediaItem`s, so it dwarfs the small
// position/state variants. Boxing them is rejected deliberately: this is a
// serde + specta wire type whose generated TypeScript must not shift, and the
// events are emitted a few times a second at most — never bulk-allocated — so
// the size difference costs nothing measurable.
#[allow(clippy::large_enum_variant)]
pub enum PlayerStatusEvent {
/// Playback position updated (emitted periodically during playback)
PositionUpdate {
+1 -1
View File
@@ -243,7 +243,7 @@ impl MpvBackend {
});
}
}
libmpv::events::Event::PropertyChange { name, .. } if name == "pause" => {
libmpv::events::Event::PropertyChange { name: "pause", .. } => {
// Handle pause state changes
if let Ok(is_paused) = mpv.get_property::<bool>("pause") {
let media_id = state
+14 -13
View File
@@ -1,14 +1,15 @@
/// Tests for MpvBackend to prevent regressions
///
/// These tests are designed to catch common issues like:
/// - Tokio runtime panics when spawning async tasks from std::thread
/// - Position update thread failures
/// - Event emission issues
///
/// TRACES: UR-003, UR-004 | IR-003 | IT-003, IT-004
//! Tests for MpvBackend to prevent regressions
//!
//! These tests are designed to catch common issues like:
//! - Tokio runtime panics when spawning async tasks from std::thread
//! - Position update thread failures
//! - Event emission issues
//!
//! TRACES: UR-003, UR-004 | IR-003 | IT-003, IT-004
#[cfg(test)]
mod tests {
use crate::utils::lock::MutexSafe;
use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as TokioMutex;
@@ -67,14 +68,14 @@ mod tests {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
// Has runtime (shouldn't happen in this test)
handle.spawn(async move {
*counter_clone.lock().unwrap() += 1;
*counter_clone.lock_safe() += 1;
});
} else {
// No runtime - use fallback (should happen in this test)
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
*counter_clone.lock().unwrap() += 1;
*counter_clone.lock_safe() += 1;
});
});
}
@@ -85,7 +86,7 @@ mod tests {
// Wait for async task to complete
std::thread::sleep(std::time::Duration::from_millis(100));
let count = *counter.lock().unwrap();
let count = *counter.lock_safe();
assert_eq!(
count, 1,
"Fallback pattern should execute async code successfully"
@@ -109,13 +110,13 @@ mod tests {
let position = i as f64 * 0.25;
// Store position (simulating event emission)
positions_clone.lock().unwrap().push(position);
positions_clone.lock_safe().push(position);
}
});
handle.join().unwrap();
let recorded_positions = positions.lock().unwrap();
let recorded_positions = positions.lock_safe();
assert_eq!(
recorded_positions.len(),
5,
+1 -2
View File
@@ -806,11 +806,10 @@ mod tests {
assert_eq!(queue.current_index(), Some(first_shuffled_index));
// Move through shuffle order
for i in 1..shuffle_order.len() {
for &expected_index in &shuffle_order[1..] {
assert!(queue.has_next());
let result = queue.next();
assert!(result.is_some());
let expected_index = shuffle_order[i];
assert_eq!(queue.current_index(), Some(expected_index));
}
+3
View File
@@ -159,6 +159,9 @@ mod tests {
#[test]
fn test_end_reason_clone() {
let reason = EndReason::Finished;
// Deliberately exercising the derived `Clone` impl, not a plain copy:
// `EndReason` is also `Copy`, so clippy flags the call as redundant.
#[allow(clippy::clone_on_copy)]
let cloned = reason.clone();
assert_eq!(reason, cloned);
}
@@ -196,7 +196,7 @@ mod tests {
impl PlayerEventEmitter for RecordingEmitter {
fn emit(&self, event: PlayerStatusEvent) {
self.events.lock().unwrap().push(event);
self.events.lock_safe().push(event);
}
}
@@ -244,7 +244,7 @@ mod tests {
let (mut b, events) = backend();
b.load(&test_media()).unwrap();
let ev = events.lock().unwrap();
let ev = events.lock_safe();
let load = ev
.iter()
.find(|e| matches!(e, PlayerStatusEvent::WebviewAudioLoad { .. }))
@@ -263,7 +263,7 @@ mod tests {
b.pause().unwrap();
b.seek(42.0).unwrap();
let ev = events.lock().unwrap();
let ev = events.lock_safe();
assert!(ev.iter().any(|e| matches!(
e,
PlayerStatusEvent::ControlCommand { action, .. } if action == "pause"