Skip to content

Commit 243b5d4

Browse files
committed
Surface HTTP and audio system errors
1 parent 578bbcd commit 243b5d4

8 files changed

Lines changed: 132 additions & 19 deletions

File tree

AudioStreaming.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
540A1A412F6FBFF700A63C35 /* NetworkErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540A1A402F6FBFF700A63C35 /* NetworkErrorTests.swift */; };
1011
B500732024D00BAC00BB4475 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500731F24D00BAC00BB4475 /* Logger.swift */; };
1112
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514657E248E3884005C03F7 /* DispatchTimerSource.swift */; };
1213
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */; };
@@ -94,6 +95,7 @@
9495
/* End PBXCopyFilesBuildPhase section */
9596

9697
/* Begin PBXFileReference section */
98+
540A1A402F6FBFF700A63C35 /* NetworkErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkErrorTests.swift; sourceTree = "<group>"; };
9799
B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
98100
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = "<group>"; };
99101
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = "<group>"; };
@@ -412,6 +414,7 @@
412414
isa = PBXGroup;
413415
children = (
414416
B5EF954D247DA5AC003E8FF8 /* NetworkingClientTests.swift */,
417+
540A1A402F6FBFF700A63C35 /* NetworkErrorTests.swift */,
415418
);
416419
path = Network;
417420
sourceTree = "<group>";
@@ -649,6 +652,7 @@
649652
B5EF954E247DA5AC003E8FF8 /* NetworkingClientTests.swift in Sources */,
650653
B59CB46C25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift in Sources */,
651654
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */,
655+
540A1A412F6FBFF700A63C35 /* NetworkErrorTests.swift in Sources */,
652656
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */,
653657
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */,
654658
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */,

AudioStreaming/Core/Extensions/AVAudioUnit+Convenience.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension AVAudioUnit {
2121
completion(.failure(error))
2222
return
2323
}
24-
completion(.failure(AudioPlayerError.audioSystemError(.playerNotFound)))
24+
completion(.failure(AudioPlayerError.audioSystemError(.playerNotFound(nil))))
2525
return
2626
}
2727
completion(.success(audioUnit))

AudioStreaming/Core/Network/NetworkingClient.swift

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ enum DataStreamError: Error {
1212

1313
public enum NetworkError: Error, Equatable {
1414
case failure(Error)
15-
case serverError
15+
case serverError(statusCode: Int)
1616
case missingData
17+
1718
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
1819
switch (lhs, rhs) {
19-
case (.failure, failure):
20-
return true
21-
case (.serverError, .serverError):
22-
return true
20+
case let (.failure(lhsError), .failure(rhsError)):
21+
return compareErrors(lhsError, rhsError)
22+
case let (.serverError(lhsStatusCode), .serverError(rhsStatusCode)):
23+
return lhsStatusCode == rhsStatusCode
2324
case (.missingData, .missingData):
2425
return true
2526
default:
@@ -28,6 +29,34 @@ public enum NetworkError: Error, Equatable {
2829
}
2930
}
3031

32+
extension NetworkError: LocalizedError {
33+
public var errorDescription: String? {
34+
switch self {
35+
case let .failure(error):
36+
let nsError = error as NSError
37+
return "\(error.localizedDescription) [\(nsError.domain):\(nsError.code)]"
38+
case let .serverError(statusCode):
39+
return "HTTP server error \(statusCode)"
40+
case .missingData:
41+
return "Missing audio data from network stream"
42+
}
43+
}
44+
}
45+
46+
func compareErrors(_ lhs: Error?, _ rhs: Error?) -> Bool {
47+
switch (lhs, rhs) {
48+
case (nil, nil):
49+
return true
50+
case let (lhs?, rhs?):
51+
let lhsNSError = lhs as NSError
52+
let rhsNSError = rhs as NSError
53+
return lhsNSError.domain == rhsNSError.domain &&
54+
lhsNSError.code == rhsNSError.code
55+
default:
56+
return false
57+
}
58+
}
59+
3160
protocol StreamTaskProvider: AnyObject {
3261
func dataStream(for request: URLSessionTask) -> NetworkDataStream?
3362
}

AudioStreaming/Streaming/Audio Source/FileAudioSource.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
8080

8181
private func performOpen(seek seekOffset: Int) throws {
8282
guard let inputStream = InputStream(url: url) else {
83-
throw AudioSystemError.playerStartError
83+
throw AudioSystemError.playerStartError(nil)
8484
}
8585
self.inputStream = inputStream
8686

AudioStreaming/Streaming/Audio Source/RemoteAudioSource.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public class RemoteAudioSource: AudioStreamSource {
273273
} else if statusCode >= 300 {
274274
delegate?.errorOccurred(
275275
source: self,
276-
error: NetworkError.serverError
276+
error: NetworkError.serverError(statusCode: statusCode)
277277
)
278278
}
279279
}

AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ open class AudioPlayer {
192192
do {
193193
try self.startEngineIfNeeded()
194194
} catch {
195-
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
195+
self.raiseUnexpected(error: .audioSystemError(.engineFailure(error)))
196196
}
197197
}
198198

@@ -417,7 +417,7 @@ open class AudioPlayer {
417417
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
418418
case let .failure(error):
419419
assertionFailure("couldn't create player unit: \(error)")
420-
self.raiseUnexpected(error: .audioSystemError(.playerNotFound))
420+
self.raiseUnexpected(error: .audioSystemError(.playerNotFound(error)))
421421
}
422422
}
423423
}
@@ -536,7 +536,7 @@ open class AudioPlayer {
536536
try player.auAudioUnit.startHardware()
537537
} catch {
538538
stopEngine(reason: .error)
539-
raiseUnexpected(error: .audioSystemError(.playerStartError))
539+
raiseUnexpected(error: .audioSystemError(.playerStartError(error)))
540540
}
541541
}
542542

AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,45 @@ public enum AudioPlayerError: LocalizedError, Equatable {
101101
}
102102

103103
public enum AudioSystemError: LocalizedError, Equatable {
104-
case engineFailure
105-
case playerNotFound
106-
case playerStartError
104+
case engineFailure(Error?)
105+
case playerNotFound(Error?)
106+
case playerStartError(Error?)
107107
case fileStreamError(AudioFileStreamError)
108108
case converterError(AudioConverterError)
109109

110+
public static func == (lhs: AudioSystemError, rhs: AudioSystemError) -> Bool {
111+
switch (lhs, rhs) {
112+
case let (.engineFailure(lhsError), .engineFailure(rhsError)),
113+
let (.playerNotFound(lhsError), .playerNotFound(rhsError)),
114+
let (.playerStartError(lhsError), .playerStartError(rhsError)):
115+
return compareErrors(lhsError, rhsError)
116+
case let (.fileStreamError(lhsError), .fileStreamError(rhsError)):
117+
return lhsError == rhsError
118+
case let (.converterError(lhsError), .converterError(rhsError)):
119+
return lhsError == rhsError
120+
default:
121+
return false
122+
}
123+
}
124+
110125
public var errorDescription: String? {
111126
switch self {
112-
case .engineFailure: return "Audio engine couldn't start"
113-
case .playerNotFound: return "Player not found"
114-
case .playerStartError: return "Player couldn't start"
127+
case let .engineFailure(error):
128+
return detailedDescription(prefix: "Audio engine couldn't start", error: error)
129+
case let .playerNotFound(error):
130+
return detailedDescription(prefix: "Player not found", error: error)
131+
case let .playerStartError(error):
132+
return detailedDescription(prefix: "Player couldn't start", error: error)
115133
case let .fileStreamError(error):
116-
return "Audio file stream error'd: \(error)"
134+
return "Audio file stream errored: \(error)"
117135
case let .converterError(error):
118-
return "Audio converter error'd: \(error)"
136+
return "Audio converter errored: \(error)"
119137
}
120138
}
121139
}
140+
141+
private func detailedDescription(prefix: String, error: Error?) -> String {
142+
guard let error else { return prefix }
143+
let nsError = error as NSError
144+
return "\(prefix): \(error.localizedDescription) [\(nsError.domain):\(nsError.code)]"
145+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
import XCTest
3+
4+
@testable import AudioStreaming
5+
6+
final class NetworkErrorTests: XCTestCase {
7+
func testFailureEqualityUsesNSErrorIdentity() {
8+
XCTAssertEqual(
9+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)),
10+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut))
11+
)
12+
XCTAssertNotEqual(
13+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)),
14+
NetworkError.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost))
15+
)
16+
}
17+
18+
func testServerErrorDescriptionIncludesStatusCode() {
19+
XCTAssertEqual(NetworkError.serverError(statusCode: 403).localizedDescription, "HTTP server error 403")
20+
XCTAssertEqual(NetworkError.serverError(statusCode: 404).localizedDescription, "HTTP server error 404")
21+
XCTAssertEqual(NetworkError.serverError(statusCode: 500).localizedDescription, "HTTP server error 500")
22+
}
23+
24+
func testMissingDataDescription() {
25+
XCTAssertEqual(NetworkError.missingData.localizedDescription, "Missing audio data from network stream")
26+
}
27+
28+
func testEngineFailureDescriptionIncludesUnderlyingNSErrorDetails() {
29+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost)
30+
let description = AudioSystemError.engineFailure(error).localizedDescription
31+
32+
XCTAssertTrue(description.contains("Audio engine couldn't start"))
33+
XCTAssertTrue(description.contains(NSURLErrorDomain))
34+
XCTAssertTrue(description.contains("\(NSURLErrorCannotConnectToHost)"))
35+
}
36+
37+
func testNilUnderlyingErrorUsesPlainPrefix() {
38+
XCTAssertEqual(AudioSystemError.engineFailure(nil).localizedDescription, "Audio engine couldn't start")
39+
XCTAssertEqual(AudioSystemError.playerNotFound(nil).localizedDescription, "Player not found")
40+
XCTAssertEqual(AudioSystemError.playerStartError(nil).localizedDescription, "Player couldn't start")
41+
}
42+
43+
func testUnderlyingAudioSystemErrorsIncludePrefixAndNSErrorDetails() {
44+
let playerNotFoundDescription =
45+
AudioSystemError.playerNotFound(NSError(domain: "AudioUnit", code: -50)).localizedDescription
46+
XCTAssertTrue(playerNotFoundDescription.contains("Player not found"))
47+
XCTAssertTrue(playerNotFoundDescription.contains("AudioUnit"))
48+
XCTAssertTrue(playerNotFoundDescription.contains("-50"))
49+
50+
let playerStartDescription =
51+
AudioSystemError.playerStartError(NSError(domain: NSOSStatusErrorDomain, code: -10875)).localizedDescription
52+
XCTAssertTrue(playerStartDescription.contains("Player couldn't start"))
53+
XCTAssertTrue(playerStartDescription.contains(NSOSStatusErrorDomain))
54+
XCTAssertTrue(playerStartDescription.contains("-10875"))
55+
}
56+
}

0 commit comments

Comments
 (0)