You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/MusicTests/StreamingPlaybackEndToEndTe...

82 lines
3.9 KiB

import Testing
import Foundation
import AVFoundation
import MusicShared
@testable import Music
// End-to-end reproduction of the reported bug:
// "After streaming a full track, AVPlayer fails with 'Operation Stopped'
// and the player does NOT auto-advance to the next track."
//
// These tests drive the *real* StreamingPlaybackProvider against the *real*
// StreamingServer, exactly as the app does, and assert that playing a track to
// its natural end results in a clean finish (onTrackFinished fires, no error).
@MainActor
struct StreamingPlaybackEndToEndTests {
static let testAPIKey = "e2e-test-key"
// Spins the main run loop (where AVPlayer delivers its callbacks) until
// `condition` is true or `timeout` seconds elapse. Returns true if the
// condition was met. Polls in small slices so an async @MainActor test
// lets AVPlayer's main-queue observers run.
private func waitUntil(timeout: Double, _ condition: () -> Bool) async -> Bool {
let slices = Int(timeout / 0.05)
for _ in 0..<slices {
if condition() { return true }
try? await Task.sleep(nanoseconds: 50_000_000) // 50 ms
}
return condition()
}
// Reproduces the auto-advance failure at end-of-track.
// Steps:
// 1. Create an in-memory DB and insert a track pointing at a real audio
// fixture, stored the way the production scanner stores it (absoluteString).
// 2. Start the real StreamingServer on an OS-assigned port.
// 3. Create the real StreamingPlaybackProvider pointed at that server.
// 4. Install an onTrackFinished callback that records the clean-finish signal
// (this is the callback PlayerViewModel uses to advance to the next track).
// 5. Begin playback of the track's /file URL and let it play to the end.
// 6. Wait until EITHER a clean finish fires OR a playback error appears.
// 7. Assert: no playback error occurred AND the clean-finish callback fired.
// On the current (buggy) server this fails playback ends in an error
// instead of a clean finish, so the next track never plays.
@Test(.timeLimit(.minutes(1)))
func playingTrackToEndFiresCleanFinish() async throws {
// 1. DB + fixture track (stored as absoluteString, like the real scanner).
let db = try DatabaseService(inMemory: true)
let fixtureURL = try TestFixtures.shortMP3URL()
var track = Track.fixture(id: nil, fileURL: fixtureURL.absoluteString, title: "E2E Track")
try db.insert(&track)
// 2. Start the real streaming server.
let server = StreamingServer(db: db, apiKey: Self.testAPIKey, port: 0)
try await server.start()
defer { server.stop() }
let port = try #require(server.actualPort)
// 3. Real provider against the real server.
let provider = StreamingPlaybackProvider(hostURL: "http://127.0.0.1:\(port)", apiKey: Self.testAPIKey)
// 4. Record the clean-finish signal (drives PlayerViewModel.next()).
var finishedCleanly = false
provider.onTrackFinished = { finishedCleanly = true }
// 5. Start playback of the track.
let url = try #require(provider.urlForTrack(track))
provider.play(url: url)
// 6. Wait for a terminal outcome: clean finish or error. The fixture is a
// few seconds long; allow generous headroom for buffering.
_ = await waitUntil(timeout: 25) {
finishedCleanly || provider.playbackError != nil
}
// --- Diagnostics (printed regardless of pass/fail) ---
print("E2E DIAGNOSTIC -> finishedCleanly=\(finishedCleanly), playbackError=\(String(describing: provider.playbackError)), providerDuration=\(provider.duration), currentTime=\(provider.currentTime)")
// 7. A fully streamed track must end cleanly so the queue can advance.
#expect(provider.playbackError == nil)
#expect(finishedCleanly == true)
}
}