Skip to content

Commit c09b881

Browse files
committed
Add concurrency tests
1 parent eebbc02 commit c09b881

File tree

5 files changed

+100
-7
lines changed

5 files changed

+100
-7
lines changed

APIKit.xcodeproj/project.pbxproj

+12
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
7F7048F31D9D8A1F003C99F6 /* URLEncodedSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7048F21D9D8A1F003C99F6 /* URLEncodedSerialization.swift */; };
4949
7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA1690C1D9D8C80006C982B /* HTTPStub.swift */; };
5050
C5725F4B28D8C36500810D7C /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5725F4A28D8C36500810D7C /* Concurrency.swift */; };
51+
C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */; };
5152
C5FF1DC128A80FFD0059573D /* test.json in Resources */ = {isa = PBXBuildFile; fileRef = C5FF1DC028A80FFD0059573D /* test.json */; };
5253
ECA831481DE4DDBF004EB1B5 /* ProtobufDataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */; };
5354
ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */; };
@@ -130,6 +131,7 @@
130131
7F8ECDFD1B6A799E00234E04 /* Demo.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Demo.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
131132
7FA1690C1D9D8C80006C982B /* HTTPStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPStub.swift; sourceTree = "<group>"; };
132133
C5725F4A28D8C36500810D7C /* Concurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = "<group>"; };
134+
C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = "<group>"; };
133135
C5FF1DC028A80FFD0059573D /* test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test.json; sourceTree = "<group>"; };
134136
ECA831471DE4DDBF004EB1B5 /* ProtobufDataParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtobufDataParser.swift; path = Sources/APIKit/DataParser/ProtobufDataParser.swift; sourceTree = SOURCE_ROOT; };
135137
ECA831491DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtobufDataParserTests.swift; sourceTree = "<group>"; };
@@ -242,6 +244,7 @@
242244
7F698E451D9D680C00F1561D /* RequestTests.swift */,
243245
7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */,
244246
7F698E4A1D9D680C00F1561D /* SessionTests.swift */,
247+
C5B144D628D8D7D000E30ECD /* Concurrency */,
245248
0973EE33259E2DD000879BA2 /* Combine */,
246249
7F698E3B1D9D680C00F1561D /* BodyParametersType */,
247250
7F698E401D9D680C00F1561D /* DataParserType */,
@@ -376,6 +379,14 @@
376379
path = APIKit/Concurrency;
377380
sourceTree = "<group>";
378381
};
382+
C5B144D628D8D7D000E30ECD /* Concurrency */ = {
383+
isa = PBXGroup;
384+
children = (
385+
C5B144D728D8D7DC00E30ECD /* ConcurrencyTests.swift */,
386+
);
387+
path = Concurrency;
388+
sourceTree = "<group>";
389+
};
379390
C5FF1DBF28A80FFD0059573D /* Resources */ = {
380391
isa = PBXGroup;
381392
children = (
@@ -534,6 +545,7 @@
534545
7F698E581D9D680C00F1561D /* RequestTests.swift in Sources */,
535546
ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */,
536547
7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */,
548+
C5B144D828D8D7DC00E30ECD /* ConcurrencyTests.swift in Sources */,
537549
7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */,
538550
0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */,
539551
7FA1690D1D9D8C80006C982B /* HTTPStub.swift in Sources */,

Sources/APIKit/Combine/Combine.swift

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public struct SessionTaskPublisher<Request: APIKit.Request>: Publisher {
6868
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
6969
public extension Session {
7070
/// Calls `sessionTaskPublisher(for:callbackQueue:)` of `Session.shared`.
71+
///
7172
/// - parameter request: The request to be sent.
7273
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
7374
/// - returns: A publisher that wraps a session task for the request.

Sources/APIKit/Concurrency/Concurrency.swift

+16-7
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,31 @@ import Foundation
44

55
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
66
public extension Session {
7+
/// Calls `response(for:callbackQueue:)` of `Session.shared`.
8+
///
9+
/// - parameter request: The request to be sent.
10+
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
11+
/// - returns: `Request.Response`
712
static func response<Request: APIKit.Request>(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response {
813
return try await shared.response(for: request, callbackQueue: callbackQueue)
914
}
1015

16+
/// Convenience method to load `Request.Response` using an `Request`, creates and resumes an `SessionTask` internally.
17+
///
18+
/// - parameter request: The request to be sent.
19+
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
20+
/// - returns: `Request.Response`
1121
func response<Request: APIKit.Request>(for request: Request, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response {
1222
let cancellationHandler = SessionTaskCancellationHandler()
1323
return try await withTaskCancellationHandler(operation: {
14-
return try await withUnsafeThrowingContinuation { continuation in
24+
return try await withCheckedThrowingContinuation { continuation in
25+
guard !Task.isCancelled else {
26+
continuation.resume(throwing: SessionTaskError.taskAlreadyCancelledError)
27+
return
28+
}
1529
Task {
1630
let sessionTask = send(request, callbackQueue: callbackQueue) { result in
17-
switch result {
18-
case .success(let response):
19-
continuation.resume(returning: response)
20-
case .failure(let error):
21-
continuation.resume(throwing: error)
22-
}
31+
continuation.resume(with: result)
2332
}
2433
await cancellationHandler.register(with: sessionTask)
2534
}

Sources/APIKit/Error/SessionTaskError.swift

+3
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ public enum SessionTaskError: Error {
1010

1111
/// Error while creating `Request.Response` from `(Data, URLResponse)`.
1212
case responseError(Error)
13+
14+
/// Error when the `Task` in Concurrency was cancelled before execution.
15+
case taskAlreadyCancelledError
1316
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#if compiler(>=5.6.0) && canImport(_Concurrency)
2+
3+
import XCTest
4+
import APIKit
5+
6+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
7+
final class ConcurrencyTests: XCTestCase {
8+
var adapter: TestSessionAdapter!
9+
var session: Session!
10+
11+
override func setUp() {
12+
super.setUp()
13+
adapter = TestSessionAdapter()
14+
session = Session(adapter: adapter)
15+
}
16+
17+
func testSuccess() async throws {
18+
let dictionary = ["key": "value"]
19+
adapter.data = try XCTUnwrap(JSONSerialization.data(withJSONObject: dictionary, options: []))
20+
21+
let request = TestRequest()
22+
let value = try await session.response(for: request)
23+
XCTAssertEqual((value as? [String: String])?["key"], "value")
24+
}
25+
26+
func testParseDataError() async throws {
27+
adapter.data = "{\"broken\": \"json}".data(using: .utf8, allowLossyConversion: false)
28+
29+
let request = TestRequest()
30+
do {
31+
_ = try await session.response(for: request)
32+
XCTFail()
33+
} catch {
34+
let sessionError = try XCTUnwrap(error as? SessionTaskError)
35+
if case .responseError(let responseError as NSError) = sessionError {
36+
XCTAssertEqual(responseError.domain, NSCocoaErrorDomain)
37+
XCTAssertEqual(responseError.code, 3840)
38+
} else {
39+
XCTFail()
40+
}
41+
}
42+
}
43+
44+
func testCancel() async throws {
45+
let request = TestRequest()
46+
47+
let task = Task {
48+
do {
49+
_ = try await session.response(for: request)
50+
XCTFail()
51+
} catch {
52+
let sessionError = try XCTUnwrap(error as? SessionTaskError)
53+
switch sessionError {
54+
case .taskAlreadyCancelledError:
55+
XCTAssertTrue(Task.isCancelled)
56+
default:
57+
XCTFail()
58+
}
59+
}
60+
}
61+
task.cancel()
62+
_ = try await task.value
63+
64+
XCTAssertTrue(task.isCancelled)
65+
}
66+
}
67+
68+
#endif

0 commit comments

Comments
 (0)