From f02c453bb544085109a02f2f2fe8f2cfb0b7f6e8 Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:14:51 -0700 Subject: [PATCH 1/3] Add Swift 6 support with Sendable conformance - Update Package.swift to swift-tools-version:6.0 - Remove swiftLanguageVersions lock - Add @unchecked Sendable to JSON struct - Add Sendable to Type, SwiftyJSONError, JSONKey enums - Add comprehensive concurrency tests (18 tests) - Update README requirements and CHANGELOG The JSON type can now safely cross actor boundaries in Swift 6 concurrency. Closes #1163 --- CHANGELOG.md | 9 + Package.swift | 5 +- README.md | 3 +- Source/SwiftyJSON/SwiftyJSON.swift | 8 +- Tests/SwiftJSONTests/ConcurrencyTests.swift | 386 ++++++++++++++++++++ 5 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 Tests/SwiftJSONTests/ConcurrencyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0935d3..082b0247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [Full Changelog](https://github.com/SwiftyJSON/SwiftyJSON/compare/2.2.0...HEAD) +### Added +- Swift 6 support with full Sendable conformance ([#1163](https://github.com/SwiftyJSON/SwiftyJSON/issues/1163)) +- Comprehensive concurrency tests for actor boundary validation + +### Changed +- `JSON` struct conforms to `@unchecked Sendable` +- `Type`, `SwiftyJSONError`, `JSONKey` enums conform to `Sendable` +- Minimum Swift tools version updated to 6.0 + **Closed issues:** - 156 compiler errors Mavericks + Xcode 6.2 [\#220](https://github.com/SwiftyJSON/SwiftyJSON/issues/220) diff --git a/Package.swift b/Package.swift index ba89fb5f..fb026e55 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -20,6 +20,5 @@ let package = Package( .copy("Tests.json") ] ) - ], - swiftLanguageVersions: [.v5] + ] ) diff --git a/README.md b/README.md index e1740680..a6f3037a 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ if let userName = result.string { ## Requirements - iOS 8.0+ | macOS 10.10+ | tvOS 9.0+ | watchOS 2.0+ -- Xcode 8 +- Xcode 16+ +- Swift 6.0+ ## Integration diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 97daf4fc..d2f39ef2 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -24,7 +24,7 @@ import Foundation // MARK: - Error // swiftlint:disable line_length -public enum SwiftyJSONError: Int, Swift.Error { +public enum SwiftyJSONError: Int, Swift.Error, Sendable { case unsupportedType = 999 case indexOutOfBounds = 900 case elementTooDeep = 902 @@ -67,7 +67,7 @@ JSON's type definitions. See http://www.json.org */ -public enum Type: Int { +public enum Type: Int, Sendable { case number case string case bool @@ -79,7 +79,7 @@ public enum Type: Int { // MARK: - JSON Base -public struct JSON { +public struct JSON: @unchecked Sendable { /** Creates a JSON using the data. @@ -339,7 +339,7 @@ extension JSON: Swift.Collection { /** * To mark both String and Int can be used in subscript. */ -public enum JSONKey { +public enum JSONKey: Sendable { case index(Int) case key(String) } diff --git a/Tests/SwiftJSONTests/ConcurrencyTests.swift b/Tests/SwiftJSONTests/ConcurrencyTests.swift new file mode 100644 index 00000000..bb51afde --- /dev/null +++ b/Tests/SwiftJSONTests/ConcurrencyTests.swift @@ -0,0 +1,386 @@ +// ConcurrencyTests.swift +// +// Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import SwiftyJSON + +class ConcurrencyTests: XCTestCase { + + // MARK: - Test 1: Actor Boundary Passing + + actor DataProcessor { + func process(_ json: JSON) -> String { + return json["name"].stringValue + } + + func processDictionary(_ json: JSON) -> Int { + return json["count"].intValue + } + + func processArray(_ json: JSON) -> Int { + return json[0].intValue + } + } + + func testActorBoundaryPassing() async { + let json = JSON(["name": "Test"]) + let processor = DataProcessor() + let result = await processor.process(json) + XCTAssertEqual(result, "Test") + } + + func testActorBoundaryPassingWithDictionary() async { + let json = JSON(["count": 42, "status": "active"]) + let processor = DataProcessor() + let result = await processor.processDictionary(json) + XCTAssertEqual(result, 42) + } + + func testActorBoundaryPassingWithArray() async { + let json = JSON([100, 200, 300]) + let processor = DataProcessor() + let result = await processor.processArray(json) + XCTAssertEqual(result, 100) + } + + // MARK: - Test 2: Sendable Closure and Task + + func testSendableClosureWithTask() async { + let json = JSON(["value": "Hello"]) + let task = Task { @Sendable () -> String in + return json["value"].stringValue + } + let result = await task.value + XCTAssertEqual(result, "Hello") + } + + func testSendableClosureWithComplexData() async { + let json = JSON([ + "user": ["name": "Alice", "age": 30], + "active": true + ]) + let task = Task { @Sendable () -> String in + return json["user"]["name"].stringValue + } + let result = await task.value + XCTAssertEqual(result, "Alice") + } + + // MARK: - Test 3: Async JSON Parsing + + func testAsyncJSONParsing() async throws { + let jsonString = "{\"status\":\"ok\"}" + let data = jsonString.data(using: .utf8)! + + let task = Task { + return try JSON(data: data) + } + + let json = try await task.value + let status = json["status"].stringValue + XCTAssertEqual(status, "ok") + } + + func testAsyncJSONParsingWithComplexData() async throws { + let jsonString = "{\"results\": [{\"id\": 1, \"name\": \"Item1\"}, {\"id\": 2, \"name\": \"Item2\"}]}" + let data = jsonString.data(using: .utf8)! + + let task = Task { + return try JSON(data: data) + } + + let json = try await task.value + XCTAssertEqual(json["results"].arrayValue.count, 2) + XCTAssertEqual(json["results"][0]["name"].stringValue, "Item1") + } + + // MARK: - Test 4: Concurrent Read Access with TaskGroup + + func testConcurrentReadAccess() async { + let json = JSON(["values": [1, 2, 3, 4, 5]]) + + await withTaskGroup(of: Int.self) { group in + for i in 0..<5 { + group.addTask { + return json["values"][i].intValue + } + } + + var sum = 0 + for await value in group { + sum += value + } + XCTAssertEqual(sum, 15) + } + } + + func testConcurrentReadAccessWithDictionary() async { + let json = JSON([ + "items": [ + ["id": 1, "value": 10], + ["id": 2, "value": 20], + ["id": 3, "value": 30] + ] + ]) + + await withTaskGroup(of: Int.self) { group in + for i in 0..<3 { + group.addTask { + return json["items"][i]["value"].intValue + } + } + + var sum = 0 + for await value in group { + sum += value + } + XCTAssertEqual(sum, 60) + } + } + + // MARK: - Test 5: MainActor Isolation + + @MainActor + func mainActorFunction(_ json: JSON) -> String { + return json["title"].stringValue + } + + func testMainActorIsolation() async { + let json = JSON(["title": "Main Thread Task"]) + let result = await mainActorFunction(json) + XCTAssertEqual(result, "Main Thread Task") + } + + // MARK: - Test 6: Task.detached Isolation + + func testTaskDetachedIsolation() async { + let json = JSON(["data": "detached"]) + + let task = Task.detached { @Sendable () -> String in + return json["data"].stringValue + } + + let result = await task.value + XCTAssertEqual(result, "detached") + } + + // MARK: - Test 7: Complex Nested JSON + + func testComplexNestedJSONConcurrency() async { + let json = JSON([ + "users": [ + [ + "id": 1, + "name": "Alice", + "posts": [ + ["id": 101, "title": "Post 1"], + ["id": 102, "title": "Post 2"] + ] + ], + [ + "id": 2, + "name": "Bob", + "posts": [ + ["id": 201, "title": "Post 3"] + ] + ] + ] + ]) + + await withTaskGroup(of: (Int, String).self) { group in + for i in 0..<2 { + group.addTask { + let userId = json["users"][i]["id"].intValue + let userName = json["users"][i]["name"].stringValue + return (userId, userName) + } + } + + var names: [String] = [] + for await (_, name) in group { + names.append(name) + } + + XCTAssert(names.contains("Alice")) + XCTAssert(names.contains("Bob")) + } + } + + func testComplexNestedJSONAccess() async { + let json = JSON([ + [ + ["deep": [1, 2, 3]], + ["deep": [4, 5, 6]] + ], + [ + ["deep": [7, 8, 9]], + ["deep": [10, 11, 12]] + ] + ]) + + let task = Task { @Sendable () -> Int in + return json[0][1]["deep"][2].intValue + } + + let result = await task.value + XCTAssertEqual(result, 6) + } + + // MARK: - Test 8: All JSON Types in Concurrent Context + + func testAllJSONTypesInConcurrency() async { + let json = JSON([ + "string": "value", + "number": 42, + "bool": true, + "null": NSNull(), + "array": [1, 2, 3], + "dictionary": ["nested": "object"] + ]) + + await withTaskGroup(of: String.self) { group in + group.addTask { + return json["string"].stringValue + } + group.addTask { + return String(json["number"].intValue) + } + group.addTask { + return String(json["bool"].boolValue) + } + group.addTask { + return json["null"].type == .null ? "null" : "not_null" + } + group.addTask { + return String(json["array"].arrayValue.count) + } + group.addTask { + return json["dictionary"]["nested"].stringValue + } + + var results: [String] = [] + for await result in group { + results.append(result) + } + + XCTAssertEqual(results.count, 6) + XCTAssert(results.contains("value")) + XCTAssert(results.contains("42")) + XCTAssert(results.contains("true")) + XCTAssert(results.contains("null")) + XCTAssert(results.contains("3")) + XCTAssert(results.contains("object")) + } + } + + // MARK: - Test 9: Error Handling Across Actor Boundaries + + actor ErrorProcessor { + func processJSON(_ json: JSON) throws -> String { + guard json["required"].exists() else { + throw SwiftyJSONError.notExist + } + return json["required"].stringValue + } + + func processWithTypeError(_ json: JSON) throws -> Int { + guard json["count"].type == .number else { + throw SwiftyJSONError.wrongType + } + return json["count"].intValue + } + } + + func testErrorHandlingAcrossActorBoundaries() async { + let json = JSON(["other": "value"]) + let processor = ErrorProcessor() + + do { + _ = try await processor.processJSON(json) + XCTFail("Should have thrown notExist error") + } catch SwiftyJSONError.notExist { + // Expected behavior + } catch { + XCTFail("Unexpected error type: \(error)") + } + } + + func testErrorHandlingWithWrongType() async { + let json = JSON(["count": "not_a_number"]) + let processor = ErrorProcessor() + + do { + _ = try await processor.processWithTypeError(json) + XCTFail("Should have thrown wrongType error") + } catch SwiftyJSONError.wrongType { + // Expected behavior + } catch { + XCTFail("Unexpected error type: \(error)") + } + } + + func testSuccessfulErrorHandling() async { + let json = JSON(["required": "present", "count": 123]) + let processor = ErrorProcessor() + + do { + let result = try await processor.processJSON(json) + XCTAssertEqual(result, "present") + + let count = try await processor.processWithTypeError(json) + XCTAssertEqual(count, 123) + } catch { + XCTFail("Should not have thrown error: \(error)") + } + } + + // MARK: - Test 10: Mixed Async Operations + + func testMixedAsyncOperations() async throws { + let jsonString = "{\"users\": [{\"id\": 1, \"name\": \"User1\"}, {\"id\": 2, \"name\": \"User2\"}]}" + let data = jsonString.data(using: .utf8)! + + let parseTask = Task { + return try JSON(data: data) + } + + let json = try await parseTask.value + + let names = await withTaskGroup(of: String.self) { group in + for i in 0..<2 { + group.addTask { + return json["users"][i]["name"].stringValue + } + } + + var result: [String] = [] + for await name in group { + result.append(name) + } + return result + } + + XCTAssertEqual(names.count, 2) + XCTAssert(names.contains("User1")) + XCTAssert(names.contains("User2")) + } +} From 636b70007f3ea3fe57df40151faaf1d62042fdaf Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:27:31 -0700 Subject: [PATCH 2/3] Consolidate concurrency tests to 3 focused tests Each test now directly corresponds to a type made Sendable: - testJSONSendable: JSON crosses actor boundary - testTypeSendable: Type enum crosses actor boundary - testSwiftyJSONErrorSendable: SwiftyJSONError crosses actor boundary Removed redundant tests that proved the same thing in different ways (TaskGroup, Task.detached, MainActor variations). Added missing coverage for Type enum Sendable conformance. --- Tests/SwiftJSONTests/ConcurrencyTests.swift | 375 ++------------------ 1 file changed, 39 insertions(+), 336 deletions(-) diff --git a/Tests/SwiftJSONTests/ConcurrencyTests.swift b/Tests/SwiftJSONTests/ConcurrencyTests.swift index bb51afde..a6eaa099 100644 --- a/Tests/SwiftJSONTests/ConcurrencyTests.swift +++ b/Tests/SwiftJSONTests/ConcurrencyTests.swift @@ -23,364 +23,67 @@ import XCTest import SwiftyJSON +/// Tests verifying Sendable conformance for Swift 6 concurrency. +/// Each test targets a specific type made Sendable in this PR. class ConcurrencyTests: XCTestCase { - // MARK: - Test 1: Actor Boundary Passing - - actor DataProcessor { - func process(_ json: JSON) -> String { - return json["name"].stringValue - } - - func processDictionary(_ json: JSON) -> Int { - return json["count"].intValue - } - - func processArray(_ json: JSON) -> Int { - return json[0].intValue - } - } - - func testActorBoundaryPassing() async { - let json = JSON(["name": "Test"]) - let processor = DataProcessor() - let result = await processor.process(json) - XCTAssertEqual(result, "Test") - } - - func testActorBoundaryPassingWithDictionary() async { - let json = JSON(["count": 42, "status": "active"]) - let processor = DataProcessor() - let result = await processor.processDictionary(json) - XCTAssertEqual(result, 42) - } - - func testActorBoundaryPassingWithArray() async { - let json = JSON([100, 200, 300]) - let processor = DataProcessor() - let result = await processor.processArray(json) - XCTAssertEqual(result, 100) - } - - // MARK: - Test 2: Sendable Closure and Task - - func testSendableClosureWithTask() async { - let json = JSON(["value": "Hello"]) - let task = Task { @Sendable () -> String in - return json["value"].stringValue - } - let result = await task.value - XCTAssertEqual(result, "Hello") - } - - func testSendableClosureWithComplexData() async { - let json = JSON([ - "user": ["name": "Alice", "age": 30], - "active": true - ]) - let task = Task { @Sendable () -> String in - return json["user"]["name"].stringValue - } - let result = await task.value - XCTAssertEqual(result, "Alice") - } - - // MARK: - Test 3: Async JSON Parsing - - func testAsyncJSONParsing() async throws { - let jsonString = "{\"status\":\"ok\"}" - let data = jsonString.data(using: .utf8)! - - let task = Task { - return try JSON(data: data) + actor Processor { + func extract(_ json: JSON) -> String { + json["name"].stringValue } - let json = try await task.value - let status = json["status"].stringValue - XCTAssertEqual(status, "ok") - } - - func testAsyncJSONParsingWithComplexData() async throws { - let jsonString = "{\"results\": [{\"id\": 1, \"name\": \"Item1\"}, {\"id\": 2, \"name\": \"Item2\"}]}" - let data = jsonString.data(using: .utf8)! - - let task = Task { - return try JSON(data: data) + func extractType(_ json: JSON) -> Type { + json["value"].type } - let json = try await task.value - XCTAssertEqual(json["results"].arrayValue.count, 2) - XCTAssertEqual(json["results"][0]["name"].stringValue, "Item1") - } - - // MARK: - Test 4: Concurrent Read Access with TaskGroup - - func testConcurrentReadAccess() async { - let json = JSON(["values": [1, 2, 3, 4, 5]]) - - await withTaskGroup(of: Int.self) { group in - for i in 0..<5 { - group.addTask { - return json["values"][i].intValue - } - } - - var sum = 0 - for await value in group { - sum += value - } - XCTAssertEqual(sum, 15) - } - } - - func testConcurrentReadAccessWithDictionary() async { - let json = JSON([ - "items": [ - ["id": 1, "value": 10], - ["id": 2, "value": 20], - ["id": 3, "value": 30] - ] - ]) - - await withTaskGroup(of: Int.self) { group in - for i in 0..<3 { - group.addTask { - return json["items"][i]["value"].intValue - } - } - - var sum = 0 - for await value in group { - sum += value - } - XCTAssertEqual(sum, 60) - } - } - - // MARK: - Test 5: MainActor Isolation - - @MainActor - func mainActorFunction(_ json: JSON) -> String { - return json["title"].stringValue - } - - func testMainActorIsolation() async { - let json = JSON(["title": "Main Thread Task"]) - let result = await mainActorFunction(json) - XCTAssertEqual(result, "Main Thread Task") - } - - // MARK: - Test 6: Task.detached Isolation - - func testTaskDetachedIsolation() async { - let json = JSON(["data": "detached"]) - - let task = Task.detached { @Sendable () -> String in - return json["data"].stringValue - } - - let result = await task.value - XCTAssertEqual(result, "detached") - } - - // MARK: - Test 7: Complex Nested JSON - - func testComplexNestedJSONConcurrency() async { - let json = JSON([ - "users": [ - [ - "id": 1, - "name": "Alice", - "posts": [ - ["id": 101, "title": "Post 1"], - ["id": 102, "title": "Post 2"] - ] - ], - [ - "id": 2, - "name": "Bob", - "posts": [ - ["id": 201, "title": "Post 3"] - ] - ] - ] - ]) - - await withTaskGroup(of: (Int, String).self) { group in - for i in 0..<2 { - group.addTask { - let userId = json["users"][i]["id"].intValue - let userName = json["users"][i]["name"].stringValue - return (userId, userName) - } - } - - var names: [String] = [] - for await (_, name) in group { - names.append(name) - } - - XCTAssert(names.contains("Alice")) - XCTAssert(names.contains("Bob")) - } - } - - func testComplexNestedJSONAccess() async { - let json = JSON([ - [ - ["deep": [1, 2, 3]], - ["deep": [4, 5, 6]] - ], - [ - ["deep": [7, 8, 9]], - ["deep": [10, 11, 12]] - ] - ]) - - let task = Task { @Sendable () -> Int in - return json[0][1]["deep"][2].intValue - } - - let result = await task.value - XCTAssertEqual(result, 6) - } - - // MARK: - Test 8: All JSON Types in Concurrent Context - - func testAllJSONTypesInConcurrency() async { - let json = JSON([ - "string": "value", - "number": 42, - "bool": true, - "null": NSNull(), - "array": [1, 2, 3], - "dictionary": ["nested": "object"] - ]) - - await withTaskGroup(of: String.self) { group in - group.addTask { - return json["string"].stringValue - } - group.addTask { - return String(json["number"].intValue) - } - group.addTask { - return String(json["bool"].boolValue) - } - group.addTask { - return json["null"].type == .null ? "null" : "not_null" - } - group.addTask { - return String(json["array"].arrayValue.count) - } - group.addTask { - return json["dictionary"]["nested"].stringValue - } - - var results: [String] = [] - for await result in group { - results.append(result) - } - - XCTAssertEqual(results.count, 6) - XCTAssert(results.contains("value")) - XCTAssert(results.contains("42")) - XCTAssert(results.contains("true")) - XCTAssert(results.contains("null")) - XCTAssert(results.contains("3")) - XCTAssert(results.contains("object")) - } - } - - // MARK: - Test 9: Error Handling Across Actor Boundaries - - actor ErrorProcessor { - func processJSON(_ json: JSON) throws -> String { + func requireField(_ json: JSON) throws -> String { guard json["required"].exists() else { throw SwiftyJSONError.notExist } return json["required"].stringValue } - - func processWithTypeError(_ json: JSON) throws -> Int { - guard json["count"].type == .number else { - throw SwiftyJSONError.wrongType - } - return json["count"].intValue - } } - func testErrorHandlingAcrossActorBoundaries() async { - let json = JSON(["other": "value"]) - let processor = ErrorProcessor() - - do { - _ = try await processor.processJSON(json) - XCTFail("Should have thrown notExist error") - } catch SwiftyJSONError.notExist { - // Expected behavior - } catch { - XCTFail("Unexpected error type: \(error)") - } - } + /// Verifies JSON conforms to Sendable (can cross actor boundary) + func testJSONSendable() async { + let json = JSON(["name": "test", "count": 42]) + let processor = Processor() - func testErrorHandlingWithWrongType() async { - let json = JSON(["count": "not_a_number"]) - let processor = ErrorProcessor() + let result = await processor.extract(json) - do { - _ = try await processor.processWithTypeError(json) - XCTFail("Should have thrown wrongType error") - } catch SwiftyJSONError.wrongType { - // Expected behavior - } catch { - XCTFail("Unexpected error type: \(error)") - } + XCTAssertEqual(result, "test") } - func testSuccessfulErrorHandling() async { - let json = JSON(["required": "present", "count": 123]) - let processor = ErrorProcessor() + /// Verifies Type enum conforms to Sendable (can be returned across actor boundary) + func testTypeSendable() async { + let processor = Processor() - do { - let result = try await processor.processJSON(json) - XCTAssertEqual(result, "present") + let stringType = await processor.extractType(JSON(["value": "hello"])) + let numberType = await processor.extractType(JSON(["value": 123])) + let boolType = await processor.extractType(JSON(["value": true])) + let nullType = await processor.extractType(JSON(["value": NSNull()])) + let arrayType = await processor.extractType(JSON(["value": [1, 2, 3]])) + let dictType = await processor.extractType(JSON(["value": ["nested": "object"]])) - let count = try await processor.processWithTypeError(json) - XCTAssertEqual(count, 123) - } catch { - XCTFail("Should not have thrown error: \(error)") - } + XCTAssertEqual(stringType, .string) + XCTAssertEqual(numberType, .number) + XCTAssertEqual(boolType, .bool) + XCTAssertEqual(nullType, .null) + XCTAssertEqual(arrayType, .array) + XCTAssertEqual(dictType, .dictionary) } - // MARK: - Test 10: Mixed Async Operations + /// Verifies SwiftyJSONError conforms to Sendable (can be thrown across actor boundary) + func testSwiftyJSONErrorSendable() async { + let processor = Processor() - func testMixedAsyncOperations() async throws { - let jsonString = "{\"users\": [{\"id\": 1, \"name\": \"User1\"}, {\"id\": 2, \"name\": \"User2\"}]}" - let data = jsonString.data(using: .utf8)! - - let parseTask = Task { - return try JSON(data: data) - } - - let json = try await parseTask.value - - let names = await withTaskGroup(of: String.self) { group in - for i in 0..<2 { - group.addTask { - return json["users"][i]["name"].stringValue - } - } - - var result: [String] = [] - for await name in group { - result.append(name) - } - return result + do { + _ = try await processor.requireField(JSON(["other": "value"])) + XCTFail("Should have thrown") + } catch SwiftyJSONError.notExist { + // Error successfully crossed actor boundary + } catch { + XCTFail("Unexpected error: \(error)") } - - XCTAssertEqual(names.count, 2) - XCTAssert(names.contains("User1")) - XCTAssert(names.contains("User2")) } } From d24f061e37c6e0f40b99a53846e9d4682fe01d57 Mon Sep 17 00:00:00 2001 From: wongzigii Date: Wed, 4 Feb 2026 17:10:27 +0800 Subject: [PATCH 3/3] update ci --- .github/workflows/ci.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2680e910..a3816d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,12 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Set up Swift - uses: swift-actions/setup-swift@v2 - with: - swift-version: '6.0' + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: Build - run: swift build --build-tests + run: xcrun swift build --build-tests - name: Run tests - run: swift test \ No newline at end of file + run: xcrun swift test \ No newline at end of file