-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInterruptionTransportTests.swift
More file actions
162 lines (136 loc) · 6.5 KB
/
Copy pathInterruptionTransportTests.swift
File metadata and controls
162 lines (136 loc) · 6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// InterruptionTransportTests.swift
// PlayolaPlayer
//
// Host-driven interruption transport: pauseForInterruption() /
// resumeAfterInterruption(). The SDK does not own the AVAudioSession or observe
// interruptions; the host calls these explicitly. None of these tests touch the
// shared CoreAudio graph: constructing a station player resolves the mixer
// lazily, and the resume tests exercise only the unarmed early-return path
// (the armed path would start a real engine).
import AVFAudio
import Foundation
import Testing
@testable import PlayolaPlayer
@MainActor
struct InterruptionTransportTests {
// Engine-config recovery must NOT fire while idle or host-paused (those paths
// would otherwise spin up a real engine via restartEngine). Only the guarded
// skip paths are unit-tested; the active recovery path needs CoreAudio.
@Test("Engine configuration change is ignored while idle")
func engineConfigChangeIgnoredWhileIdle() {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.idle, stationId: nil)
player.handleAudioEngineConfigurationChange(
Notification(name: .AVAudioEngineConfigurationChange))
if case .idle = player.state {
} else {
Issue.record("config change while idle must not change state, got \(player.state)")
}
}
@Test("Engine configuration change is ignored while host-paused")
func engineConfigChangeIgnoredWhilePaused() {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.playing(.mock), stationId: "station-1")
player.pauseForInterruption() // sets isSuspended; state -> .paused
player.handleAudioEngineConfigurationChange(
Notification(name: .AVAudioEngineConfigurationChange))
#expect(player.isSuspendedForTesting == true)
if case .paused = player.state {
} else {
Issue.record("config change while paused must not change state, got \(player.state)")
}
}
@Test("pauseForInterruption bumps generation, publishes .paused, keeps station id")
func pauseBumpsGenerationPublishesPausedAndKeepsStationId() {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.playing(.mock), stationId: "station-1")
let generationBefore = player.playGeneration
player.pauseForInterruption()
#expect(player.playGeneration == generationBefore + 1)
#expect(player.stationId == "station-1")
#expect(player.isCurrentGeneration(generationBefore) == false)
if case .paused(let spin) = player.state {
#expect(spin.id == Spin.mock.id)
} else {
Issue.record("pause must publish .paused, got \(player.state)")
}
}
@Test("resumeAfterInterruption without prior pause is a no-op")
func resumeWithoutPriorPauseIsNoOp() async throws {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
try await player.resumeAfterInterruption()
#expect(player.isPlaying == false)
#expect(player.interruptedStationIdForTesting == nil)
}
@Test("pause while not playing does not arm resume")
func pauseWhileNotPlayingDoesNotArmResume() async throws {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.idle, stationId: "station-1")
player.pauseForInterruption()
try await player.resumeAfterInterruption()
#expect(player.isPlaying == false)
#expect(player.interruptedStationIdForTesting == nil)
}
@Test("Double pause keeps resume armed")
func doublePauseKeepsResumeArmed() {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.playing(.mock), stationId: "station-1")
player.pauseForInterruption()
player.pauseForInterruption()
#expect(player.isSuspendedForTesting == true)
#expect(player.interruptedStationIdForTesting == "station-1")
#expect(player.wasPlayingBeforeInterruptionForTesting == true)
}
@Test("stop() clears armed interruption state so a later resume can't revive it")
func stopClearsArmedInterruptionState() {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.playing(.mock), stationId: "station-1")
player.pauseForInterruption()
#expect(player.interruptedStationIdForTesting == "station-1") // armed
player.stop()
#expect(player.interruptedStationIdForTesting == nil)
#expect(player.wasPlayingBeforeInterruptionForTesting == false)
#expect(player.isSuspendedForTesting == false)
}
@Test("resumeAfterInterruption after a stop is a no-op")
func resumeAfterStopIsNoOp() async throws {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.playing(.mock), stationId: "station-1")
player.pauseForInterruption()
player.stop()
// Guard fails (stop cleared the armed fields) → returns before touching the
// engine, so this never resolves the shared mixer / starts CoreAudio.
try await player.resumeAfterInterruption()
#expect(player.isPlaying == false)
}
@Test("Pause during loading arms resume and clears the spinner state")
func pauseDuringLoadingArmsResumeAndClearsSpinner() {
let player = PlayolaStationPlayer(
fileDownloadManager: MockFileDownloadManager(), urlSession: MockURLSession())
player.configure(authProvider: MockAuthProvider())
player.setStateForTesting(.loading(0.5), stationId: "station-1")
player.pauseForInterruption()
if case .idle = player.state {
} else {
Issue.record("pause during loading must clear the spinner, got \(player.state)")
}
#expect(player.wasPlayingBeforeInterruptionForTesting == true)
#expect(player.interruptedStationIdForTesting == "station-1")
}
}