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/PlaybackPipelineTeardownTes...

71 lines
3.3 KiB

import Testing
import Foundation
import AVFoundation
@testable import Music
// Reproduces the "CoreMediaErrorDomain error -16044 after streaming a few tracks"
// bug (plus the accompanying HALC_ProxyIOContext overload / out-of-order log spew).
//
// Root cause: setting `player = nil` does NOT release an AVPlayer's decode/render
// pipeline it is the *association* of an AVPlayerItem with an AVPlayer that owns
// the pipeline, and ARC tears it down asynchronously. The teardown path paused and
// nilled the player but never called `replaceCurrentItem(with: nil)`, so each track
// switch leaked a still-associated pipeline. After a handful of tracks they exceed
// CoreMedia's small concurrent-pipeline limit and a new player can't acquire a
// decode session, failing with -16044.
//
// The invariant proven here: tearing down the player must dissociate its item so
// the pipeline is released immediately.
@MainActor
struct PlaybackPipelineTeardownTests {
// Verifies the streaming provider releases the previous player's pipeline on teardown.
// Steps:
// 1. Create a StreamingPlaybackProvider (host/key unused we drive AVPlayer
// directly with a local file URL to bypass the network pre-flight).
// 2. Start an AVPlayer on a real local audio fixture and capture a strong
// reference to it, then confirm the pipeline is established (currentItem set).
// 3. Tear down via stop() the same cleanup path a queue advance runs.
// 4. The captured player must have been dissociated from its item (currentItem
// == nil). Before the fix it is still set, so pipelines accumulate.
@Test func streamingProviderReleasesPipelineOnTeardown() throws {
// 1. Provider with throwaway connection details.
let provider = StreamingPlaybackProvider(hostURL: "http://unused.invalid", apiKey: "unused")
// 2. Start playback on a real local file and grab the AVPlayer it created.
let fixture = try TestFixtures.shortMP3URL()
provider.startAVPlayer(url: fixture)
let firstPlayer = try #require(provider.player)
#expect(firstPlayer.currentItem != nil)
// 3. Tear down (queue advance / stop runs this path).
provider.stop()
// 4. The pipeline must be released: the item is dissociated from the player.
#expect(firstPlayer.currentItem == nil)
}
// Verifies the local-playback provider (AudioService) has the same fix.
// Steps:
// 1. Create an AudioService.
// 2. Play a real local audio fixture and capture the AVPlayer; confirm the
// pipeline is established (currentItem set).
// 3. Tear down via stop().
// 4. The captured player must have been dissociated from its item.
@Test func audioServiceReleasesPipelineOnTeardown() throws {
// 1. Local playback provider.
let provider = AudioService()
// 2. Play a real local file and grab the AVPlayer it created.
let fixture = try TestFixtures.shortMP3URL()
provider.play(url: fixture)
let firstPlayer = try #require(provider.player)
#expect(firstPlayer.currentItem != nil)
// 3. Tear down.
provider.stop()
// 4. The pipeline must be released: the item is dissociated from the player.
#expect(firstPlayer.currentItem == nil)
}
}