First working POC
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
/// 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
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
|
||||
/// Test that simulates the position update thread spawning async tasks
|
||||
/// without a Tokio runtime (the bug we just fixed)
|
||||
#[test]
|
||||
fn test_position_thread_handles_missing_tokio_runtime() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
let success = Arc::new(AtomicBool::new(false));
|
||||
let success_clone = success.clone();
|
||||
|
||||
// Spawn a regular thread (no Tokio runtime)
|
||||
let handle = std::thread::spawn(move || {
|
||||
// This simulates what the position update thread does
|
||||
// It should handle the case where there's no Tokio runtime
|
||||
|
||||
// Try to get the current Tokio runtime handle
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
// We have a runtime, use it
|
||||
handle.spawn(async move {
|
||||
// Async work here
|
||||
});
|
||||
} else {
|
||||
// No runtime, spawn a new thread with its own runtime
|
||||
// This is the fix we applied
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async move {
|
||||
// Async work here
|
||||
success_clone.store(true, Ordering::SeqCst);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
// Give the spawned thread time to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
assert!(
|
||||
success.load(Ordering::SeqCst),
|
||||
"Should successfully execute async code from std::thread without panicking"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the Tokio runtime fallback pattern works correctly
|
||||
#[test]
|
||||
fn test_tokio_runtime_fallback_pattern() {
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let counter_clone = counter.clone();
|
||||
|
||||
// Spawn from a regular thread (no runtime)
|
||||
let handle = std::thread::spawn(move || {
|
||||
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;
|
||||
});
|
||||
} 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
// Wait for async task to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
let count = *counter.lock().unwrap();
|
||||
assert_eq!(count, 1, "Fallback pattern should execute async code successfully");
|
||||
}
|
||||
|
||||
/// Test that position update logic works in a thread
|
||||
#[test]
|
||||
fn test_position_update_in_thread() {
|
||||
use std::time::Duration;
|
||||
|
||||
let positions = Arc::new(Mutex::new(Vec::new()));
|
||||
let positions_clone = positions.clone();
|
||||
|
||||
// Simulate the position update thread
|
||||
let handle = std::thread::spawn(move || {
|
||||
for i in 0..5 {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
// Simulate getting position from player
|
||||
let position = i as f64 * 0.25;
|
||||
|
||||
// Store position (simulating event emission)
|
||||
positions_clone.lock().unwrap().push(position);
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
let recorded_positions = positions.lock().unwrap();
|
||||
assert_eq!(recorded_positions.len(), 5, "Should have recorded 5 position updates");
|
||||
|
||||
// Verify positions are increasing
|
||||
for (i, pos) in recorded_positions.iter().enumerate() {
|
||||
let expected = i as f64 * 0.25;
|
||||
assert!(
|
||||
(*pos - expected).abs() < 0.001,
|
||||
"Position {} should be close to {}",
|
||||
pos,
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test async progress reporting pattern
|
||||
#[tokio::test]
|
||||
async fn test_progress_reporting_with_tokio_mutex() {
|
||||
use crate::playback_reporting::{EventThrottler, PlaybackReporter};
|
||||
|
||||
// Create mock reporter (None for this test)
|
||||
let reporter = Arc::new(TokioMutex::new(None::<PlaybackReporter>));
|
||||
let throttler = Arc::new(EventThrottler::new());
|
||||
|
||||
// Simulate progress reporting
|
||||
let item_id = "test_item_123".to_string();
|
||||
|
||||
// This should not panic even though reporter is None
|
||||
let reporter_guard = reporter.lock().await;
|
||||
if let Some(_reporter_instance) = reporter_guard.as_ref() {
|
||||
// Would report here if reporter was configured
|
||||
} else {
|
||||
// Reporter not configured - this is OK
|
||||
}
|
||||
drop(reporter_guard);
|
||||
|
||||
// Verify throttler works
|
||||
assert!(
|
||||
throttler.should_report(&item_id),
|
||||
"First report should be allowed"
|
||||
);
|
||||
|
||||
throttler.mark_reported(&item_id);
|
||||
|
||||
// Immediate second report should be throttled
|
||||
// (EventThrottler has internal logic for this)
|
||||
}
|
||||
|
||||
/// Test that position updates are emitted even when paused (for scrubbing)
|
||||
/// This is critical for UI responsiveness when seeking while paused
|
||||
#[test]
|
||||
fn test_position_updates_while_paused() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
let update_count = Arc::new(AtomicUsize::new(0));
|
||||
let update_count_clone = update_count.clone();
|
||||
|
||||
// Simulate a position update thread that runs regardless of pause state
|
||||
let handle = std::thread::spawn(move || {
|
||||
// Simulate 5 position updates
|
||||
for _ in 0..5 {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
|
||||
// In the real implementation, we check position from MPV
|
||||
// and emit PositionUpdate events even when paused
|
||||
// This simulates that behavior:
|
||||
let _is_paused = true; // Simulating paused state
|
||||
|
||||
// Key: We DON'T skip the update when paused
|
||||
// This allows scrubbing to work
|
||||
update_count_clone.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
let final_count = update_count.load(Ordering::SeqCst);
|
||||
assert_eq!(
|
||||
final_count, 5,
|
||||
"Position updates should be emitted even when paused (got {} updates)",
|
||||
final_count
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that progress reporting is skipped when paused
|
||||
/// Progress reporting to the server should only happen during active playback
|
||||
#[test]
|
||||
fn test_progress_reporting_skipped_when_paused() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
let report_count = Arc::new(AtomicUsize::new(0));
|
||||
let report_count_clone = report_count.clone();
|
||||
|
||||
// Simulate the progress reporting logic
|
||||
let handle = std::thread::spawn(move || {
|
||||
// Simulate 5 update cycles
|
||||
for i in 0..5 {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
|
||||
// Position updates are emitted (tested separately)
|
||||
// But progress reporting depends on pause state
|
||||
let is_paused = i % 2 == 0; // Alternate between paused and playing
|
||||
|
||||
// Key: Only report when NOT paused
|
||||
if !is_paused {
|
||||
report_count_clone.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
let final_count = report_count.load(Ordering::SeqCst);
|
||||
assert_eq!(
|
||||
final_count, 2,
|
||||
"Progress reporting should only happen when not paused (got {} reports)",
|
||||
final_count
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that position updates are suppressed briefly after a seek
|
||||
/// This prevents "jumping to zero" visual glitches during seek operations
|
||||
#[test]
|
||||
fn test_position_updates_suppressed_after_seek() {
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
let last_seek_time = Arc::new(AtomicU64::new(0));
|
||||
let last_seek_time_clone = last_seek_time.clone();
|
||||
let update_count = Arc::new(AtomicUsize::new(0));
|
||||
let update_count_clone = update_count.clone();
|
||||
|
||||
// Simulate a seek happening
|
||||
let seek_time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
last_seek_time.store(seek_time, Ordering::Relaxed);
|
||||
|
||||
// Simulate position update thread
|
||||
let handle = std::thread::spawn(move || {
|
||||
// Try 2 position updates at 50ms intervals (well within the 150ms window)
|
||||
for _ in 0..2 {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
|
||||
// Check if we should suppress updates (within 150ms of seek)
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
let last_seek = last_seek_time_clone.load(Ordering::Relaxed);
|
||||
let time_since_seek = now.saturating_sub(last_seek);
|
||||
|
||||
if time_since_seek < 150 {
|
||||
// Suppress update (don't increment counter)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Emit update
|
||||
update_count_clone.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
let final_count = update_count.load(Ordering::SeqCst);
|
||||
// With 50ms intervals and 150ms suppression window, updates at 50ms and 100ms
|
||||
// should both be suppressed
|
||||
assert_eq!(
|
||||
final_count, 0,
|
||||
"Position updates should be suppressed within 150ms of seek (got {} updates)",
|
||||
final_count
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that position updates resume after seek suppression window
|
||||
#[test]
|
||||
fn test_position_updates_resume_after_seek_window() {
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
let last_seek_time = Arc::new(AtomicU64::new(0));
|
||||
let last_seek_time_clone = last_seek_time.clone();
|
||||
let update_count = Arc::new(AtomicUsize::new(0));
|
||||
let update_count_clone = update_count.clone();
|
||||
|
||||
// Simulate a seek that happened 200ms ago (past the suppression window)
|
||||
let seek_time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
- 200; // 200ms ago
|
||||
last_seek_time.store(seek_time, Ordering::Relaxed);
|
||||
|
||||
// Simulate position update thread
|
||||
let handle = std::thread::spawn(move || {
|
||||
// Try 3 position updates
|
||||
for _ in 0..3 {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
// Check if we should suppress updates (within 150ms of seek)
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
let last_seek = last_seek_time_clone.load(Ordering::Relaxed);
|
||||
let time_since_seek = now.saturating_sub(last_seek);
|
||||
|
||||
if time_since_seek < 150 {
|
||||
continue; // Should not happen in this test
|
||||
}
|
||||
|
||||
// Emit update
|
||||
update_count_clone.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
let final_count = update_count.load(Ordering::SeqCst);
|
||||
assert_eq!(
|
||||
final_count, 3,
|
||||
"Position updates should resume after seek suppression window (got {} updates)",
|
||||
final_count
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user