diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92dbab2..f020394 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,15 +4,16 @@ on: push: { branches: [ main ] } jobs: - macos-test: - runs-on: macos-latest - steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest - - uses: actions/checkout@v4 - - name: Darwin build & test - run: swift test --skip IntegrationTests + # Disabled because `latest` is only XCode 16.2 which has Swift 6.0.3 + # macos-test: + # runs-on: macos-latest + # steps: + # - uses: maxim-lobanov/setup-xcode@v1 + # with: + # xcode-version: latest + # - uses: actions/checkout@v4 + # - name: Darwin build & test + # run: swift test --enable-all-traits --skip IntegrationTests linux-test: runs-on: ubuntu-latest container: @@ -20,4 +21,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: Linux build & test - run: swift test --skip IntegrationTests + run: swift test --traits ClientNIO,ServerVapor --skip IntegrationTests diff --git a/.spi.yml b/.spi.yml index fbf5631..7ea7490 100644 --- a/.spi.yml +++ b/.spi.yml @@ -4,7 +4,4 @@ builder: - documentation_targets: - "Haystack" - "HaystackClient" - - "HaystackClientDarwin" - - "HaystackClientNIO" - "HaystackServer" - - "HaystackServerVapor" diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..2ff2e98 --- /dev/null +++ b/.swiftformat @@ -0,0 +1 @@ +--ifdef no-indent diff --git a/Package.swift b/Package.swift index 390eb95..77fc3c6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,211 +1,80 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.1 import PackageDescription -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - let package = Package( - name: "Haystack", - platforms: [ - .macOS(.v12), - .iOS(.v15), - .tvOS(.v15), - .watchOS(.v8), - ], - products: [ - .library( - name: "Haystack", - targets: ["Haystack"] - ), - .library( - name: "HaystackClientDarwin", - targets: [ - "HaystackClient", - "HaystackClientDarwin", - ] - ), - .library( - name: "HaystackClientNIO", - targets: [ - "HaystackClient", - "HaystackClientNIO", - ] - ), - .library( - name: "HaystackServer", - targets: [ - "HaystackServer", - ] - ), - .library( - name: "HaystackServerVapor", - targets: [ - "HaystackServerVapor", - ] - ), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "3.0.0"), - .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), - ], - targets: [ - .target( - name: "Haystack", - dependencies: [] - ), - .target( - name: "HaystackClient", - dependencies: [ - "Haystack", - .product(name: "Crypto", package: "swift-crypto"), - ] - ), - .target( - name: "HaystackClientDarwin", - dependencies: [ - "Haystack", - "HaystackClient", - ] - ), - .target( - name: "HaystackClientNIO", - dependencies: [ - "Haystack", - "HaystackClient", - .product(name: "AsyncHTTPClient", package: "async-http-client"), - ] - ), - .target( - name: "HaystackServer", - dependencies: [ - "Haystack", - ] - ), - .target( - name: "HaystackServerVapor", - dependencies: [ - "Haystack", - "HaystackServer", - .product(name: "Vapor", package: "vapor"), - ] - ), +let package = Package( + name: "Haystack", + platforms: [ + .macOS(.v12), + .iOS(.v15), + .tvOS(.v15), + .watchOS(.v8), + ], + products: [ + .library( + name: "Haystack", + targets: ["Haystack"] + ), + .library( + name: "HaystackClient", + targets: [ + "HaystackClient", + ] + ), + .library( + name: "HaystackServer", + targets: [ + "HaystackServer", + ] + ), + ], + traits: [ + "ServerVapor", + "ClientNIO", + "ClientDarwin", + .default(enabledTraits: []), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "3.0.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), + ], + targets: [ + .target( + name: "Haystack", + dependencies: [] + ), + .target( + name: "HaystackClient", + dependencies: [ + "Haystack", + .product(name: "Crypto", package: "swift-crypto"), + .product(name: "AsyncHTTPClient", package: "async-http-client", condition: .when(traits: ["ClientNIO"])), + ] + ), + .target( + name: "HaystackServer", + dependencies: [ + "Haystack", + .product(name: "Vapor", package: "vapor", condition: .when(traits: ["ServerVapor"])), + ] + ), - // Tests - .testTarget( - name: "HaystackTests", - dependencies: ["Haystack"] - ), - .testTarget( - name: "HaystackClientTests", - dependencies: ["HaystackClient"] - ), - .testTarget( - name: "HaystackClientNIOIntegrationTests", - dependencies: ["HaystackClientNIO"] - ), - .testTarget( - name: "HaystackClientDarwinIntegrationTests", - dependencies: ["HaystackClientDarwin"] - ), - .testTarget( - name: "HaystackServerTests", - dependencies: ["HaystackServer"] - ), - .testTarget( - name: "HaystackServerVaporTests", - dependencies: ["HaystackServerVapor", .product(name: "XCTVapor", package: "vapor")] - ), - ] - ) -#else - let package = Package( - name: "Haystack", - products: [ - .library( - name: "Haystack", - targets: ["Haystack"] - ), - .library( - name: "HaystackClientNIO", - targets: [ - "HaystackClient", - "HaystackClientNIO", - ] - ), - .library( - name: "HaystackServer", - targets: [ - "HaystackServer", - ] - ), - .library( - name: "HaystackServerVapor", - targets: [ - "HaystackServerVapor", - ] - ), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "3.0.0"), - .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), - ], - targets: [ - .target( - name: "Haystack", - dependencies: [] - ), - .target( - name: "HaystackClient", - dependencies: [ - "Haystack", - .product(name: "Crypto", package: "swift-crypto"), - ] - ), - .target( - name: "HaystackClientNIO", - dependencies: [ - "Haystack", - "HaystackClient", - .product(name: "AsyncHTTPClient", package: "async-http-client"), - ] - ), - .target( - name: "HaystackServer", - dependencies: [ - "Haystack", - ] - ), - .target( - name: "HaystackServerVapor", - dependencies: [ - "Haystack", - "HaystackServer", - .product(name: "Vapor", package: "vapor"), - ] - ), - - // Tests - .testTarget( - name: "HaystackTests", - dependencies: ["Haystack"] - ), - .testTarget( - name: "HaystackClientTests", - dependencies: ["HaystackClient"] - ), - .testTarget( - name: "HaystackClientNIOIntegrationTests", - dependencies: ["HaystackClientNIO"] - ), - .testTarget( - name: "HaystackServerTests", - dependencies: ["HaystackServer"] - ), - .testTarget( - name: "HaystackServerVaporTests", - dependencies: ["HaystackServerVapor", .product(name: "XCTVapor", package: "vapor")] - ), - ] - ) -#endif + // Tests + .testTarget( + name: "HaystackTests", + dependencies: ["Haystack"] + ), + .testTarget( + name: "HaystackClientTests", + dependencies: ["HaystackClient"] + ), + .testTarget( + name: "HaystackServerTests", + dependencies: [ + "HaystackServer", + .product(name: "VaporTesting", package: "vapor", condition: .when(traits: ["ServerVapor"])), + ] + ), + ] +) diff --git a/README.md b/README.md index a8d7477..5a5637f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,51 @@ This contains the [Haystack type-system primitives](https://project-haystack.org/doc/docHaystack/Kinds) and utilities to interact with them. -### HaystackClientDarwin +### HaystackClient + +This defines the main functionality of Haystack API clients. It should not be imported directly; +its assets are imported automatically by `HaystackClientDarwin` or `HaystackClientNIO`. + +Once you create a client, you can use it to make requests: + +```swift +func yesterdaysValues() async throws -> Grid { + let client = ... + + // Open and authenticate. This must be called before requests can be made + try await client.open() + + // Request the historical values for @28e7fb7d-e20316e0 + let grid = try await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .yesterday) + + // Close the client session and log out + try await client.close() + + return grid +} +``` + +### HaystackServer + +This defines the standard functionality and data processing of Haystack API servers, based on generic backing data +stores. In most cases, Haystack servers should use the `HaystackServer` class and customize storage behavior by +implementing the `RecordStore`, `HistoryStore`, and `WatchStore` protocols. + +```swift +struct InfluxHistoryStore: HistoryStore { + // Define storage behavior here + ... +} + +let server = HaystackServer( + historyStore: InfluxHistoryStore(), + ... +) +``` + +## Available Traits + +### ClientDarwin A Darwin-only client driver for the [Haystack HTTP API](https://project-haystack.org/doc/docHaystack/HttpApi) that @@ -60,7 +104,7 @@ to reduce dependencies. Here's an example of how to use it: ```swift -import HaystackClientDarwin +import HaystackClient func client() throws -> Client { return try Client( @@ -71,7 +115,7 @@ func client() throws -> Client { } ``` -### HaystackClientNIO +### ClientNIO A cross-platform client driver for the [Haystack HTTP API](https://project-haystack.org/doc/docHaystack/HttpApi) that @@ -81,7 +125,7 @@ are deploying to Darwin platforms and are willing to accept more dependencies. Here's an example of how to use it: ```swift -import HaystackClientNIO +import HaystackClient func client() throws -> Client { let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) @@ -94,30 +138,6 @@ func client() throws -> Client { } ``` -### HaystackClient - -This defines the main functionality of Haystack API clients. It should not be imported directly; -its assets are imported automatically by `HaystackClientDarwin` or `HaystackClientNIO`. - -Once you create a client, you can use it to make requests: - -```swift -func yesterdaysValues() async throws -> Grid { - let client = ... - - // Open and authenticate. This must be called before requests can be made - try await client.open() - - // Request the historical values for @28e7fb7d-e20316e0 - let grid = try await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .yesterday) - - // Close the client session and log out - try await client.close() - - return grid -} -``` - ### HaystackServerVapor A server for the [Haystack HTTP API](https://project-haystack.org/doc/docHaystack/HttpApi) that uses @@ -135,22 +155,14 @@ try app.register(collection: HaystackRouteCollection(delegate: ...)) The delegate is a protocol that can be implemented however the user sees fit, although the standard Haystack implementation is defined in `HaystackServer`. -### HaystackServer +## Contributing -This defines the standard functionality and data processing of Haystack API servers, based on generic backing data -stores. In most cases, Haystack servers should use the `HaystackServer` class and customize storage behavior by -implementing the `RecordStore`, `HistoryStore`, and `WatchStore` protocols. +### Integration tests -```swift -struct InfluxHistoryStore: HistoryStore { - // Define storage behavior here - ... -} +To run the integration tests, run a local Haxall server: -let server = HaystackServer( - historyStore: InfluxHistoryStore(), - ... -) +```sh +docker run -e 'SU_PASS=su' -p '8080:8080' needleinajaystack/haxall:latest ``` ## License diff --git a/Sources/Haystack/API/HisItem.swift b/Sources/Haystack/API/HisItem.swift index 5f7741c..f542b2b 100644 --- a/Sources/Haystack/API/HisItem.swift +++ b/Sources/Haystack/API/HisItem.swift @@ -1,5 +1,5 @@ /// A timestamp/value pair. -public struct HisItem { +public struct HisItem: Sendable { public let ts: DateTime public let val: any Val diff --git a/Sources/Haystack/API/HisReadRange.swift b/Sources/Haystack/API/HisReadRange.swift index 6814515..f35c3a6 100644 --- a/Sources/Haystack/API/HisReadRange.swift +++ b/Sources/Haystack/API/HisReadRange.swift @@ -1,7 +1,7 @@ import Foundation /// Query-able DateTime ranges, which support relative and absolute values. -public enum HisReadRange { +public enum HisReadRange: Sendable { case today case yesterday case date(Haystack.Date) diff --git a/Sources/Haystack/DateTime.swift b/Sources/Haystack/DateTime.swift index 0d78428..cfa2303 100644 --- a/Sources/Haystack/DateTime.swift +++ b/Sources/Haystack/DateTime.swift @@ -191,7 +191,7 @@ public struct DateTime: Val { } } -var calendar = Calendar(identifier: .gregorian) +let calendar = Calendar(identifier: .gregorian) // DateTime + Codable extension DateTime { diff --git a/Sources/Haystack/Val.swift b/Sources/Haystack/Val.swift index e5d1ab6..6359983 100644 --- a/Sources/Haystack/Val.swift +++ b/Sources/Haystack/Val.swift @@ -24,7 +24,7 @@ extension Val { } } -public enum ValType: String, CaseIterable { +public enum ValType: String, CaseIterable, Sendable { case Bool case Coord case Date diff --git a/Sources/HaystackClientDarwin/Client+URLSession.swift b/Sources/HaystackClient/ClientDarwin/Client+URLSession.swift similarity index 97% rename from Sources/HaystackClientDarwin/Client+URLSession.swift rename to Sources/HaystackClient/ClientDarwin/Client+URLSession.swift index 0d375d4..9e7d154 100644 --- a/Sources/HaystackClientDarwin/Client+URLSession.swift +++ b/Sources/HaystackClient/ClientDarwin/Client+URLSession.swift @@ -1,5 +1,5 @@ +#if ClientDarwin import Foundation -import HaystackClient public extension Client { /// Create a Haystack API Client that uses a `URLSession` from `Foundation` that @@ -26,3 +26,4 @@ public extension Client { ) } } +#endif diff --git a/Sources/HaystackClientDarwin/URLSessionFetcher.swift b/Sources/HaystackClient/ClientDarwin/URLSessionFetcher.swift similarity index 98% rename from Sources/HaystackClientDarwin/URLSessionFetcher.swift rename to Sources/HaystackClient/ClientDarwin/URLSessionFetcher.swift index 004360c..c54058e 100644 --- a/Sources/HaystackClientDarwin/URLSessionFetcher.swift +++ b/Sources/HaystackClient/ClientDarwin/URLSessionFetcher.swift @@ -1,5 +1,5 @@ +#if ClientDarwin import Foundation -import HaystackClient /// A Haystack API Client fetcher based on `URLSession`. This is only available on Darwin platforms. struct URLSessionFetcher: Fetcher { @@ -50,3 +50,4 @@ struct URLSessionFetcher: Fetcher { public enum URLSessionFetcherError: Error { case invalidUrl(String) } +#endif diff --git a/Sources/HaystackClientNIO/Client+HTTPClient.swift b/Sources/HaystackClient/ClientNIO/Client+HTTPClient.swift similarity index 97% rename from Sources/HaystackClientNIO/Client+HTTPClient.swift rename to Sources/HaystackClient/ClientNIO/Client+HTTPClient.swift index 2048218..62d3e74 100644 --- a/Sources/HaystackClientNIO/Client+HTTPClient.swift +++ b/Sources/HaystackClient/ClientNIO/Client+HTTPClient.swift @@ -1,6 +1,6 @@ +#if ClientNIO import AsyncHTTPClient import Foundation -import HaystackClient import NIO public extension Client { @@ -29,3 +29,4 @@ public extension Client { ) } } +#endif diff --git a/Sources/HaystackClientNIO/HTTPClientFetcher.swift b/Sources/HaystackClient/ClientNIO/HTTPClientFetcher.swift similarity index 98% rename from Sources/HaystackClientNIO/HTTPClientFetcher.swift rename to Sources/HaystackClient/ClientNIO/HTTPClientFetcher.swift index 2513c5c..663d084 100644 --- a/Sources/HaystackClientNIO/HTTPClientFetcher.swift +++ b/Sources/HaystackClient/ClientNIO/HTTPClientFetcher.swift @@ -1,6 +1,6 @@ +#if ClientNIO import AsyncHTTPClient import Foundation -import HaystackClient import NIO import NIOFoundationCompat @@ -51,3 +51,4 @@ struct HTTPClientFetcher: Fetcher { ) } } +#endif diff --git a/Sources/HaystackClientDarwin/Exports.swift b/Sources/HaystackClientDarwin/Exports.swift deleted file mode 100644 index 4b7fe47..0000000 --- a/Sources/HaystackClientDarwin/Exports.swift +++ /dev/null @@ -1 +0,0 @@ -@_exported import HaystackClient diff --git a/Sources/HaystackClientNIO/Exports.swift b/Sources/HaystackClientNIO/Exports.swift deleted file mode 100644 index 4b7fe47..0000000 --- a/Sources/HaystackClientNIO/Exports.swift +++ /dev/null @@ -1 +0,0 @@ -@_exported import HaystackClient diff --git a/Sources/HaystackServer/HaystackServer.swift b/Sources/HaystackServer/HaystackServer.swift index 69f4db9..8152d03 100644 --- a/Sources/HaystackServer/HaystackServer.swift +++ b/Sources/HaystackServer/HaystackServer.swift @@ -3,22 +3,22 @@ import Haystack /// A HaystackServer is a server that implements the Haystack API. /// It translates API calls into operations on the underlying data stores. -public class HaystackServer: API { +public final class HaystackServer: API, Sendable { let recordStore: RecordStore let historyStore: HistoryStore let watchStore: WatchStore - let onInvokeAction: (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid - let onEval: (String) async throws -> Haystack.Grid + let onInvokeAction: @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid + let onEval: @Sendable (String) async throws -> Haystack.Grid public init( recordStore: RecordStore, historyStore: HistoryStore, watchStore: WatchStore, - onInvokeAction: @escaping (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in + onInvokeAction: @escaping @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in GridBuilder().toGrid() }, - onEval: @escaping (String) async throws -> Haystack.Grid = { _ in + onEval: @escaping @Sendable (String) async throws -> Haystack.Grid = { _ in GridBuilder().toGrid() } ) { diff --git a/Sources/HaystackServer/ServerVapor/Application+Haystack.swift b/Sources/HaystackServer/ServerVapor/Application+Haystack.swift new file mode 100644 index 0000000..5582047 --- /dev/null +++ b/Sources/HaystackServer/ServerVapor/Application+Haystack.swift @@ -0,0 +1,28 @@ +#if ServerVapor +import Haystack +import Vapor + +extension Application { + struct HaystackServerKey: StorageKey { + typealias Value = API & Sendable + } + + public var haystack: (API & Sendable)? { + get { + storage[HaystackServerKey.self] + } + set { + storage[HaystackServerKey.self] = newValue + } + } +} + +extension Request { + func haystack() throws -> any API { + guard let haystack = application.haystack else { + fatalError("HaystackServer is not configured in the Vapor application.") + } + return haystack + } +} +#endif diff --git a/Sources/HaystackServerVapor/HTTPMediaType+zinc.swift b/Sources/HaystackServer/ServerVapor/HTTPMediaType+zinc.swift similarity index 91% rename from Sources/HaystackServerVapor/HTTPMediaType+zinc.swift rename to Sources/HaystackServer/ServerVapor/HTTPMediaType+zinc.swift index 6938557..06c975f 100644 --- a/Sources/HaystackServerVapor/HTTPMediaType+zinc.swift +++ b/Sources/HaystackServer/ServerVapor/HTTPMediaType+zinc.swift @@ -1,6 +1,8 @@ +#if ServerVapor import Vapor public extension HTTPMediaType { /// The `application/zinc` media type: https://project-haystack.org/doc/docHaystack/Zinc static let zinc = HTTPMediaType(type: "text", subType: "zinc", parameters: ["charset": "utf-8"]) } +#endif diff --git a/Sources/HaystackServerVapor/HaystackRouteCollection.swift b/Sources/HaystackServer/ServerVapor/HaystackRouteCollection.swift similarity index 89% rename from Sources/HaystackServerVapor/HaystackRouteCollection.swift rename to Sources/HaystackServer/ServerVapor/HaystackRouteCollection.swift index 4f2498e..9024cc1 100644 --- a/Sources/HaystackServerVapor/HaystackRouteCollection.swift +++ b/Sources/HaystackServer/ServerVapor/HaystackRouteCollection.swift @@ -1,21 +1,17 @@ +#if ServerVapor import Haystack import Vapor /// A route collection that exposes Haystack API endpoints. public struct HaystackRouteCollection: RouteCollection { - /// This instance defines all Haystack API processing that is done server-side. - let delegate: any API - - public init(delegate: any API) { - self.delegate = delegate - } + public init() {} public func boot(routes: any Vapor.RoutesBuilder) throws { /// Closes the current authentication session. /// /// https://project-haystack.org/doc/docHaystack/Ops#close - routes.post("close") { _ in - try await delegate.close() + routes.post("close") { request in + try await request.haystack().close() return "" } @@ -23,14 +19,14 @@ public struct HaystackRouteCollection: RouteCollection { /// /// https://project-haystack.org/doc/docHaystack/Ops#about routes.get("about") { request in - try await request.respond(with: delegate.about()) + try await request.respond(with: request.haystack().about()) } /// Queries basic information about the server /// /// https://project-haystack.org/doc/docHaystack/Ops#about routes.post("about") { request in - try await request.respond(with: delegate.about()) + try await request.respond(with: request.haystack().about()) } /// Queries def dicts from the current namespace @@ -48,7 +44,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.defs(filter: filter, limit: limit) + with: request.haystack().defs(filter: filter, limit: limit) ) } @@ -67,7 +63,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.defs(filter: filter, limit: limit) + with: request.haystack().defs(filter: filter, limit: limit) ) } @@ -86,7 +82,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.libs(filter: filter, limit: limit) + with: request.haystack().libs(filter: filter, limit: limit) ) } @@ -105,7 +101,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.libs(filter: filter, limit: limit) + with: request.haystack().libs(filter: filter, limit: limit) ) } @@ -124,7 +120,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.libs(filter: filter, limit: limit) + with: request.haystack().libs(filter: filter, limit: limit) ) } @@ -143,7 +139,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.ops(filter: filter, limit: limit) + with: request.haystack().ops(filter: filter, limit: limit) ) } @@ -162,7 +158,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.filetypes(filter: filter, limit: limit) + with: request.haystack().filetypes(filter: filter, limit: limit) ) } @@ -181,7 +177,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.filetypes(filter: filter, limit: limit) + with: request.haystack().filetypes(filter: filter, limit: limit) ) } @@ -208,10 +204,10 @@ public struct HaystackRouteCollection: RouteCollection { } if let ids = ids { - return try await request.respond(with: delegate.read(ids: ids)) + return try await request.respond(with: request.haystack().read(ids: ids)) } else if let filter = filter { return try await request.respond( - with: delegate.read(filter: filter, limit: limit) + with: request.haystack().read(filter: filter, limit: limit) ) } else { throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'") @@ -241,10 +237,10 @@ public struct HaystackRouteCollection: RouteCollection { } if let ids = ids { - return try await request.respond(with: delegate.read(ids: ids)) + return try await request.respond(with: request.haystack().read(ids: ids)) } else if let filter = filter { return try await request.respond( - with: delegate.read(filter: filter, limit: limit) + with: request.haystack().read(filter: filter, limit: limit) ) } else { throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'") @@ -264,7 +260,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.nav(navId: navId) + with: request.haystack().nav(navId: navId) ) } @@ -281,7 +277,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.nav(navId: navId) + with: request.haystack().nav(navId: navId) ) } @@ -300,7 +296,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.hisRead( + with: request.haystack().hisRead( id: id, range: range ) @@ -325,7 +321,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.hisRead( + with: request.haystack().hisRead( id: id, range: range ) @@ -351,7 +347,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.hisWrite(id: id, items: items) + with: request.haystack().hisWrite(id: id, items: items) ) } @@ -375,7 +371,7 @@ public struct HaystackRouteCollection: RouteCollection { } guard let level = level else { return try await request.respond( - with: delegate.pointWriteStatus(id: id) + with: request.haystack().pointWriteStatus(id: id) ) } @@ -391,7 +387,7 @@ public struct HaystackRouteCollection: RouteCollection { throw Abort(.badRequest, reason: error.localizedDescription) } return try await request.respond( - with: delegate.pointWrite( + with: request.haystack().pointWrite( id: id, level: level, val: val, @@ -424,7 +420,7 @@ public struct HaystackRouteCollection: RouteCollection { if let watchDis = watchDis { return try await request.respond( - with: delegate.watchSubCreate( + with: request.haystack().watchSubCreate( watchDis: watchDis, lease: lease, ids: ids @@ -434,7 +430,7 @@ public struct HaystackRouteCollection: RouteCollection { if let watchId = watchId { return try await request.respond( - with: delegate.watchSubAdd( + with: request.haystack().watchSubAdd( watchId: watchId, lease: lease, ids: ids @@ -460,7 +456,7 @@ public struct HaystackRouteCollection: RouteCollection { if grid.meta.has("close") { return try await request.respond( - with: delegate.watchUnsubDelete( + with: request.haystack().watchUnsubDelete( watchId: watchId ) ) @@ -475,7 +471,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.watchUnsubRemove( + with: request.haystack().watchUnsubRemove( watchId: watchId, ids: ids ) @@ -498,7 +494,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.watchPoll( + with: request.haystack().watchPoll( watchId: watchId, refresh: refresh ) @@ -524,7 +520,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.invokeAction( + with: request.haystack().invokeAction( id: id, action: action, args: args @@ -547,7 +543,8 @@ public struct HaystackRouteCollection: RouteCollection { throw Abort(.badRequest, reason: error.localizedDescription) } - return try await request.respond(with: delegate.eval(expression: expr)) + return try await request.respond(with: request.haystack().eval(expression: expr)) } } } +#endif diff --git a/Sources/HaystackServerVapor/Request+Grid.swift b/Sources/HaystackServer/ServerVapor/Request+Grid.swift similarity index 98% rename from Sources/HaystackServerVapor/Request+Grid.swift rename to Sources/HaystackServer/ServerVapor/Request+Grid.swift index 94ca1e2..30ace28 100644 --- a/Sources/HaystackServerVapor/Request+Grid.swift +++ b/Sources/HaystackServer/ServerVapor/Request+Grid.swift @@ -1,3 +1,4 @@ +#if ServerVapor import Haystack import Vapor @@ -44,3 +45,4 @@ extension Request { return Dict(dictMap) } } +#endif diff --git a/Sources/HaystackServer/Stores/HistoryStore.swift b/Sources/HaystackServer/Stores/HistoryStore.swift index 2f2fea4..7dbb47a 100644 --- a/Sources/HaystackServer/Stores/HistoryStore.swift +++ b/Sources/HaystackServer/Stores/HistoryStore.swift @@ -1,7 +1,7 @@ import Haystack /// Defines a storage system that allows reading and writing of Haystack history data. -public protocol HistoryStore { +public protocol HistoryStore: Sendable { /// Reads history data for a given ID and time range. func hisRead(id: Haystack.Ref, range: Haystack.HisReadRange) async throws -> [Haystack.Dict] diff --git a/Sources/HaystackServer/Stores/RecordStore.swift b/Sources/HaystackServer/Stores/RecordStore.swift index fad76d4..24d63c3 100644 --- a/Sources/HaystackServer/Stores/RecordStore.swift +++ b/Sources/HaystackServer/Stores/RecordStore.swift @@ -1,7 +1,7 @@ import Haystack /// Defines a storage system that allows reading and writing of Haystack records. -public protocol RecordStore { +public protocol RecordStore: Sendable { /// Reads records from the store based on a list of IDs. func read(ids: [Haystack.Ref]) async throws -> [Haystack.Dict] @@ -12,7 +12,7 @@ public protocol RecordStore { func commitAll(diffs: [RecordDiff]) async throws -> [RecordDiff] } -public struct RecordDiff { +public struct RecordDiff: Sendable { public init( id: Haystack.Ref, old: Haystack.Dict?, diff --git a/Sources/HaystackServer/Stores/WatchStore.swift b/Sources/HaystackServer/Stores/WatchStore.swift index 08f4df3..753eab5 100644 --- a/Sources/HaystackServer/Stores/WatchStore.swift +++ b/Sources/HaystackServer/Stores/WatchStore.swift @@ -2,7 +2,7 @@ import Foundation import Haystack /// Defines a storage system that allows stateful storage of system watches. -public protocol WatchStore { +public protocol WatchStore: Sendable { func read(watchId: String) async throws -> WatchResponse func create(ids: [Haystack.Ref], lease: Haystack.Number?) async throws -> String func addIds(watchId: String, ids: [Haystack.Ref]) async throws @@ -11,7 +11,7 @@ public protocol WatchStore { func delete(watchId: String) async throws } -public struct WatchResponse { +public struct WatchResponse: Sendable { public let ids: [Haystack.Ref] public let lease: Haystack.Number public let lastReported: Foundation.Date? diff --git a/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift b/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift deleted file mode 100644 index d1b96d8..0000000 --- a/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -import AsyncHTTPClient -import Haystack -import HaystackClientNIO -import NIO -import XCTest - -/// To use these tests, run a [Haxall](https://github.com/haxall/haxall) server and set the username and password -/// in the `HAYSTACK_USER` and `HAYSTACK_PASSWORD` environment variables -final class HaystackClientNIOIntegrationTests: XCTestCase { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - var httpClient: HTTPClient! - var client: Client! - - override func setUp() async throws { - httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) - client = try Client( - baseUrl: "http://localhost:8080/api/", - username: ProcessInfo.processInfo.environment["HAYSTACK_USER"] ?? "su", - password: ProcessInfo.processInfo.environment["HAYSTACK_PASSWORD"] ?? "su", - httpClient: httpClient - ) - try await client.open() - } - - override func tearDown() async throws { - try await client.close() - try httpClient.syncShutdown() - } - - func testCloseAndOpen() async throws { - try print(await client.close()) - try print(await client.open()) - } - - func testAbout() async throws { - try print(await client.about().toZinc()) - } - - func testDefs() async throws { - try print(await client.defs().toZinc()) - try print(await client.defs(filter: "lib==^lib:phIoT").toZinc()) - try print(await client.defs(limit: Number(1)).toZinc()) - try print(await client.defs(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) - } - - func testLibs() async throws { - try print(await client.libs().toZinc()) - try print(await client.libs(filter: "lib==^lib:phIoT").toZinc()) - try print(await client.libs(limit: Number(1)).toZinc()) - try print(await client.libs(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) - } - - func testOps() async throws { - try print(await client.ops().toZinc()) - try print(await client.ops(filter: "lib==^lib:phIoT").toZinc()) - try print(await client.ops(limit: Number(1)).toZinc()) - try print(await client.ops(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) - } - - func testFiletypes() async throws { - try print(await client.filetypes().toZinc()) - try print(await client.filetypes(filter: "lib==^lib:phIoT").toZinc()) - try print(await client.filetypes(limit: Number(1)).toZinc()) - try print(await client.filetypes(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) - } - - func testRead() async throws { - try print(await client.read(ids: [Ref("28e7fb47-d67ab19a")]).toZinc()) - } - - func testReadAll() async throws { - try print(await client.read(filter: "site").toZinc()) - } - - func testHisRead() async throws { - try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .today).toZinc()) - try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .yesterday).toZinc()) - try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .date(Date("2022-01-01"))).toZinc()) - try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .dateRange(from: Date("2022-01-01"), to: Date("2022-02-01"))).toZinc()) - try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .dateTimeRange(from: DateTime("2022-01-01T00:00:00Z"), to: DateTime("2022-02-01T00:00:00Z"))).toZinc()) - try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .after(DateTime("2022-01-01T00:00:00Z"))).toZinc()) - } - - func testHisWrite() async throws { - try print(await client.hisWrite( - id: Ref("28e7fb7d-e20316e0"), - items: [ - HisItem(ts: DateTime("2022-01-01T00:00:00-07:00 Denver"), val: Number(14)), - ] - ).toZinc()) - } - - func testEval() async throws { - try print(await client.eval(expression: "readAll(site)").toZinc()) - } - - func testWatchUnsub() async throws { - try print(await client.watchUnsubRemove(watchId: "id", ids: [Ref("28e7fb47-d67ab19a")])) - } -} diff --git a/Tests/HaystackClientDarwinIntegrationTests/HaystackClientDarwinIntegrationTests.swift b/Tests/HaystackClientTests/HaystackClientDarwinIntegrationTests.swift similarity index 81% rename from Tests/HaystackClientDarwinIntegrationTests/HaystackClientDarwinIntegrationTests.swift rename to Tests/HaystackClientTests/HaystackClientDarwinIntegrationTests.swift index 9959056..d259795 100644 --- a/Tests/HaystackClientDarwinIntegrationTests/HaystackClientDarwinIntegrationTests.swift +++ b/Tests/HaystackClientTests/HaystackClientDarwinIntegrationTests.swift @@ -1,70 +1,64 @@ +#if ClientDarwin +import Foundation import Haystack -import HaystackClientDarwin -import XCTest +import HaystackClient +import Testing /// To use these tests, run a [Haxall](https://github.com/haxall/haxall) server and set the username and password /// in the `HAYSTACK_USER` and `HAYSTACK_PASSWORD` environment variables -final class HaystackClientDarwinIntegrationTests: XCTestCase { +struct HaystackClientDarwinIntegration { var client: Client = try! Client( baseUrl: "http://localhost:8080/api/", username: ProcessInfo.processInfo.environment["HAYSTACK_USER"] ?? "su", password: ProcessInfo.processInfo.environment["HAYSTACK_PASSWORD"] ?? "su" ) - override func setUp() async throws { - try await client.open() - } - - override func tearDown() async throws { - try await client.close() - } - - func testCloseAndOpen() async throws { + @Test func closeAndOpen() async throws { try print(await client.close()) try print(await client.open()) } - func testAbout() async throws { + @Test func about() async throws { try print(await client.about().toZinc()) } - func testDefs() async throws { + @Test func defs() async throws { try print(await client.defs().toZinc()) try print(await client.defs(filter: "lib==^lib:phIoT").toZinc()) try print(await client.defs(limit: Number(1)).toZinc()) try print(await client.defs(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) } - func testLibs() async throws { + @Test func libs() async throws { try print(await client.libs().toZinc()) try print(await client.libs(filter: "lib==^lib:phIoT").toZinc()) try print(await client.libs(limit: Number(1)).toZinc()) try print(await client.libs(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) } - func testOps() async throws { + @Test func ops() async throws { try print(await client.ops().toZinc()) try print(await client.ops(filter: "lib==^lib:phIoT").toZinc()) try print(await client.ops(limit: Number(1)).toZinc()) try print(await client.ops(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) } - func testFiletypes() async throws { + @Test func filetypes() async throws { try print(await client.filetypes().toZinc()) try print(await client.filetypes(filter: "lib==^lib:phIoT").toZinc()) try print(await client.filetypes(limit: Number(1)).toZinc()) try print(await client.filetypes(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) } - func testRead() async throws { + @Test func read() async throws { try print(await client.read(ids: [Ref("28e7fb47-d67ab19a")]).toZinc()) } - func testReadAll() async throws { + @Test func readAll() async throws { try print(await client.read(filter: "site").toZinc()) } - func testHisRead() async throws { + @Test func hisRead() async throws { try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .today).toZinc()) try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .yesterday).toZinc()) try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .date(Date("2022-01-01"))).toZinc()) @@ -73,7 +67,7 @@ final class HaystackClientDarwinIntegrationTests: XCTestCase { try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .after(DateTime("2022-01-01T00:00:00Z"))).toZinc()) } - func testHisWrite() async throws { + @Test func hisWrite() async throws { try print(await client.hisWrite( id: Ref("28e7fb7d-e20316e0"), items: [ @@ -82,11 +76,12 @@ final class HaystackClientDarwinIntegrationTests: XCTestCase { ).toZinc()) } - func testEval() async throws { + @Test func eval() async throws { try print(await client.eval(expression: "readAll(site)").toZinc()) } - func testWatchUnsub() async throws { + @Test func watchUnsub() async throws { try print(await client.watchUnsubRemove(watchId: "id", ids: [Ref("28e7fb47-d67ab19a")])) } } +#endif diff --git a/Tests/HaystackClientTests/HaystackClientNIOIntegrationTests.swift b/Tests/HaystackClientTests/HaystackClientNIOIntegrationTests.swift new file mode 100644 index 0000000..38ca745 --- /dev/null +++ b/Tests/HaystackClientTests/HaystackClientNIOIntegrationTests.swift @@ -0,0 +1,124 @@ +#if ClientNIO +import AsyncHTTPClient +import Foundation +import Haystack +import HaystackClient +import NIO +import Testing + +/// To use these tests, run a [Haxall](https://github.com/haxall/haxall) server and set the username and password +/// in the `HAYSTACK_USER` and `HAYSTACK_PASSWORD` environment variables +struct HaystackClientNIOIntegrationTests { + private func withClient(_ block: (Client) async throws -> Void) async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) + let client = try Client( + baseUrl: "http://localhost:8080/api/", + username: ProcessInfo.processInfo.environment["HAYSTACK_USER"] ?? "su", + password: ProcessInfo.processInfo.environment["HAYSTACK_PASSWORD"] ?? "su", + httpClient: httpClient + ) + try await client.open() + + try await block(client) + + try await client.close() + try await httpClient.shutdown() + } + + @Test func closeAndOpen() async throws { + try await withClient { client in + try print(await client.close()) + try print(await client.open()) + } + } + + @Test func about() async throws { + try await withClient { client in + try print(await client.about().toZinc()) + } + } + + @Test func defs() async throws { + try await withClient { client in + try print(await client.defs().toZinc()) + try print(await client.defs(filter: "lib==^lib:phIoT").toZinc()) + try print(await client.defs(limit: Number(1)).toZinc()) + try print(await client.defs(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) + } + } + + @Test func libs() async throws { + try await withClient { client in + try print(await client.libs().toZinc()) + try print(await client.libs(filter: "lib==^lib:phIoT").toZinc()) + try print(await client.libs(limit: Number(1)).toZinc()) + try print(await client.libs(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) + } + } + + @Test func ops() async throws { + try await withClient { client in + try print(await client.ops().toZinc()) + try print(await client.ops(filter: "lib==^lib:phIoT").toZinc()) + try print(await client.ops(limit: Number(1)).toZinc()) + try print(await client.ops(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) + } + } + + @Test func filetypes() async throws { + try await withClient { client in + try print(await client.filetypes().toZinc()) + try print(await client.filetypes(filter: "lib==^lib:phIoT").toZinc()) + try print(await client.filetypes(limit: Number(1)).toZinc()) + try print(await client.filetypes(filter: "lib==^lib:phIoT", limit: Number(1)).toZinc()) + } + } + + @Test func read() async throws { + try await withClient { client in + try print(await client.read(ids: [Ref("28e7fb47-d67ab19a")]).toZinc()) + } + } + + @Test func readAll() async throws { + try await withClient { client in + try print(await client.read(filter: "site").toZinc()) + } + } + + @Test func hisRead() async throws { + try await withClient { client in + try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .today).toZinc()) + try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .yesterday).toZinc()) + try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .date(Date("2022-01-01"))).toZinc()) + try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .dateRange(from: Date("2022-01-01"), to: Date("2022-02-01"))).toZinc()) + try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .dateTimeRange(from: DateTime("2022-01-01T00:00:00Z"), to: DateTime("2022-02-01T00:00:00Z"))).toZinc()) + try print(await client.hisRead(id: Ref("28e7fb7d-e20316e0"), range: .after(DateTime("2022-01-01T00:00:00Z"))).toZinc()) + } + } + + @Test func hisWrite() async throws { + try await withClient { client in + try print(await client.hisWrite( + id: Ref("28e7fb7d-e20316e0"), + items: [ + HisItem(ts: DateTime("2022-01-01T00:00:00-07:00 Denver"), val: Number(14)), + ] + ).toZinc()) + } + } + + @Test func eval() async throws { + try await withClient { client in + try print(await client.eval(expression: "readAll(site)").toZinc()) + } + } + + @Test func watchUnsub() async throws { + try await withClient { client in + try print(await client.watchUnsubRemove(watchId: "id", ids: [Ref("28e7fb47-d67ab19a")])) + } + } +} +#endif diff --git a/Tests/HaystackClientTests/SCRAMTests.swift b/Tests/HaystackClientTests/SCRAMTests.swift index 4c52cf5..f2afddd 100644 --- a/Tests/HaystackClientTests/SCRAMTests.swift +++ b/Tests/HaystackClientTests/SCRAMTests.swift @@ -1,8 +1,8 @@ import Crypto @testable import HaystackClient -import XCTest +import Testing -final class SCRAMTests: XCTestCase { +struct SCRAMTests { let scram = ScramClient( hash: Insecure.SHA1.self, username: "user", @@ -11,92 +11,81 @@ final class SCRAMTests: XCTestCase { ) /// Tests example from [RFC-5802's SCRAM Authentication Exchange section](https://www.rfc-editor.org/rfc/rfc5802#section-5) - func testClientExample() async throws { - XCTAssertEqual( - scram.clientFirstMessage(), - "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" - ) + @Test func clientExample() async throws { + #expect(scram.clientFirstMessage() == "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL") - XCTAssertEqual( + #expect( try scram.clientFinalMessage( serverFirstMessage: "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096" - ), - "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" + ) == "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" ) - XCTAssertNoThrow( + #expect(throws: Never.self) { try scram.validate(serverFinalMessage: "v=rmF9pqV8S7suAoZWja4dJRkFsKQ=") - ) + } } - func testServerFirstMessageResponseErrors() async throws { - XCTAssertEqual( - scram.clientFirstMessage(), - "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" - ) + @Test func serverFirstMessageResponseErrors() async throws { + #expect(scram.clientFirstMessage() == "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL") // Server first message has no nonce attribute - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.clientFinalMessage( serverFirstMessage: "s=QSXCR+Q6sek8bf92,i=4096" ) - ) + } // Server first message has no salt attribute - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.clientFinalMessage( serverFirstMessage: "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,i=4096" ) - ) + } // Server first message has no iteration attribute - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.clientFinalMessage( serverFirstMessage: "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92" ) - ) + } // Server first message iteration is not integer - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.clientFinalMessage( serverFirstMessage: "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=BAD" ) - ) + } // Server nonce isn't prefixed with client nonce - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.clientFinalMessage( serverFirstMessage: "r=BAD_fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096" ) - ) + } } - func testServerFinalMessageValidateErrors() async throws { - XCTAssertEqual( - scram.clientFirstMessage(), - "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" - ) + @Test func serverFinalMessageValidateErrors() async throws { + #expect(scram.clientFirstMessage() == "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL") - XCTAssertEqual( + #expect( try scram.clientFinalMessage( serverFirstMessage: "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096" - ), - "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" + ) == "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" ) // Throws if server final message is unexpected - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.validate(serverFinalMessage: "v=BAD_rmF9pqV8S7suAoZWja4dJRkFsKQ=") - ) + } // Throws if server final message has error attribute - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.validate(serverFinalMessage: "e=No way Jose,v=rmF9pqV8S7suAoZWja4dJRkFsKQ=") - ) + } // Throws if server final message has no server key attribute - XCTAssertThrowsError( + #expect(throws: (any Error).self) { try scram.validate(serverFinalMessage: "n=user") - ) + } } } diff --git a/Tests/HaystackClientTests/UrlSafeBase64Tests.swift b/Tests/HaystackClientTests/UrlSafeBase64Tests.swift index 6a6e293..e6b6c60 100644 --- a/Tests/HaystackClientTests/UrlSafeBase64Tests.swift +++ b/Tests/HaystackClientTests/UrlSafeBase64Tests.swift @@ -1,32 +1,20 @@ @testable import HaystackClient -import XCTest +import Testing -final class UrlSafeBase64Tests: XCTestCase { - func testEncodeStandard() throws { - XCTAssertEqual( - "user".encodeBase64Standard(), - "dXNlcg==" - ) +struct UrlSafeBase64Tests { + @Test func encodeStandard() throws { + #expect("user".encodeBase64Standard() == "dXNlcg==") } - func testEncodeUrlSafe() throws { - XCTAssertEqual( - "user".encodeBase64UrlSafe(), - "dXNlcg" - ) + @Test func encodeUrlSafe() throws { + #expect("user".encodeBase64UrlSafe() == "dXNlcg") } - func testDecodeStandard() throws { - XCTAssertEqual( - "dXNlcg==".decodeBase64Standard(), - "user" - ) + @Test func decodeStandard() throws { + #expect("dXNlcg==".decodeBase64Standard() == "user") } - func testDecodeUrlSafe() throws { - XCTAssertEqual( - "dXNlcg".decodeBase64UrlSafe(), - "user" - ) + @Test func decodeUrlSafe() throws { + #expect("dXNlcg".decodeBase64UrlSafe() == "user") } } diff --git a/Tests/HaystackServerTests/HaystackServerTests.swift b/Tests/HaystackServerTests/HaystackServerTests.swift index f207ee9..e3d0059 100644 --- a/Tests/HaystackServerTests/HaystackServerTests.swift +++ b/Tests/HaystackServerTests/HaystackServerTests.swift @@ -1,29 +1,29 @@ import Foundation import Haystack import HaystackServer -import XCTest +import Testing -final class HaystackServerTests: XCTestCase { - func testAbout() async throws { +struct HaystackServerTests { + @Test func about() async throws { let server = HaystackServer( recordStore: InMemoryRecordStore(), historyStore: InMemoryHistoryStore(), watchStore: InMemoryWatchStore() ) let response = try await server.about() - let about = try XCTUnwrap(response.first) - XCTAssertNotNil(about["haystackVersion"] as? String) - XCTAssertNotNil(about["tz"] as? String) - XCTAssertNotNil(about["serverTime"] as? DateTime) - XCTAssertNotNil(about["serverBootTime"] as? DateTime) - XCTAssertEqual(about["productName"] as? String, "swift-haystack") - XCTAssertEqual(about["productUri"] as? Uri, Uri("https://github.com/NeedleInAJayStack/swift-haystack")) - XCTAssertNotNil(about["productVersion"] as? String) - XCTAssertEqual(about["vendorName"] as? String, "NeedleInAJayStack") - XCTAssertEqual(about["vendorUri"] as? Uri, Uri("https://github.com/NeedleInAJayStack")) + let about = try #require(response.first) + #expect(about["haystackVersion"] as? String != nil) + #expect(about["tz"] as? String != nil) + #expect(about["serverTime"] as? DateTime != nil) + #expect(about["serverBootTime"] as? DateTime != nil) + #expect(about["productName"] as? String == "swift-haystack") + #expect(about["productUri"] as? Uri == Uri("https://github.com/NeedleInAJayStack/swift-haystack")) + #expect(about["productVersion"] as? String != nil) + #expect(about["vendorName"] as? String == "NeedleInAJayStack") + #expect(about["vendorUri"] as? Uri == Uri("https://github.com/NeedleInAJayStack")) } - func testDefs() async throws { + @Test func defs() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("a"): ["id": Ref("a"), "def": Marker.val], @@ -36,31 +36,22 @@ final class HaystackServerTests: XCTestCase { // Test no filter var grid = try await server.defs(filter: nil, limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["a", "b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["a", "b"]) // Test filter grid = try await server.defs(filter: "foo", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - ["b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == ["b"]) // Test bad filter grid = try await server.defs(filter: "none", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - [] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == []) // Test limit grid = try await server.defs(filter: nil, limit: Number(0)) - XCTAssertEqual(grid.count, 0) + #expect(grid.count == 0) } - func testLibs() async throws { + @Test func libs() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("a"): ["id": Ref("a"), "lib": Marker.val], @@ -73,31 +64,22 @@ final class HaystackServerTests: XCTestCase { // Test no filter var grid = try await server.libs(filter: nil, limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["a", "b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["a", "b"]) // Test filter grid = try await server.libs(filter: "foo", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - ["b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == ["b"]) // Test bad filter grid = try await server.libs(filter: "none", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - [] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == []) // Test limit grid = try await server.libs(filter: nil, limit: Number(0)) - XCTAssertEqual(grid.count, 0) + #expect(grid.count == 0) } - func testOps() async throws { + @Test func ops() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("a"): ["id": Ref("a"), "def": Marker.val, "op": Marker.val], @@ -110,31 +92,22 @@ final class HaystackServerTests: XCTestCase { // Test no filter var grid = try await server.ops(filter: nil, limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["a", "b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["a", "b"]) // Test filter grid = try await server.ops(filter: "foo", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - ["b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == ["b"]) // Test bad filter grid = try await server.ops(filter: "none", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - [] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == []) // Test limit grid = try await server.ops(filter: nil, limit: Number(0)) - XCTAssertEqual(grid.count, 0) + #expect(grid.count == 0) } - func testFiletypes() async throws { + @Test func filetypes() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("a"): ["id": Ref("a"), "def": Marker.val, "filetype": Marker.val], @@ -147,31 +120,22 @@ final class HaystackServerTests: XCTestCase { // Test no filter var grid = try await server.filetypes(filter: nil, limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["a", "b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["a", "b"]) // Test filter grid = try await server.filetypes(filter: "foo", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - ["b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == ["b"]) // Test bad filter grid = try await server.filetypes(filter: "none", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }, - [] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val } == []) // Test limit grid = try await server.filetypes(filter: nil, limit: Number(0)) - XCTAssertEqual(grid.count, 0) + #expect(grid.count == 0) } - func testReadIds() async throws { + @Test func readIds() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("a"): ["id": Ref("a"), "def": Marker.val, "filetype": Marker.val], @@ -183,13 +147,10 @@ final class HaystackServerTests: XCTestCase { ) let grid = try await server.read(ids: [Ref("a"), Ref("b")]) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["a", "b"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["a", "b"]) } - func testReadFilter() async throws { + @Test func readFilter() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("ahu"): ["id": Ref("ahu"), "equip": Marker.val, "ahu": Marker.val], @@ -203,45 +164,30 @@ final class HaystackServerTests: XCTestCase { // Test normal var grid = try await server.read(filter: "equip", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["ahu", "vav"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["ahu", "vav"]) // Test limit grid = try await server.read(filter: "equip", limit: Number(1)) - XCTAssertEqual(grid.count, 1) + #expect(grid.count == 1) // Test and grid = try await server.read(filter: "equip and ahu", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["ahu"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["ahu"]) // Test or grid = try await server.read(filter: "ahu or vav", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["ahu", "vav"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["ahu", "vav"]) // Test path grid = try await server.read(filter: "point and equipRef->ahu", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["supply-air-temp"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["supply-air-temp"]) // Test ref equality grid = try await server.read(filter: "equipRef == @ahu", limit: nil) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["supply-air-temp"] - ) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["supply-air-temp"]) } - func testNav() async throws { + @Test func nav() async throws { let server = try HaystackServer( recordStore: InMemoryRecordStore([ Ref("site"): ["id": Ref("site"), "site": Marker.val], @@ -254,20 +200,14 @@ final class HaystackServerTests: XCTestCase { var grid = try await server.nav(navId: Ref("site")) // TODO: Implement nav -// XCTAssertEqual( -// grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), -// ["equip"] -// ) +// #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["equip"]) grid = try await server.nav(navId: Ref("equip")) // TODO: Implement nav -// XCTAssertEqual( -// grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), -// ["point"] -// ) +// #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["point"]) } - func testHisRead() async throws { + @Test func hisRead() async throws { // Test absolute time ranges let absoluteServer = try HaystackServer( @@ -287,9 +227,8 @@ final class HaystackServerTests: XCTestCase { // After var grid = try await absoluteServer.hisRead(id: Ref("point"), range: .after(DateTime("2025-05-09T17:00:00-07:00"))) - try XCTAssertEqual( - grid.rows, - [ + #expect( + try grid.rows == [ ["ts": DateTime("2025-05-10T00:00:00-07:00"), "val": Number(2)], ["ts": DateTime("2025-05-10T12:00:00-07:00"), "val": Number(3)], ["ts": DateTime("2025-05-11T00:00:00-07:00"), "val": Number(4)], @@ -298,9 +237,8 @@ final class HaystackServerTests: XCTestCase { // Date grid = try await absoluteServer.hisRead(id: Ref("point"), range: .date(Date("2025-05-10"))) - try XCTAssertEqual( - grid.rows, - [ + #expect( + try grid.rows == [ ["ts": DateTime("2025-05-10T00:00:00-07:00"), "val": Number(2)], ["ts": DateTime("2025-05-10T12:00:00-07:00"), "val": Number(3)], ] @@ -308,9 +246,8 @@ final class HaystackServerTests: XCTestCase { // Date Range grid = try await absoluteServer.hisRead(id: Ref("point"), range: .dateRange(from: Date("2025-05-10"), to: Date("2025-05-11"))) - try XCTAssertEqual( - grid.rows, - [ + #expect( + try grid.rows == [ ["ts": DateTime("2025-05-10T00:00:00-07:00"), "val": Number(2)], ["ts": DateTime("2025-05-10T12:00:00-07:00"), "val": Number(3)], ["ts": DateTime("2025-05-11T00:00:00-07:00"), "val": Number(4)], @@ -319,9 +256,8 @@ final class HaystackServerTests: XCTestCase { // DateTime Range grid = try await absoluteServer.hisRead(id: Ref("point"), range: .dateTimeRange(from: DateTime("2025-05-10T00:00:00-07:00"), to: DateTime("2025-05-11T00:00:00-07:00"))) - try XCTAssertEqual( - grid.rows, - [ + #expect( + try grid.rows == [ ["ts": DateTime("2025-05-10T00:00:00-07:00"), "val": Number(2)], ["ts": DateTime("2025-05-10T12:00:00-07:00"), "val": Number(3)], ] @@ -350,32 +286,30 @@ final class HaystackServerTests: XCTestCase { ) grid = try await relativeServer.hisRead(id: Ref("point"), range: .today) - XCTAssertEqual( - grid.rows, - [ + #expect( + grid.rows == [ ["ts": DateTime(date: now), "val": Number(3)], ] ) grid = try await relativeServer.hisRead(id: Ref("point"), range: .yesterday) - XCTAssertEqual( - grid.rows, - [ + #expect( + grid.rows == [ ["ts": DateTime(date: yesterday), "val": Number(2)], ] ) } - func testPointWrite() async throws { + @Test func pointWrite() async throws { // TODO: Implement } - func testPointWriteStatus() async throws { + @Test func pointWriteStatus() async throws { // TODO: Implement } // Test all watch methods together to avoid state complexities - func testWatch() async throws { + @Test func watch() async throws { let idA = try Ref("a") let idB = try Ref("b") let idC = try Ref("c") @@ -395,12 +329,9 @@ final class HaystackServerTests: XCTestCase { // Create the watch and validate A and B are returned var grid = try await server.watchSubCreate(watchDis: "ab", lease: nil, ids: [idA, idB]) - let watchId = try XCTUnwrap(grid.meta["watchId"] as? String) - XCTAssertEqual(grid.meta, ["watchId": watchId, "lease": null]) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["a", "b"] - ) + let watchId = try #require(grid.meta["watchId"] as? String) + #expect(grid.meta == ["watchId": watchId, "lease": null]) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["a", "b"]) // Change B to increment "mod" var newB = recB @@ -411,18 +342,15 @@ final class HaystackServerTests: XCTestCase { // Check that subsequent poll picks up B grid = try await server.watchPoll(watchId: watchId, refresh: false) - XCTAssertEqual(grid.meta, ["watchId": watchId]) - XCTAssertEqual(grid.count, 1) - XCTAssertEqual(grid.first?["id"] as? Ref, idB) - XCTAssertNotNil(grid.first?["new"]) + #expect(grid.meta == ["watchId": watchId]) + #expect(grid.count == 1) + #expect(grid.first?["id"] as? Ref == idB) + #expect(grid.first?["new"] != nil) // Add C to the watch and validate C is returned grid = try await server.watchSubAdd(watchId: watchId, lease: nil, ids: [idC]) - XCTAssertEqual(grid.meta, ["watchId": watchId, "lease": null]) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["c"] - ) + #expect(grid.meta == ["watchId": watchId, "lease": null]) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["c"]) // Change C to increment "mod" var newC = recC @@ -433,14 +361,14 @@ final class HaystackServerTests: XCTestCase { // Validate poll picks up C grid = try await server.watchPoll(watchId: watchId, refresh: false) - XCTAssertEqual(grid.meta, ["watchId": watchId]) - XCTAssertEqual(grid.count, 1) - XCTAssertEqual(grid.first?["id"] as? Ref, idC) - XCTAssertNotNil(grid.first?["new"]) + #expect(grid.meta == ["watchId": watchId]) + #expect(grid.count == 1) + #expect(grid.first?["id"] as? Ref == idC) + #expect(grid.first?["new"] != nil) // Remove A from watch grid = try await server.watchUnsubRemove(watchId: watchId, ids: [Ref("a")]) - XCTAssertTrue(grid.isEmpty) + #expect(grid.isEmpty) // Change A to increment "mod" var newA = recA @@ -451,19 +379,16 @@ final class HaystackServerTests: XCTestCase { // Validate poll does not pick up A grid = try await server.watchPoll(watchId: watchId, refresh: false) - XCTAssertEqual(grid.meta, ["watchId": watchId]) - XCTAssertEqual(grid.count, 0) + #expect(grid.meta == ["watchId": watchId]) + #expect(grid.count == 0) // Test that poll with refresh gives B and C grid = try await server.watchPoll(watchId: watchId, refresh: true) - XCTAssertEqual(grid.meta, ["watchId": watchId]) - XCTAssertEqual( - grid.compactMap { ($0["id"] as? Ref)?.val }.sorted(), - ["b", "c"] - ) + #expect(grid.meta == ["watchId": watchId]) + #expect(grid.compactMap { ($0["id"] as? Ref)?.val }.sorted() == ["b", "c"]) } - func testInvokeAction() async throws { + @Test func invokeAction() async throws { let server = HaystackServer( recordStore: InMemoryRecordStore(), historyStore: InMemoryHistoryStore(), @@ -475,12 +400,12 @@ final class HaystackServerTests: XCTestCase { } ) let grid = try await server.invokeAction(id: Ref("1"), action: "a", args: ["foo": "bar"]) - try XCTAssertEqual(grid[0]["id"] as? Ref, Ref("1")) - XCTAssertEqual(grid[0]["action"] as? String, "a") - XCTAssertEqual(grid[0]["args"] as? Dict, Dict(["foo": "bar"])) + #expect(try grid[0]["id"] as? Ref == Ref("1")) + #expect(grid[0]["action"] as? String == "a") + #expect(grid[0]["args"] as? Dict == Dict(["foo": "bar"])) } - func testEval() async throws { + @Test func eval() async throws { let server = HaystackServer( recordStore: InMemoryRecordStore(), historyStore: InMemoryHistoryStore(), @@ -492,19 +417,14 @@ final class HaystackServerTests: XCTestCase { } ) let grid = try await server.eval(expression: "anything") - XCTAssertEqual( - grid, - [ - ["foo": "bar"], - ] - ) + #expect(grid == [["foo": "bar"]]) } } /// This is a super inefficient record store based on an in-memory list of Diffs /// /// Any change automatically updates the `mod` DateTime tag on the record -class InMemoryRecordStore: RecordStore { +actor InMemoryRecordStore: RecordStore { var recs: [Haystack.Ref: Haystack.Dict] = [:] init() {} @@ -564,7 +484,7 @@ class InMemoryRecordStore: RecordStore { } /// This is a super inefficient history store based on an in-memory list of histories -class InMemoryHistoryStore: HistoryStore { +actor InMemoryHistoryStore: HistoryStore { /// Maps Refs to histories. These histories are not assumed to be ordered in time (for simplicity). var histories: [Ref: [HisItem]] @@ -604,7 +524,7 @@ class InMemoryHistoryStore: HistoryStore { } /// This is a super inefficient history store based on an in-memory list of histories -class InMemoryWatchStore: WatchStore { +actor InMemoryWatchStore: WatchStore { /// Maps watch IDs to a list of Refs. This is used to track which records are being watched. var watches: [String: Watch] = [:] diff --git a/Tests/HaystackServerTests/HaystackServerVaporTests.swift b/Tests/HaystackServerTests/HaystackServerVaporTests.swift new file mode 100644 index 0000000..c82637e --- /dev/null +++ b/Tests/HaystackServerTests/HaystackServerVaporTests.swift @@ -0,0 +1,238 @@ +#if ServerVapor +import Haystack +import HaystackServer +import Testing +import VaporTesting + +struct HaystackServerVaporTests { + private func withApp(_ test: (Application) async throws -> Void) async throws { + let app = try await Application.make(.testing) + do { + app.haystack = HaystackAPIMock() + try app.register(collection: HaystackRouteCollection()) + try await test(app) + } catch { + try await app.asyncShutdown() + throw error + } + try await app.asyncShutdown() + } + + @Test func get() async throws { + try await withApp { app in + let responseGrid = try GridBuilder() + .addCols(names: ["id", "foo"]) + .addRow([Haystack.Ref("a"), Marker.val]) + .addRow([Haystack.Ref("b"), Marker.val]) + .toGrid() + + // Test zinc encoding + try await app.test( + .GET, + "/read?id=[@a,@b]", + headers: [ + HTTPHeaders.Name.accept.description: HTTPMediaType.zinc.description, + ] + ) { res in + #expect(res.status == .ok) + #expect(res.headers.contentType == .zinc) + #expect(res.body.string == responseGrid.toZinc()) + } + + // Test JSON encoding + try await app.test( + .GET, + "/read?id=[@a,@b]", + headers: [ + HTTPHeaders.Name.accept.description: HTTPMediaType.json.description, + ] + ) { res in + #expect(res.status == .ok) + #expect(res.headers.contentType == .json) + try #expect(res.content.decode(Grid.self) == responseGrid) + } + } + } + + @Test func getBadQuery() async throws { + try await withApp { app in + try await app.test( + .GET, + "/read?id=[a,b]" // Invalid because expecting Ref, not String + ) { res in + #expect(res.status == .badRequest) + } + } + } + + @Test func post() async throws { + try await withApp { app in + let requestGrid = try GridBuilder() + .addCol(name: "id") + .addRow([Haystack.Ref("a")]) + .addRow([Haystack.Ref("b")]) + .toGrid() + + let responseGrid = try GridBuilder() + .addCols(names: ["id", "foo"]) + .addRow([Haystack.Ref("a"), Marker.val]) + .addRow([Haystack.Ref("b"), Marker.val]) + .toGrid() + + // Test zinc encoding + try await app.test( + .POST, + "/read", + headers: [ + HTTPHeaders.Name.accept.description: HTTPMediaType.zinc.description, + ], + body: .init(string: requestGrid.toZinc()), + beforeRequest: { req in + req.headers.contentType = .zinc + } + ) { res in + #expect(res.status == .ok) + #expect(res.headers.contentType == .zinc) + #expect(res.body.string == responseGrid.toZinc()) + } + + // Test JSON encoding + try await app.test( + .POST, + "/read", + headers: [ + HTTPHeaders.Name.accept.description: HTTPMediaType.json.description, + ], + beforeRequest: { req in + req.headers.contentType = .json + try req.content.encode(requestGrid) + } + ) { res in + #expect(res.status == .ok) + #expect(res.headers.contentType == .json) + try #expect(res.content.decode(Grid.self) == responseGrid) + } + } + } + + @Test func postBadQuery() async throws { + try await withApp { app in + let requestGrid = try GridBuilder() + .addCol(name: "id") + .addRow(["a"]) // Invalid because expecting Ref, not String + .addRow(["b"]) + .toGrid() + + // Test zinc encoding + try await app.test( + .POST, + "/read", + body: .init(string: requestGrid.toZinc()), + beforeRequest: { req in + req.headers.contentType = .zinc + } + ) { res in + #expect(res.status == .badRequest) + } + + // Test JSON encoding + try await app.test( + .POST, + "/read", + beforeRequest: { req in + req.headers.contentType = .json + try req.content.encode(requestGrid) + } + ) { res in + #expect(res.status == .badRequest) + } + } + } +} + +struct HaystackAPIMock: Haystack.API, Sendable { + func close() async throws {} + + func about() async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func defs(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func libs(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func ops(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func filetypes(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func read(ids: [Haystack.Ref]) async throws -> Haystack.Grid { + let gb = GridBuilder() + try gb.addCol(name: "id") + try gb.addCol(name: "foo") + for id in ids { + try gb.addRow([id, Marker.val]) + } + return gb.toGrid() + } + + func read(filter _: String, limit _: Haystack.Number?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func nav(navId _: Haystack.Ref?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func hisRead(id _: Haystack.Ref, range _: Haystack.HisReadRange) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func hisWrite(id _: Haystack.Ref, items _: [Haystack.HisItem]) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func pointWrite(id _: Haystack.Ref, level _: Haystack.Number, val _: any Haystack.Val, who _: String?, duration _: Haystack.Number?) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func pointWriteStatus(id _: Haystack.Ref) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func watchSubCreate(watchDis _: String, lease _: Haystack.Number?, ids _: [Haystack.Ref]) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func watchSubAdd(watchId _: String, lease _: Haystack.Number?, ids _: [Haystack.Ref]) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func watchUnsubRemove(watchId _: String, ids _: [Haystack.Ref]) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func watchUnsubDelete(watchId _: String) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func watchPoll(watchId _: String, refresh _: Bool) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func invokeAction(id _: Haystack.Ref, action _: String, args _: [String: any Haystack.Val]) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } + + func eval(expression _: String) async throws -> Haystack.Grid { + return GridBuilder().toGrid() + } +} +#endif diff --git a/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift b/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift deleted file mode 100644 index 795d928..0000000 --- a/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift +++ /dev/null @@ -1,243 +0,0 @@ -import Haystack -import HaystackServerVapor -import XCTest -import XCTVapor - -final class HaystackServerVaporTests: XCTestCase { - func testGet() throws { - let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) - defer { app.shutdown() } - - let responseGrid = try GridBuilder() - .addCols(names: ["id", "foo"]) - .addRow([Haystack.Ref("a"), Marker.val]) - .addRow([Haystack.Ref("b"), Marker.val]) - .toGrid() - - // Test zinc encoding - try app.test( - .GET, - "/read?id=[@a,@b]", - headers: [ - HTTPHeaders.Name.accept.description: HTTPMediaType.zinc.description, - ] - ) { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.headers.contentType, .zinc) - XCTAssertEqual( - res.body.string, - responseGrid.toZinc() - ) - } - - // Test JSON encoding - try app.test( - .GET, - "/read?id=[@a,@b]", - headers: [ - HTTPHeaders.Name.accept.description: HTTPMediaType.json.description, - ] - ) { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.headers.contentType, .json) - try XCTAssertEqual( - res.content.decode(Grid.self), - responseGrid - ) - } - } - - func testGetBadQuery() throws { - let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) - defer { app.shutdown() } - - try app.test( - .GET, - "/read?id=[a,b]" // Invalid because expecting Ref, not String - ) { res in - XCTAssertEqual(res.status, .badRequest) - } - } - - func testPost() throws { - let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) - defer { app.shutdown() } - - let requestGrid = try GridBuilder() - .addCol(name: "id") - .addRow([Haystack.Ref("a")]) - .addRow([Haystack.Ref("b")]) - .toGrid() - - let responseGrid = try GridBuilder() - .addCols(names: ["id", "foo"]) - .addRow([Haystack.Ref("a"), Marker.val]) - .addRow([Haystack.Ref("b"), Marker.val]) - .toGrid() - - // Test zinc encoding - try app.test( - .POST, - "/read", - headers: [ - HTTPHeaders.Name.accept.description: HTTPMediaType.zinc.description, - ], - body: .init(string: requestGrid.toZinc()), - beforeRequest: { req in - req.headers.contentType = .zinc - } - ) { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.headers.contentType, .zinc) - XCTAssertEqual( - res.body.string, - responseGrid.toZinc() - ) - } - - // Test JSON encoding - try app.test( - .POST, - "/read", - headers: [ - HTTPHeaders.Name.accept.description: HTTPMediaType.json.description, - ], - beforeRequest: { req in - req.headers.contentType = .json - try req.content.encode(requestGrid) - } - ) { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.headers.contentType, .json) - try XCTAssertEqual( - res.content.decode(Grid.self), - responseGrid - ) - } - } - - func testPostBadQuery() throws { - let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) - defer { app.shutdown() } - - let requestGrid = try GridBuilder() - .addCol(name: "id") - .addRow(["a"]) // Invalid because expecting Ref, not String - .addRow(["b"]) - .toGrid() - - // Test zinc encoding - try app.test( - .POST, - "/read", - body: .init(string: requestGrid.toZinc()), - beforeRequest: { req in - req.headers.contentType = .zinc - } - ) { res in - XCTAssertEqual(res.status, .badRequest) - } - - // Test JSON encoding - try app.test( - .POST, - "/read", - beforeRequest: { req in - req.headers.contentType = .json - try req.content.encode(requestGrid) - } - ) { res in - XCTAssertEqual(res.status, .badRequest) - } - } -} - -struct HaystackAPIMock: Haystack.API { - func close() async throws {} - - func about() async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func defs(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func libs(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func ops(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func filetypes(filter _: String?, limit _: Haystack.Number?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func read(ids: [Haystack.Ref]) async throws -> Haystack.Grid { - let gb = GridBuilder() - try gb.addCol(name: "id") - try gb.addCol(name: "foo") - for id in ids { - try gb.addRow([id, Marker.val]) - } - return gb.toGrid() - } - - func read(filter _: String, limit _: Haystack.Number?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func nav(navId _: Haystack.Ref?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func hisRead(id _: Haystack.Ref, range _: Haystack.HisReadRange) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func hisWrite(id _: Haystack.Ref, items _: [Haystack.HisItem]) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func pointWrite(id _: Haystack.Ref, level _: Haystack.Number, val _: any Haystack.Val, who _: String?, duration _: Haystack.Number?) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func pointWriteStatus(id _: Haystack.Ref) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func watchSubCreate(watchDis _: String, lease _: Haystack.Number?, ids _: [Haystack.Ref]) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func watchSubAdd(watchId _: String, lease _: Haystack.Number?, ids _: [Haystack.Ref]) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func watchUnsubRemove(watchId _: String, ids _: [Haystack.Ref]) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func watchUnsubDelete(watchId _: String) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func watchPoll(watchId _: String, refresh _: Bool) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func invokeAction(id _: Haystack.Ref, action _: String, args _: [String: any Haystack.Val]) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } - - func eval(expression _: String) async throws -> Haystack.Grid { - return GridBuilder().toGrid() - } -} diff --git a/Tests/HaystackTests/BoolTests.swift b/Tests/HaystackTests/BoolTests.swift index a66a87c..e1d317e 100644 --- a/Tests/HaystackTests/BoolTests.swift +++ b/Tests/HaystackTests/BoolTests.swift @@ -1,33 +1,28 @@ +import Foundation import Haystack -import XCTest +import Testing -final class BoolTests: XCTestCase { - func testJsonCoding() throws { +struct BoolTests { + @Test func jsonCoding() throws { let value = true let jsonString = #"true"# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Bool.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Bool.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual(false.toZinc(), "F") - XCTAssertEqual(true.toZinc(), "T") + @Test func toZinc() throws { + #expect(false.toZinc() == "F") + #expect(true.toZinc() == "T") } - func testComparable() throws { - XCTAssertFalse(false < false) - XCTAssertTrue(false < true) - XCTAssertFalse(true < false) - XCTAssertFalse(true < true) + @Test func comparable() throws { + #expect((false < false) == false) + #expect(false < true) + #expect((true < false) == false) + #expect((true < true) == false) } } diff --git a/Tests/HaystackTests/CoordTests.swift b/Tests/HaystackTests/CoordTests.swift index 2d1dbb7..dfcc4fe 100644 --- a/Tests/HaystackTests/CoordTests.swift +++ b/Tests/HaystackTests/CoordTests.swift @@ -1,36 +1,28 @@ +import Foundation import Haystack -import XCTest +import Testing -final class CoordTests: XCTestCase { - func testInit() throws { - try XCTAssertThrowsError(Coord(latitude: -91, longitude: 0)) - try XCTAssertThrowsError(Coord(latitude: 91, longitude: 0)) - try XCTAssertThrowsError(Coord(latitude: 0, longitude: -181)) - try XCTAssertThrowsError(Coord(latitude: 0, longitude: 181)) +struct CoordTests { + @Test func testInit() throws { + #expect(throws: CoordError.self) { try Coord(latitude: -91, longitude: 0) } + #expect(throws: CoordError.self) { try Coord(latitude: 91, longitude: 0) } + #expect(throws: CoordError.self) { try Coord(latitude: 0, longitude: -181) } + #expect(throws: CoordError.self) { try Coord(latitude: 0, longitude: 181) } } - func testJsonCoding() throws { + @Test func jsonCoding() throws { let value = try Coord(latitude: 40, longitude: -111.84) let jsonString = #"{"_kind":"coord","lat":40,"lng":-111.84}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Coord.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Coord.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Coord.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Coord.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - try Coord(latitude: 40, longitude: -111.84).toZinc(), - "C(40.0,-111.84)" - ) + @Test func toZinc() throws { + #expect(try Coord(latitude: 40, longitude: -111.84).toZinc() == "C(40.0,-111.84)") } } diff --git a/Tests/HaystackTests/DateTests.swift b/Tests/HaystackTests/DateTests.swift index efd4303..7f5b319 100644 --- a/Tests/HaystackTests/DateTests.swift +++ b/Tests/HaystackTests/DateTests.swift @@ -1,8 +1,9 @@ +import Foundation import Haystack -import XCTest +import Testing -final class DateTests: XCTestCase { - func testJsonCoding() throws { +struct DateTests { + @Test func jsonCoding() throws { let value = try Date( year: 1991, month: 6, @@ -12,26 +13,19 @@ final class DateTests: XCTestCase { // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Date.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Date.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Haystack.Date.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Haystack.Date.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( + @Test func toZinc() throws { + #expect( try Date( year: 1991, month: 6, day: 7 - ).toZinc(), - "1991-06-07" + ).toZinc() == "1991-06-07" ) } } diff --git a/Tests/HaystackTests/DateTimeTests.swift b/Tests/HaystackTests/DateTimeTests.swift index f349b15..ccf0f37 100644 --- a/Tests/HaystackTests/DateTimeTests.swift +++ b/Tests/HaystackTests/DateTimeTests.swift @@ -1,8 +1,9 @@ +import Foundation import Haystack -import XCTest +import Testing -final class DateTimeTests: XCTestCase { - func testJsonCoding() throws { +struct DateTimeTests { + @Test func jsonCoding() throws { let value = try DateTime( year: 1988, month: 4, @@ -18,19 +19,13 @@ final class DateTimeTests: XCTestCase { // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(DateTime.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(DateTime.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(DateTime.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(DateTime.self, from: decodedData) == value) } - func testJsonCoding_noMilliseconds() throws { + @Test func jsonCoding_noMilliseconds() throws { let value = try DateTime( year: 1988, month: 4, @@ -45,20 +40,14 @@ final class DateTimeTests: XCTestCase { // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(DateTime.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(DateTime.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(DateTime.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(DateTime.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( + @Test func toZinc() throws { + #expect( try DateTime( year: 1988, month: 4, @@ -68,11 +57,10 @@ final class DateTimeTests: XCTestCase { second: 43, gmtOffset: 0, timezone: DateTime.utcName - ).toZinc(), - "1988-04-01T10:05:43Z" + ).toZinc() == "1988-04-01T10:05:43Z" ) - XCTAssertEqual( + #expect( try DateTime( year: 1988, month: 4, @@ -82,8 +70,7 @@ final class DateTimeTests: XCTestCase { second: 43, gmtOffset: -5 * 60 * 60, timezone: "New_York" - ).toZinc(), - "1988-04-01T10:05:43-05:00 New_York" + ).toZinc() == "1988-04-01T10:05:43-05:00 New_York" ) } } diff --git a/Tests/HaystackTests/DictTests.swift b/Tests/HaystackTests/DictTests.swift index fae93c5..0958180 100644 --- a/Tests/HaystackTests/DictTests.swift +++ b/Tests/HaystackTests/DictTests.swift @@ -1,8 +1,9 @@ +import Foundation import Haystack -import XCTest +import Testing -final class DictTests: XCTestCase { - func testJsonCoding() throws { +struct DictTests { + @Test func jsonCoding() throws { let value: Dict = [ "bool": true, "str": "abc", @@ -17,20 +18,14 @@ final class DictTests: XCTestCase { // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Dict.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Dict.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Dict.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Dict.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( + @Test func toZinc() throws { + #expect( Dict([ "bool": true, "str": "abc", @@ -39,57 +34,48 @@ final class DictTests: XCTestCase { "bool": false, "str": "xyz", ]), - ]).toZinc(), - // Keys are sorted alphabetically. - #"{bool:T dict:{bool:F str:"xyz"} number:42furloghs str:"abc"}"# + ]).toZinc() == + // Keys are sorted alphabetically. + #"{bool:T dict:{bool:F str:"xyz"} number:42furloghs str:"abc"}"# ) } - func testEquatable() { + @Test func equatable() { // Test basic - XCTAssertEqual( - Dict(["a": "b"]), - Dict(["a": "b"]) - ) - XCTAssertNotEqual( - Dict(["a": "a"]), - Dict(["a": "b"]) - ) + #expect(Dict(["a": "b"]) == Dict(["a": "b"])) + #expect(Dict(["a": "a"]) != Dict(["a": "b"])) // Test element count matters - XCTAssertNotEqual( + #expect( Dict([ "a": "a", "b": "b", - ]), - Dict([ + ]) != Dict([ "a": "a", ]) ) - XCTAssertNotEqual( + #expect( Dict([ "a": "a", - ]), - Dict([ + ]) != Dict([ "a": "a", "b": "b", ]) ) // Test order does not matter - XCTAssertEqual( + #expect( Dict([ "a": "a", "b": "b", - ]), - Dict([ + ]) == Dict([ "b": "b", "a": "a", ]) ) // Test nested - XCTAssertEqual( + #expect( Dict([ "bool": true, "str": "abc", @@ -98,8 +84,7 @@ final class DictTests: XCTestCase { "bool": false, "str": "xyz", ]), - ]), - Dict([ + ]) == Dict([ "bool": true, "str": "abc", "number": Number(42, unit: "furloghs"), @@ -109,7 +94,7 @@ final class DictTests: XCTestCase { ]), ]) ) - XCTAssertNotEqual( + #expect( Dict([ "bool": true, "str": "abc", @@ -118,8 +103,7 @@ final class DictTests: XCTestCase { "bool": false, "str": "xyz", ]), - ]), - Dict([ + ]) != Dict([ "bool": true, "str": "abc", "number": Number(42, unit: "furloghs"), @@ -131,27 +115,27 @@ final class DictTests: XCTestCase { ) } - func testTrap() { + @Test func trap() throws { let dict: Dict = ["a": "abc", "b": null] - try XCTAssertNoThrow(dict.trap("a")) - try XCTAssertEqual(dict.trap("a", as: String.self), "abc") - try XCTAssertThrowsError(dict.trap("a", as: Number.self)) - try XCTAssertThrowsError(dict.trap("b")) - try XCTAssertThrowsError(dict.trap("c")) + #expect(throws: Never.self) { try dict.trap("a") } + try #expect(dict.trap("a", as: String.self) == "abc") + #expect(throws: (any Error).self) { try dict.trap("a", as: Number.self) } + #expect(throws: (any Error).self) { try dict.trap("b") } + #expect(throws: (any Error).self) { try dict.trap("c") } } - func testGet() { + @Test func get() throws { let dict: Dict = ["a": "abc", "b": null] - try XCTAssertNotNil(dict.get("a")) - try XCTAssertEqual(dict.get("a", as: String.self), "abc") - try XCTAssertThrowsError(dict.get("a", as: Number.self)) - try XCTAssertNil(dict.get("b")) - try XCTAssertNil(dict.get("c")) + try #expect(dict.get("a") != nil) + try #expect(dict.get("a", as: String.self) == "abc") + #expect(throws: ValError.self) { try dict.get("a", as: Number.self) } + try #expect(dict.get("b") == nil) + try #expect(dict.get("c") == nil) } - func testCollection() { + @Test func collection() { let dict: Dict = [ "bool": true, "str": "abc", @@ -163,18 +147,18 @@ final class DictTests: XCTestCase { ] // Test index access - XCTAssertEqual(dict["bool"] as? Bool, true) - XCTAssertEqual(dict["str"] as? String, "abc") - XCTAssertEqual(dict["number"] as? Number, Number(42, unit: "furloghs")) - XCTAssertEqual(dict["dict"] as? Dict, ["bool": true, "str": "xyz"]) + #expect(dict["bool"] as? Bool == true) + #expect(dict["str"] as? String == "abc") + #expect(dict["number"] as? Number == Number(42, unit: "furloghs")) + #expect(dict["dict"] as? Dict == ["bool": true, "str": "xyz"]) // Test loop for (key, value) in dict { switch key { - case "bool": XCTAssertEqual(value as? Bool, true) - case "str": XCTAssertEqual(value as? String, "abc") - case "number": XCTAssertEqual((value as? Number), Number(42, unit: "furloghs")) - case "dict": XCTAssertEqual((value as? Dict), ["bool": true, "str": "xyz"]) + case "bool": #expect(value as? Bool == true) + case "str": #expect(value as? String == "abc") + case "number": #expect((value as? Number) == Number(42, unit: "furloghs")) + case "dict": #expect((value as? Dict) == ["bool": true, "str": "xyz"]) default: break } } diff --git a/Tests/HaystackTests/FilterTests.swift b/Tests/HaystackTests/FilterTests.swift index 92c897b..ce42c9e 100644 --- a/Tests/HaystackTests/FilterTests.swift +++ b/Tests/HaystackTests/FilterTests.swift @@ -1,13 +1,13 @@ import Haystack -import XCTest +import Testing -final class FilterTests: XCTestCase { - func testIdentity() throws { - try XCTAssertTrue(FilterFactory.has("a").equals(FilterFactory.has("a"))) - try XCTAssertFalse(FilterFactory.has("a").equals(FilterFactory.has("b"))) +struct FilterTests { + @Test func identity() throws { + try #expect(FilterFactory.has("a").equals(FilterFactory.has("a"))) + try #expect(!FilterFactory.has("a").equals(FilterFactory.has("b"))) } - func testBasics() throws { + @Test func basics() throws { try verifyParse("x", FilterFactory.has("x")) try verifyParse("foo", FilterFactory.has("foo")) try verifyParse("fooBar", FilterFactory.has("fooBar")) @@ -17,35 +17,35 @@ final class FilterTests: XCTestCase { try verifyParse("not foo", FilterFactory.missing("foo")) } - func testZincOnlyLiteralsDontWork() throws { - try XCTAssertThrowsError(FilterFactory.make("x==T")) - try XCTAssertThrowsError(FilterFactory.make("x==F")) - try XCTAssertThrowsError(FilterFactory.make("x==F")) + @Test func zincOnlyLiteralsDontWork() throws { + #expect(throws: (any Error).self) { try FilterFactory.make("x==T") } + #expect(throws: (any Error).self) { try FilterFactory.make("x==F") } + #expect(throws: (any Error).self) { try FilterFactory.make("x==F") } } - func testBool() throws { + @Test func bool() throws { try verifyParse("x->y==true", FilterFactory.eq("x->y", true)) try verifyParse("x->y!=false", FilterFactory.ne("x->y", false)) } - func testStr() throws { + @Test func str() throws { try verifyParse("x==\"hi\"", FilterFactory.eq("x", "hi")) try verifyParse("x!=\"\\\"hi\\\"\"", FilterFactory.ne("x", "\"hi\"")) try verifyParse("x==\"_\\uabcd_\\n_\"", FilterFactory.eq("x", "_\u{abcd}_\n_")) } - func testUri() throws { + @Test func uri() throws { try verifyParse("ref==`http://foo/?bar`", FilterFactory.eq("ref", Uri("http://foo/?bar"))) try verifyParse("ref->x==`file name`", FilterFactory.eq("ref->x", Uri("file name"))) try verifyParse("ref == `foo bar`", FilterFactory.eq("ref", Uri("foo bar"))) } - func testInt() throws { + @Test func int() throws { try verifyParse("num < 4", FilterFactory.lt("num", Number(4))) try verifyParse("num <= -99", FilterFactory.le("num", Number(-99))) } - func testFloat() throws { + @Test func float() throws { try verifyParse("num < 4.0", FilterFactory.lt("num", Number(4.0))) try verifyParse("num <= -9.6", FilterFactory.le("num", Number(-9.6))) try verifyParse("num > 400000", FilterFactory.gt("num", Number(4e5))) @@ -53,42 +53,42 @@ final class FilterTests: XCTestCase { try verifyParse("num >= 2.16", FilterFactory.ge("num", Number(2.16))) } - func testUnit() throws { + @Test func unit() throws { try verifyParse("dur < 5ns", FilterFactory.lt("dur", Number(5, unit: "ns"))) try verifyParse("dur < 10kg", FilterFactory.lt("dur", Number(10, unit: "kg"))) try verifyParse("dur < -9sec", FilterFactory.lt("dur", Number(-9, unit: "sec"))) try verifyParse("dur < 2.5hr", FilterFactory.lt("dur", Number(2.5, unit: "hr"))) } - func testDateTime() throws { + @Test func dateTime() throws { try verifyParse("foo < 2009-10-30", FilterFactory.lt("foo", Date("2009-10-30"))) try verifyParse("foo < 08:30:00", FilterFactory.lt("foo", Time("08:30:00"))) try verifyParse("foo < 13:00:00", FilterFactory.lt("foo", Time("13:00:00"))) } - func testRef() throws { + @Test func ref() throws { try verifyParse("author == @xyz", FilterFactory.eq("author", Ref("xyz"))) try verifyParse("author==@xyz:foo.bar", FilterFactory.eq("author", Ref("xyz:foo.bar"))) } - func testAnd() throws { + @Test func and() throws { try verifyParse("a and b", FilterFactory.has("a").and(FilterFactory.has("b"))) try verifyParse("a and b and c == 3", FilterFactory.has("a").and(FilterFactory.has("b").and(FilterFactory.eq("c", Number(3))))) } - func testOr() throws { + @Test func or() throws { try verifyParse("a or b", FilterFactory.has("a").or(FilterFactory.has("b"))) try verifyParse("a or b or c == 3", FilterFactory.has("a").or(FilterFactory.has("b").or(FilterFactory.eq("c", Number(3))))) } - func testParens() throws { + @Test func parens() throws { try verifyParse("(a)", FilterFactory.has("a")) try verifyParse("(a) and (b)", FilterFactory.has("a").and(FilterFactory.has("b"))) try verifyParse("( a ) and ( b ) ", FilterFactory.has("a").and(FilterFactory.has("b"))) try verifyParse("(a or b) or (c == 3)", FilterFactory.has("a").or(FilterFactory.has("b")).or(FilterFactory.eq("c", Number(3)))) } - func testCombo() throws { + @Test func combo() throws { let isA = try FilterFactory.has("a") let isB = try FilterFactory.has("b") let isC = try FilterFactory.has("c") @@ -102,10 +102,10 @@ final class FilterTests: XCTestCase { func verifyParse(_ s: String, _ expected: any Filter) throws { let actual = try FilterFactory.make(s) - XCTAssertTrue(actual.equals(expected)) + #expect(actual.equals(expected)) } - func testInclude() throws { + @Test func include() throws { let a: Dict = try [ "dis": "a", "num": Number(10), @@ -234,32 +234,32 @@ final class FilterTests: XCTestCase { actual += id } } - XCTAssertEqual(actual, expected) + #expect(actual == expected) } - func testPath() throws { + @Test func path() throws { // single name var path = try Path.make(path: "foo") - XCTAssertEqual(path.count, 1) - XCTAssertEqual(path[0], "foo") - XCTAssertEqual(path.description, "foo") - try XCTAssertEqual(path, Path.make(path: "foo")) + #expect(path.count == 1) + #expect(path[0] == "foo") + #expect(path.description == "foo") + #expect(try path == Path.make(path: "foo")) // two names path = try Path.make(path: "foo->bar") - XCTAssertEqual(path.count, 2) - XCTAssertEqual(path[0], "foo") - XCTAssertEqual(path[1], "bar") - XCTAssertEqual(path.description, "foo->bar") - try XCTAssertEqual(path, Path.make(path: "foo->bar")) + #expect(path.count == 2) + #expect(path[0] == "foo") + #expect(path[1] == "bar") + #expect(path.description == "foo->bar") + #expect(try path == Path.make(path: "foo->bar")) // three names path = try Path.make(path: "x->y->z") - XCTAssertEqual(path.count, 3) - XCTAssertEqual(path[0], "x") - XCTAssertEqual(path[1], "y") - XCTAssertEqual(path[2], "z") - XCTAssertEqual(path.description, "x->y->z") - try XCTAssertEqual(path, Path.make(path: "x->y->z")) + #expect(path.count == 3) + #expect(path[0] == "x") + #expect(path[1] == "y") + #expect(path[2] == "z") + #expect(path.description == "x->y->z") + #expect(try path == Path.make(path: "x->y->z")) } } diff --git a/Tests/HaystackTests/GridTests.swift b/Tests/HaystackTests/GridTests.swift index 6f81807..35242e7 100644 --- a/Tests/HaystackTests/GridTests.swift +++ b/Tests/HaystackTests/GridTests.swift @@ -1,8 +1,9 @@ +import Foundation import Haystack -import XCTest +import Testing -final class GridTests: XCTestCase { - func testJsonCoding() throws { +struct GridTests { + @Test func jsonCoding() throws { let value = try GridBuilder() .setMeta(["foo": "bar"]) .addCol(name: "dis", meta: ["dis": "Equip Name"]) @@ -16,38 +17,26 @@ final class GridTests: XCTestCase { // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Grid.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Grid.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Grid.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Grid.self, from: decodedData) == value) } - func testJsonCoding_empty() throws { + @Test func jsonCoding_empty() throws { let value = GridBuilder().toGrid() let jsonString = #"{"_kind":"grid","meta":{"ver":"3.0"},"cols":[{"name":"empty"}],"rows":[]}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Grid.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Grid.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Grid.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Grid.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( + @Test func toZinc() throws { + #expect( try GridBuilder() .setMeta(["foo": "bar"]) .addCol(name: "dis", meta: ["dis": "Equip Name"]) @@ -57,29 +46,31 @@ final class GridTests: XCTestCase { .addRow(["dis": "RTU-1", "equip": marker, "siteRef": Ref("153c-699a", dis: "HQ"), "installed": Date(year: 2005, month: 6, day: 1)]) .addRow(["dis": "RTU-2", "equip": marker, "siteRef": Ref("153c-699b", dis: "Library"), "installed": Date(year: 1997, month: 7, day: 12)]) .toGrid() - .toZinc(), - """ - ver:"3.0" foo:"bar" - dis dis:"Equip Name", equip, siteRef, installed - "RTU-1", M, @153c-699a HQ, 2005-06-01 - "RTU-2", M, @153c-699b Library, 1997-07-12 - """ + .toZinc() + == + """ + ver:"3.0" foo:"bar" + dis dis:"Equip Name", equip, siteRef, installed + "RTU-1", M, @153c-699a HQ, 2005-06-01 + "RTU-2", M, @153c-699b Library, 1997-07-12 + """ ) // Test empty grid - XCTAssertEqual( + #expect( GridBuilder() .toGrid() - .toZinc(), - """ - ver:"3.0" - empty + .toZinc() + == + """ + ver:"3.0" + empty - """ + """ ) } - func testEquatable() throws { + @Test func equatable() throws { let builder1 = try GridBuilder() .setMeta(["ver": "3.0", "foo": "bar"]) .addCol(name: "dis", meta: ["dis": "Equip Name"]) @@ -99,17 +90,11 @@ final class GridTests: XCTestCase { .addRow(["dis": "RTU-2", "equip": marker, "siteRef": Ref("153c-699b", dis: "Library"), "managed": false]) // Test basic - XCTAssertEqual( - builder1.toGrid(), - builder1.toGrid() - ) - XCTAssertNotEqual( - builder1.toGrid(), - builder2.toGrid() - ) + #expect(builder1.toGrid() == builder1.toGrid()) + #expect(builder1.toGrid() != builder2.toGrid()) } - func testCollection() throws { + @Test func collection() throws { let grid = try GridBuilder() .setMeta(["ver": "3.0", "foo": "bar"]) .addCol(name: "dis", meta: ["dis": "Equip Name"]) @@ -121,15 +106,15 @@ final class GridTests: XCTestCase { .toGrid() // Test index access - try XCTAssertEqual(grid[0], ["dis": "RTU-1", "equip": marker, "siteRef": Ref("153c-699a", dis: "HQ"), "managed": true]) - try XCTAssertEqual(grid[1], ["dis": "RTU-2", "equip": marker, "siteRef": Ref("153c-699b", dis: "Library"), "managed": false]) - XCTAssertEqual(grid[0]["dis"] as? String, "RTU-1") + #expect(try grid[0] == ["dis": "RTU-1", "equip": marker, "siteRef": Ref("153c-699a", dis: "HQ"), "managed": true]) + #expect(try grid[1] == ["dis": "RTU-2", "equip": marker, "siteRef": Ref("153c-699b", dis: "Library"), "managed": false]) + #expect(grid[0]["dis"] as? String == "RTU-1") // Test loop for (i, row) in grid.enumerated() { switch i { - case 0: try XCTAssertEqual(row, ["dis": "RTU-1", "equip": marker, "siteRef": Ref("153c-699a", dis: "HQ"), "managed": true]) - case 1: try XCTAssertEqual(row, ["dis": "RTU-2", "equip": marker, "siteRef": Ref("153c-699b", dis: "Library"), "managed": false]) + case 0: #expect(try row == ["dis": "RTU-1", "equip": marker, "siteRef": Ref("153c-699a", dis: "HQ"), "managed": true]) + case 1: #expect(try row == ["dis": "RTU-2", "equip": marker, "siteRef": Ref("153c-699b", dis: "Library"), "managed": false]) default: break } } diff --git a/Tests/HaystackTests/IO/ZincReaderTests.swift b/Tests/HaystackTests/IO/ZincReaderTests.swift index 439549b..09e2a34 100644 --- a/Tests/HaystackTests/IO/ZincReaderTests.swift +++ b/Tests/HaystackTests/IO/ZincReaderTests.swift @@ -1,9 +1,9 @@ import Haystack -import XCTest +import Testing -final class ZincReaderTests: XCTestCase { - func testNullGridMetaAndColMeta() throws { - try XCTAssertEqualZincGrid( +struct ZincReaderTests { + @Test func nullGridMetaAndColMeta() throws { + try expectEqualZincGrid( zinc: """ ver:"3.0" tag:N a nullmetatag:N, b markermetatag @@ -15,8 +15,8 @@ final class ZincReaderTests: XCTestCase { ) } - func testGridWithSymbols() throws { - try XCTAssertEqualZincGrid( + @Test func gridWithSymbols() throws { + try expectEqualZincGrid( zinc: """ ver:"3.0" a,b @@ -30,8 +30,8 @@ final class ZincReaderTests: XCTestCase { ) } - func test() throws { - try XCTAssertEqualZincGrid( + @Test func test() throws { + try expectEqualZincGrid( zinc: """ ver:"3.0" fooBar33 @@ -42,7 +42,7 @@ final class ZincReaderTests: XCTestCase { rows: [] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" tag foo:"bar" xyz @@ -55,7 +55,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" val @@ -70,7 +70,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" a,b @@ -85,7 +85,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" a, b, c, d @@ -112,7 +112,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" foo @@ -131,7 +131,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" a,b @@ -150,7 +150,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" a, b, c @@ -173,7 +173,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" a,b @@ -189,7 +189,7 @@ final class ZincReaderTests: XCTestCase { ] ) - try XCTAssertEqualZincGrid( + try expectEqualZincGrid( zinc: """ ver:"3.0" a: 2009-02-03T04:05:06Z foo b: 2010-02-03T04:05:06Z UTC bar c: 2009-12-03T04:05:06Z London baz a @@ -228,7 +228,7 @@ final class ZincReaderTests: XCTestCase { ) } - private func XCTAssertEqualZincGrid( + private func expectEqualZincGrid( zinc: String, meta: [String: any Val], cols: [(String, [String: any Val]?)], @@ -248,9 +248,8 @@ final class ZincReaderTests: XCTestCase { } let expected = builder.toGrid() - XCTAssertEqual( - actual, - expected, + #expect( + actual == expected, """ \(actual.toZinc()) is not equal to diff --git a/Tests/HaystackTests/IO/ZincTokenizerTests.swift b/Tests/HaystackTests/IO/ZincTokenizerTests.swift index def38df..d05e35b 100644 --- a/Tests/HaystackTests/IO/ZincTokenizerTests.swift +++ b/Tests/HaystackTests/IO/ZincTokenizerTests.swift @@ -1,113 +1,113 @@ @testable import Haystack -import XCTest +import Testing -final class ZincTokenizerTests: XCTestCase { - func testEmpty() throws { - try XCTAssertEqualTokensAndVals(zinc: "", expected: []) +struct ZincTokenizerTests { + @Test func empty() throws { + try expectEqualTokensAndVals(zinc: "", expected: []) } - func testId() throws { - try XCTAssertEqualTokensAndVals(zinc: "x", expected: [(.id, "x")]) - try XCTAssertEqualTokensAndVals(zinc: "fooBar", expected: [(.id, "fooBar")]) - try XCTAssertEqualTokensAndVals(zinc: "fooBar1999x", expected: [(.id, "fooBar1999x")]) - try XCTAssertEqualTokensAndVals(zinc: "foo_23", expected: [(.id, "foo_23")]) - try XCTAssertEqualTokensAndVals(zinc: "Foo", expected: [(.id, "Foo")]) + @Test func id() throws { + try expectEqualTokensAndVals(zinc: "x", expected: [(.id, "x")]) + try expectEqualTokensAndVals(zinc: "fooBar", expected: [(.id, "fooBar")]) + try expectEqualTokensAndVals(zinc: "fooBar1999x", expected: [(.id, "fooBar1999x")]) + try expectEqualTokensAndVals(zinc: "foo_23", expected: [(.id, "foo_23")]) + try expectEqualTokensAndVals(zinc: "Foo", expected: [(.id, "Foo")]) } - func testNum() throws { - try XCTAssertEqualTokensAndVals(zinc: "5", expected: [(.num, Number(5))]) - try XCTAssertEqualTokensAndVals(zinc: "0x1234_abcd", expected: [(.num, Number(0x1234_ABCD))]) + @Test func num() throws { + try expectEqualTokensAndVals(zinc: "5", expected: [(.num, Number(5))]) + try expectEqualTokensAndVals(zinc: "0x1234_abcd", expected: [(.num, Number(0x1234_ABCD))]) } - func testFloats() throws { - try XCTAssertEqualTokensAndVals(zinc: "5.0", expected: [(.num, Number(5))]) - try XCTAssertEqualTokensAndVals(zinc: "5.42", expected: [(.num, Number(5.42))]) - try XCTAssertEqualTokensAndVals(zinc: "123.2e32", expected: [(.num, Number(123.2e32))]) - try XCTAssertEqualTokensAndVals(zinc: "123.2e+32", expected: [(.num, Number(123.2e+32))]) - try XCTAssertEqualTokensAndVals(zinc: "2_123.2e+32", expected: [(.num, Number(2123.2e+32))]) - try XCTAssertEqualTokensAndVals(zinc: "4.2e-7", expected: [(.num, Number(4.2e-7))]) + @Test func floats() throws { + try expectEqualTokensAndVals(zinc: "5.0", expected: [(.num, Number(5))]) + try expectEqualTokensAndVals(zinc: "5.42", expected: [(.num, Number(5.42))]) + try expectEqualTokensAndVals(zinc: "123.2e32", expected: [(.num, Number(123.2e32))]) + try expectEqualTokensAndVals(zinc: "123.2e+32", expected: [(.num, Number(123.2e+32))]) + try expectEqualTokensAndVals(zinc: "2_123.2e+32", expected: [(.num, Number(2123.2e+32))]) + try expectEqualTokensAndVals(zinc: "4.2e-7", expected: [(.num, Number(4.2e-7))]) } - func testNumberWithUnits() throws { - try XCTAssertEqualTokensAndVals(zinc: "-40ms", expected: [(.num, Number(-40, unit: "ms"))]) - try XCTAssertEqualTokensAndVals(zinc: "1sec", expected: [(.num, Number(1, unit: "sec"))]) - try XCTAssertEqualTokensAndVals(zinc: "5hr", expected: [(.num, Number(5, unit: "hr"))]) - try XCTAssertEqualTokensAndVals(zinc: "2.5day", expected: [(.num, Number(2.5, unit: "day"))]) - try XCTAssertEqualTokensAndVals(zinc: "12%", expected: [(.num, Number(12, unit: "%"))]) - try XCTAssertEqualTokensAndVals(zinc: "987_foo", expected: [(.num, Number(987, unit: "_foo"))]) - try XCTAssertEqualTokensAndVals(zinc: "-1.2m/s", expected: [(.num, Number(-1.2, unit: "m/s"))]) - try XCTAssertEqualTokensAndVals(zinc: #"12kWh/ft\u00B2"#, expected: [(.num, Number(12, unit: "kWh/ft\u{00B2}"))]) - try XCTAssertEqualTokensAndVals(zinc: "3_000.5J/kg_dry", expected: [(.num, Number(3000.5, unit: "J/kg_dry"))]) + @Test func numberWithUnits() throws { + try expectEqualTokensAndVals(zinc: "-40ms", expected: [(.num, Number(-40, unit: "ms"))]) + try expectEqualTokensAndVals(zinc: "1sec", expected: [(.num, Number(1, unit: "sec"))]) + try expectEqualTokensAndVals(zinc: "5hr", expected: [(.num, Number(5, unit: "hr"))]) + try expectEqualTokensAndVals(zinc: "2.5day", expected: [(.num, Number(2.5, unit: "day"))]) + try expectEqualTokensAndVals(zinc: "12%", expected: [(.num, Number(12, unit: "%"))]) + try expectEqualTokensAndVals(zinc: "987_foo", expected: [(.num, Number(987, unit: "_foo"))]) + try expectEqualTokensAndVals(zinc: "-1.2m/s", expected: [(.num, Number(-1.2, unit: "m/s"))]) + try expectEqualTokensAndVals(zinc: #"12kWh/ft\u00B2"#, expected: [(.num, Number(12, unit: "kWh/ft\u{00B2}"))]) + try expectEqualTokensAndVals(zinc: "3_000.5J/kg_dry", expected: [(.num, Number(3000.5, unit: "J/kg_dry"))]) } - func testStrings() throws { - try XCTAssertEqualTokensAndVals(zinc: #""""#, expected: [(.str, "")]) - try XCTAssertEqualTokensAndVals(zinc: #""x y""#, expected: [(.str, "x y")]) - try XCTAssertEqualTokensAndVals(zinc: #""x\"y""#, expected: [(.str, #"x"y"#)]) - try XCTAssertEqualTokensAndVals(zinc: #""_\u012f \n \t \\_""#, expected: [(.str, "_\u{012f} \n \t \\_")]) + @Test func strings() throws { + try expectEqualTokensAndVals(zinc: #""""#, expected: [(.str, "")]) + try expectEqualTokensAndVals(zinc: #""x y""#, expected: [(.str, "x y")]) + try expectEqualTokensAndVals(zinc: #""x\"y""#, expected: [(.str, #"x"y"#)]) + try expectEqualTokensAndVals(zinc: #""_\u012f \n \t \\_""#, expected: [(.str, "_\u{012f} \n \t \\_")]) } - func testDate() throws { - try XCTAssertEqualTokensAndVals(zinc: "2016-06-06", expected: [(.date, Date(year: 2016, month: 06, day: 06))]) + @Test func date() throws { + try expectEqualTokensAndVals(zinc: "2016-06-06", expected: [(.date, Date(year: 2016, month: 06, day: 06))]) } - func testTime() throws { - try XCTAssertEqualTokensAndVals(zinc: "8:30", expected: [(.time, Time(hour: 8, minute: 30, second: 0))]) - try XCTAssertEqualTokensAndVals(zinc: "20:15", expected: [(.time, Time(hour: 20, minute: 15, second: 0))]) - try XCTAssertEqualTokensAndVals(zinc: "00:00", expected: [(.time, Time(hour: 0, minute: 0, second: 0))]) - try XCTAssertEqualTokensAndVals(zinc: "00:00:00", expected: [(.time, Time(hour: 0, minute: 0, second: 0))]) - try XCTAssertEqualTokensAndVals(zinc: "01:02:03", expected: [(.time, Time(hour: 1, minute: 2, second: 3))]) - try XCTAssertEqualTokensAndVals(zinc: "23:59:59", expected: [(.time, Time(hour: 23, minute: 59, second: 59))]) - try XCTAssertEqualTokensAndVals(zinc: "12:00:12.9", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 900))]) - try XCTAssertEqualTokensAndVals(zinc: "12:00:12.99", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 990))]) - try XCTAssertEqualTokensAndVals(zinc: "12:00:12.999", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 999))]) - try XCTAssertEqualTokensAndVals(zinc: "12:00:12.000", expected: [(.time, Time(hour: 12, minute: 0, second: 12))]) - try XCTAssertEqualTokensAndVals(zinc: "12:00:12.001", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 1))]) + @Test func time() throws { + try expectEqualTokensAndVals(zinc: "8:30", expected: [(.time, Time(hour: 8, minute: 30, second: 0))]) + try expectEqualTokensAndVals(zinc: "20:15", expected: [(.time, Time(hour: 20, minute: 15, second: 0))]) + try expectEqualTokensAndVals(zinc: "00:00", expected: [(.time, Time(hour: 0, minute: 0, second: 0))]) + try expectEqualTokensAndVals(zinc: "00:00:00", expected: [(.time, Time(hour: 0, minute: 0, second: 0))]) + try expectEqualTokensAndVals(zinc: "01:02:03", expected: [(.time, Time(hour: 1, minute: 2, second: 3))]) + try expectEqualTokensAndVals(zinc: "23:59:59", expected: [(.time, Time(hour: 23, minute: 59, second: 59))]) + try expectEqualTokensAndVals(zinc: "12:00:12.9", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 900))]) + try expectEqualTokensAndVals(zinc: "12:00:12.99", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 990))]) + try expectEqualTokensAndVals(zinc: "12:00:12.999", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 999))]) + try expectEqualTokensAndVals(zinc: "12:00:12.000", expected: [(.time, Time(hour: 12, minute: 0, second: 12))]) + try expectEqualTokensAndVals(zinc: "12:00:12.001", expected: [(.time, Time(hour: 12, minute: 0, second: 12, millisecond: 1))]) } - func testDateTime() throws { - try XCTAssertEqualTokensAndVals( + @Test func dateTime() throws { + try expectEqualTokensAndVals( zinc: "2016-01-13T09:51:33-05:00 New_York", - expected: try [(.datetime, DateTime(year: 2016, month: 1, day: 13, hour: 9, minute: 51, second: 33, gmtOffset: -5 * 60 * 60, timezone: "New_York"))] + expected: [(.datetime, DateTime(year: 2016, month: 1, day: 13, hour: 9, minute: 51, second: 33, gmtOffset: -5 * 60 * 60, timezone: "New_York"))] ) - try XCTAssertEqualTokensAndVals( + try expectEqualTokensAndVals( zinc: "2016-01-13T09:51:33.352-05:00 New_York", - expected: try [(.datetime, DateTime(year: 2016, month: 1, day: 13, hour: 9, minute: 51, second: 33, millisecond: 352, gmtOffset: -5 * 60 * 60, timezone: "New_York"))] + expected: [(.datetime, DateTime(year: 2016, month: 1, day: 13, hour: 9, minute: 51, second: 33, millisecond: 352, gmtOffset: -5 * 60 * 60, timezone: "New_York"))] ) - try XCTAssertEqualTokensAndVals( + try expectEqualTokensAndVals( zinc: "2010-12-18T14:11:30.924Z", - expected: try [(.datetime, DateTime(year: 2010, month: 12, day: 18, hour: 14, minute: 11, second: 30, millisecond: 924, timezone: DateTime.utcName))] + expected: [(.datetime, DateTime(year: 2010, month: 12, day: 18, hour: 14, minute: 11, second: 30, millisecond: 924, timezone: DateTime.utcName))] ) - try XCTAssertEqualTokensAndVals( + try expectEqualTokensAndVals( zinc: "2010-12-18T14:11:30.924Z UTC", - expected: try [(.datetime, DateTime(year: 2010, month: 12, day: 18, hour: 14, minute: 11, second: 30, millisecond: 924, timezone: DateTime.utcName))] + expected: [(.datetime, DateTime(year: 2010, month: 12, day: 18, hour: 14, minute: 11, second: 30, millisecond: 924, timezone: DateTime.utcName))] ) - try XCTAssertEqualTokensAndVals( + try expectEqualTokensAndVals( zinc: "2010-12-18T14:11:30.924Z London", - expected: try [(.datetime, DateTime(year: 2010, month: 12, day: 18, hour: 14, minute: 11, second: 30, millisecond: 924, timezone: "London"))] + expected: [(.datetime, DateTime(year: 2010, month: 12, day: 18, hour: 14, minute: 11, second: 30, millisecond: 924, timezone: "London"))] ) - try XCTAssertEqualTokensAndVals( + try expectEqualTokensAndVals( zinc: "2010-03-01T23:55:00.013-05:00 GMT+5", - expected: try [(.datetime, DateTime(year: 2010, month: 3, day: 1, hour: 23, minute: 55, second: 0, millisecond: 13, gmtOffset: -5 * 60 * 60, timezone: "GMT+5"))] + expected: [(.datetime, DateTime(year: 2010, month: 3, day: 1, hour: 23, minute: 55, second: 0, millisecond: 13, gmtOffset: -5 * 60 * 60, timezone: "GMT+5"))] ) - try XCTAssertEqualTokensAndVals( + try expectEqualTokensAndVals( zinc: "2010-03-01T23:55:00.013+10:00 GMT-10", - expected: try [(.datetime, DateTime(year: 2010, month: 3, day: 1, hour: 23, minute: 55, second: 0, millisecond: 13, gmtOffset: 10 * 60 * 60, timezone: "GMT-10"))] + expected: [(.datetime, DateTime(year: 2010, month: 3, day: 1, hour: 23, minute: 55, second: 0, millisecond: 13, gmtOffset: 10 * 60 * 60, timezone: "GMT-10"))] ) } - func testRef() throws { - try XCTAssertEqualTokensAndVals(zinc: "@125b780e-0684e169", expected: [(.ref, Ref("125b780e-0684e169"))]) - try XCTAssertEqualTokensAndVals(zinc: "@demo:_:-.~", expected: [(.ref, Ref("demo:_:-.~"))]) + @Test func ref() throws { + try expectEqualTokensAndVals(zinc: "@125b780e-0684e169", expected: [(.ref, Ref("125b780e-0684e169"))]) + try expectEqualTokensAndVals(zinc: "@demo:_:-.~", expected: [(.ref, Ref("demo:_:-.~"))]) } - func testUri() throws { - try XCTAssertEqualTokensAndVals(zinc: "`http://foo/`", expected: [(.uri, Uri("http://foo/"))]) - try XCTAssertEqualTokensAndVals(zinc: "`_ \\n \\\\ \\`_`", expected: [(.uri, Uri("_ \n \\\\ `_"))]) + @Test func uri() throws { + try expectEqualTokensAndVals(zinc: "`http://foo/`", expected: [(.uri, Uri("http://foo/"))]) + try expectEqualTokensAndVals(zinc: "`_ \\n \\\\ \\`_`", expected: [(.uri, Uri("_ \n \\\\ `_"))]) } - func testWhitespace() throws { - try XCTAssertEqualTokensAndVals( + @Test func whitespace() throws { + try expectEqualTokensAndVals( zinc: "a\n b \rc \r\nd\n\ne", expected: [ (.id, "a"), @@ -124,7 +124,7 @@ final class ZincTokenizerTests: XCTestCase { ) } - private func XCTAssertEqualTokensAndVals( + private func expectEqualTokensAndVals( zinc: String, expected: [(ZincToken, (any Val)?)], file _: StaticString = #filePath, @@ -134,30 +134,30 @@ final class ZincTokenizerTests: XCTestCase { let tokenizer = try ZincTokenizer(zinc) while true { let token = try tokenizer.next() - XCTAssertEqual(token, tokenizer.token) + #expect(token == tokenizer.token) if token == ZincToken.eof { break } actual.append((token, tokenizer.val)) } - XCTAssertEqual(actual.count, expected.count, "\(actual) does not equal \(expected)") + #expect(actual.count == expected.count, "\(actual) does not equal \(expected)") for (actualElement, expectedElement) in zip(actual, expected) { - XCTAssertEqual(actualElement.0, expectedElement.0) + #expect(actualElement.0 == expectedElement.0) switch actualElement.1 { case .none: switch expectedElement.1 { case .none: continue case let .some(expectedVal): - XCTFail("Val nil does not equal \(expectedVal)") + Issue.record("Val nil does not equal \(expectedVal)") } case let .some(actualVal): switch expectedElement.1 { case .none: - XCTFail("Val \(actualVal) does not equal nil") + Issue.record("Val \(actualVal) does not equal nil") case let .some(expectedVal): - XCTAssertTrue( + #expect( actualVal.equals(expectedVal), "Val \(actualVal) does not equal \(expectedVal)" ) diff --git a/Tests/HaystackTests/ListTests.swift b/Tests/HaystackTests/ListTests.swift index 7827f3e..ac35dc8 100644 --- a/Tests/HaystackTests/ListTests.swift +++ b/Tests/HaystackTests/ListTests.swift @@ -1,8 +1,9 @@ +import Foundation import Haystack -import XCTest +import Testing -final class ListTests: XCTestCase { - func testJsonCoding() throws { +struct ListTests { + @Test func jsonCoding() throws { let value: List = [ true, "abc", @@ -16,20 +17,14 @@ final class ListTests: XCTestCase { // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(List.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(List.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(List.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(List.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( + @Test func toZinc() throws { + #expect( List([ true, "abc", @@ -38,56 +33,26 @@ final class ListTests: XCTestCase { false, "xyz", ]), - ]).toZinc(), - #"[T, "abc", 42furloghs, [F, "xyz"]]"# + ]).toZinc() + == + #"[T, "abc", 42furloghs, [F, "xyz"]]"# ) } - func testEquatable() { + @Test func equatable() { // Test basic - XCTAssertEqual( - List(["a"]), - List(["a"]) - ) - XCTAssertNotEqual( - List(["a"]), - List(["b"]) - ) + #expect(List(["a"]) == List(["a"])) + #expect(List(["a"]) != List(["b"])) // Test element count matters - XCTAssertNotEqual( - List([ - "a", - "a", - ]), - List([ - "a", - ]) - ) - XCTAssertNotEqual( - List([ - "a", - ]), - List([ - "a", - "a", - ]) - ) + #expect(List(["a", "a"]) != List(["a"])) + #expect(List(["a"]) != List(["a", "a"])) // Test order matters - XCTAssertNotEqual( - List([ - "a", - "b", - ]), - List([ - "b", - "a", - ]) - ) + #expect(List(["a", "b"]) != List(["b", "a"])) // Test nested - XCTAssertEqual( + #expect( List([ true, "abc", @@ -96,18 +61,18 @@ final class ListTests: XCTestCase { false, "xyz", ]), - ]), - List([ - true, - "abc", - Number(42, unit: "furloghs"), + ]) == List([ - false, - "xyz", - ]), - ]) + true, + "abc", + Number(42, unit: "furloghs"), + List([ + false, + "xyz", + ]), + ]) ) - XCTAssertNotEqual( + #expect( List([ true, "abc", @@ -116,20 +81,20 @@ final class ListTests: XCTestCase { false, "xyz", ]), - ]), - List([ - true, - "abc", - Number(42, unit: "furloghs"), + ]) != List([ true, - "xyz", - ]), - ]) + "abc", + Number(42, unit: "furloghs"), + List([ + true, + "xyz", + ]), + ]) ) } - func testCollection() { + @Test func collection() { let list: List = [ true, "abc", @@ -138,18 +103,18 @@ final class ListTests: XCTestCase { ] // Test index access - XCTAssertEqual(list[0] as? Bool, true) - XCTAssertEqual(list[1] as? String, "abc") - XCTAssertEqual((list[2] as? Number), Number(42, unit: "furloghs")) - XCTAssertEqual((list[3] as? List), [true, "xyz"]) + #expect(list[0] as? Bool == true) + #expect(list[1] as? String == "abc") + #expect((list[2] as? Number) == Number(42, unit: "furloghs")) + #expect((list[3] as? List) == [true, "xyz"]) // Test loop for (i, element) in list.enumerated() { switch i { - case 0: XCTAssertEqual(element as? Bool, true) - case 1: XCTAssertEqual(element as? String, "abc") - case 2: XCTAssertEqual((element as? Number), Number(42, unit: "furloghs")) - case 3: XCTAssertEqual((element as? List), [true, "xyz"]) + case 0: #expect(element as? Bool == true) + case 1: #expect(element as? String == "abc") + case 2: #expect((element as? Number) == Number(42, unit: "furloghs")) + case 3: #expect((element as? List) == [true, "xyz"]) default: break } } diff --git a/Tests/HaystackTests/MarkerTests.swift b/Tests/HaystackTests/MarkerTests.swift index 2291a16..eca2e71 100644 --- a/Tests/HaystackTests/MarkerTests.swift +++ b/Tests/HaystackTests/MarkerTests.swift @@ -1,28 +1,20 @@ +import Foundation import Haystack -import XCTest +import Testing -final class MarkerTests: XCTestCase { - func testJsonCoding() throws { +struct MarkerTests { + @Test func jsonCoding() throws { let value = marker let jsonString = #"{"_kind":"marker"}"# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Marker.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Marker.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - marker.toZinc(), - "M" - ) + @Test func toZinc() throws { + #expect(marker.toZinc() == "M") } } diff --git a/Tests/HaystackTests/NATests.swift b/Tests/HaystackTests/NATests.swift index 434ec55..4d1d8c1 100644 --- a/Tests/HaystackTests/NATests.swift +++ b/Tests/HaystackTests/NATests.swift @@ -1,28 +1,20 @@ +import Foundation import Haystack -import XCTest +import Testing -final class NATests: XCTestCase { - func testJsonCoding() throws { +struct NATests { + @Test func jsonCoding() throws { let value = na let jsonString = #"{"_kind":"na"}"# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(NA.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(NA.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - na.toZinc(), - "NA" - ) + @Test func toZinc() throws { + #expect(na.toZinc() == "NA") } } diff --git a/Tests/HaystackTests/NullTests.swift b/Tests/HaystackTests/NullTests.swift index 6de8af8..9981daf 100644 --- a/Tests/HaystackTests/NullTests.swift +++ b/Tests/HaystackTests/NullTests.swift @@ -1,28 +1,20 @@ +import Foundation import Haystack -import XCTest +import Testing -final class NullTests: XCTestCase { - func testJsonCoding() throws { +struct NullTests { + @Test func jsonCoding() throws { let value = null let jsonString = #"null"# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Null.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Null.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - null.toZinc(), - "N" - ) + @Test func toZinc() throws { + #expect(null.toZinc() == "N") } } diff --git a/Tests/HaystackTests/NumberTests.swift b/Tests/HaystackTests/NumberTests.swift index a8bb549..4f15d49 100644 --- a/Tests/HaystackTests/NumberTests.swift +++ b/Tests/HaystackTests/NumberTests.swift @@ -1,95 +1,57 @@ +import Foundation import Haystack -import XCTest - -final class NumberTests: XCTestCase { - func testIsInt() throws { - XCTAssertTrue(Number(5).isInt) - XCTAssertFalse(Number(5.5).isInt) - XCTAssertTrue(Number(-1).isInt) - XCTAssertFalse(Number(-1.99999).isInt) +import Testing + +struct NumberTests { + @Test func isInt() throws { + #expect(Number(5).isInt) + #expect(!Number(5.5).isInt) + #expect(Number(-1).isInt) + #expect(!Number(-1.99999).isInt) } - func testJsonCoding() throws { + @Test func jsonCoding() throws { let value = Number(12.199, unit: "kWh") let jsonString = #"{"_kind":"number","val":12.199,"unit":"kWh"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Number.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Number.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Number.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Number.self, from: decodedData) == value) } - func testJsonCoding_noUnit() throws { + @Test func jsonCoding_noUnit() throws { let value = Number(3.899) let jsonString = #"3.899"# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Number.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Number.self, from: decodedData) == value) } - func testJsonCoding_infinity() throws { + @Test func jsonCoding_infinity() throws { let value = Number(.infinity) let jsonString = #"{"_kind":"number","val":"INF"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Number.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Number.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Number.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Number.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - Number(12.199, unit: "kWh").toZinc(), - "12.199kWh" - ) - XCTAssertEqual( - Number(1, unit: "kWh/ft\u{00b2}").toZinc(), - #"1kWh/ft\u00b2"# - ) - XCTAssertEqual( - Number(3.899).toZinc(), - "3.899" - ) - XCTAssertEqual( - Number(4).toZinc(), - "4" - ) - XCTAssertEqual( - Number.infinity.toZinc(), - "INF" - ) - XCTAssertEqual( - Number.negativeInfinity.toZinc(), - "-INF" - ) - XCTAssertEqual( - Number.nan.toZinc(), - "NaN" - ) + @Test func toZinc() throws { + #expect(Number(12.199, unit: "kWh").toZinc() == "12.199kWh") + #expect(Number(1, unit: "kWh/ft\u{00b2}").toZinc() == #"1kWh/ft\u00b2"#) + #expect(Number(3.899).toZinc() == "3.899") + #expect(Number(4).toZinc() == "4") + #expect(Number.infinity.toZinc() == "INF") + #expect(Number.negativeInfinity.toZinc() == "-INF") + #expect(Number.nan.toZinc() == "NaN") } } diff --git a/Tests/HaystackTests/RefTests.swift b/Tests/HaystackTests/RefTests.swift index 2f76336..7ae4e28 100644 --- a/Tests/HaystackTests/RefTests.swift +++ b/Tests/HaystackTests/RefTests.swift @@ -1,39 +1,28 @@ +import Foundation import Haystack -import XCTest +import Testing -final class RefTests: XCTestCase { - func testInit() throws { - try XCTAssertThrowsError(Ref("123 abc")) - try XCTAssertThrowsError(Ref("123$abc")) - try XCTAssertThrowsError(Ref("123%abc")) +struct RefTests { + @Test func testInit() throws { + #expect(throws: RefError.self) { try Ref("123 abc") } + #expect(throws: RefError.self) { try Ref("123$abc") } + #expect(throws: RefError.self) { try Ref("123%abc") } } - func testJsonCoding() throws { + @Test func jsonCoding() throws { let value = try Ref("123-abc", dis: "Name") let jsonString = #"{"_kind":"ref","val":"123-abc","dis":"Name"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Ref.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Ref.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Ref.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Ref.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - try Ref("123-abc", dis: "Name").toZinc(), - "@123-abc Name" - ) - XCTAssertEqual( - try Ref("123-abc").toZinc(), - "@123-abc" - ) + @Test func toZinc() throws { + #expect(try Ref("123-abc", dis: "Name").toZinc() == "@123-abc Name") + #expect(try Ref("123-abc").toZinc() == "@123-abc") } } diff --git a/Tests/HaystackTests/RemoveTests.swift b/Tests/HaystackTests/RemoveTests.swift index 0592310..564bd81 100644 --- a/Tests/HaystackTests/RemoveTests.swift +++ b/Tests/HaystackTests/RemoveTests.swift @@ -1,28 +1,20 @@ +import Foundation import Haystack -import XCTest +import Testing -final class RemoveTests: XCTestCase { - func testJsonCoding() throws { +struct RemoveTests { + @Test func jsonCoding() throws { let value = remove let jsonString = #"{"_kind":"remove"}"# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Remove.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Remove.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - remove.toZinc(), - "R" - ) + @Test func toZinc() throws { + #expect(remove.toZinc() == "R") } } diff --git a/Tests/HaystackTests/StringTests.swift b/Tests/HaystackTests/StringTests.swift index c0b1760..5c9dfb8 100644 --- a/Tests/HaystackTests/StringTests.swift +++ b/Tests/HaystackTests/StringTests.swift @@ -1,27 +1,22 @@ +import Foundation import Haystack -import XCTest +import Testing -final class StringTests: XCTestCase { - func testJsonCoding() throws { +struct StringTests { + @Test func jsonCoding() throws { let value = "hello" let jsonString = #""hello""# let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - String(data: encodedData, encoding: .utf8), - jsonString - ) + #expect(String(data: encodedData, encoding: .utf8) == jsonString) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(String.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(String.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual("hello".toZinc(), #""hello""#) - XCTAssertEqual("_ \\ \" \n \r \t \u{0011} _".toZinc(), #""_ \\ \" \n \r \t \u0011 _""#) - XCTAssertEqual("\u{0abc}".toZinc(), #""\u0abc""#) + @Test func toZinc() throws { + #expect("hello".toZinc() == #""hello""#) + #expect("_ \\ \" \n \r \t \u{0011} _".toZinc() == #""_ \\ \" \n \r \t \u0011 _""#) + #expect("\u{0abc}".toZinc() == #""\u0abc""#) } } diff --git a/Tests/HaystackTests/SymbolTests.swift b/Tests/HaystackTests/SymbolTests.swift index b2973fe..595c45f 100644 --- a/Tests/HaystackTests/SymbolTests.swift +++ b/Tests/HaystackTests/SymbolTests.swift @@ -1,35 +1,27 @@ +import Foundation import Haystack -import XCTest +import Testing -final class SymbolTests: XCTestCase { - func testInit() throws { - try XCTAssertThrowsError(Symbol("tag name")) - try XCTAssertThrowsError(Symbol("tag name")) - try XCTAssertThrowsError(Symbol("tag name")) +struct SymbolTests { + @Test func testInit() throws { + #expect(throws: SymbolError.self) { try Symbol("tag name") } + #expect(throws: SymbolError.self) { try Symbol("tag name") } + #expect(throws: SymbolError.self) { try Symbol("tag name") } } - func testJsonCoding() throws { + @Test func jsonCoding() throws { let value = try Symbol("tagName") let jsonString = #"{"_kind":"symbol","val":"tagName"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Symbol.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Symbol.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Symbol.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Symbol.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - try Symbol("tagName").toZinc(), - "^tagName" - ) + @Test func toZinc() throws { + #expect(try Symbol("tagName").toZinc() == "^tagName") } } diff --git a/Tests/HaystackTests/TimeTests.swift b/Tests/HaystackTests/TimeTests.swift index b5d9a0f..c96039b 100644 --- a/Tests/HaystackTests/TimeTests.swift +++ b/Tests/HaystackTests/TimeTests.swift @@ -1,51 +1,34 @@ +import Foundation import Haystack -import XCTest +import Testing -final class TimeTests: XCTestCase { - func testJsonCoding() throws { +struct TimeTests { + @Test func jsonCoding() throws { let value = try Time(hour: 7, minute: 7, second: 7, millisecond: 7) let jsonString = #"{"_kind":"time","val":"07:07:07.007"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Haystack.Time.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Haystack.Time.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Haystack.Time.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Haystack.Time.self, from: decodedData) == value) } - func testJsonCoding_zeroMillis() throws { + @Test func jsonCoding_zeroMillis() throws { let value = try Time(hour: 7, minute: 7, second: 7, millisecond: 0) let jsonString = #"{"_kind":"time","val":"07:07:07"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Haystack.Time.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Haystack.Time.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Haystack.Time.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Haystack.Time.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - try Time(hour: 7, minute: 7, second: 7, millisecond: 0).toZinc(), - "07:07:07" - ) - XCTAssertEqual( - try Time(hour: 7, minute: 7, second: 7, millisecond: 7).toZinc(), - "07:07:07.007" - ) + @Test func toZinc() throws { + #expect(try Time(hour: 7, minute: 7, second: 7, millisecond: 0).toZinc() == "07:07:07") + #expect(try Time(hour: 7, minute: 7, second: 7, millisecond: 7).toZinc() == "07:07:07.007") } } diff --git a/Tests/HaystackTests/UriTests.swift b/Tests/HaystackTests/UriTests.swift index 2d761b4..b23040a 100644 --- a/Tests/HaystackTests/UriTests.swift +++ b/Tests/HaystackTests/UriTests.swift @@ -1,29 +1,21 @@ +import Foundation import Haystack -import XCTest +import Testing -final class UriTests: XCTestCase { - func testJsonCoding() throws { +struct UriTests { + @Test func jsonCoding() throws { let value = Uri("http://en.wikipedia.org/") let jsonString = #"{"_kind":"uri","val":"http:\/\/en.wikipedia.org\/"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(Uri.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(Uri.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(Uri.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(Uri.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - Uri("http://en.wikipedia.org/").toZinc(), - "`http://en.wikipedia.org/`" - ) + @Test func toZinc() throws { + #expect(Uri("http://en.wikipedia.org/").toZinc() == "`http://en.wikipedia.org/`") } } diff --git a/Tests/HaystackTests/XStrTests.swift b/Tests/HaystackTests/XStrTests.swift index c1b1f1e..359ac03 100644 --- a/Tests/HaystackTests/XStrTests.swift +++ b/Tests/HaystackTests/XStrTests.swift @@ -1,35 +1,27 @@ +import Foundation import Haystack -import XCTest +import Testing -final class XStrTests: XCTestCase { - func testInit() throws { - try XCTAssertThrowsError(XStr(type: "span", val: "today")) - try XCTAssertThrowsError(XStr(type: "Span range", val: "today")) - try XCTAssertThrowsError(XStr(type: "Span!", val: "today")) +struct XStrTests { + @Test func testInit() throws { + #expect(throws: (any Error).self) { try XStr(type: "span", val: "today") } + #expect(throws: (any Error).self) { try XStr(type: "Span range", val: "today") } + #expect(throws: (any Error).self) { try XStr(type: "Span!", val: "today") } } - func testJsonCoding() throws { + @Test func jsonCoding() throws { let value = try XStr(type: "Span", val: "today") let jsonString = #"{"_kind":"xstr","type":"Span","val":"today"}"# // Must encode/decode b/c JSON ordering is not deterministic let encodedData = try JSONEncoder().encode(value) - XCTAssertEqual( - try JSONDecoder().decode(XStr.self, from: encodedData), - value - ) + #expect(try JSONDecoder().decode(XStr.self, from: encodedData) == value) - let decodedData = try XCTUnwrap(jsonString.data(using: .utf8)) - XCTAssertEqual( - try JSONDecoder().decode(XStr.self, from: decodedData), - value - ) + let decodedData = try #require(jsonString.data(using: .utf8)) + #expect(try JSONDecoder().decode(XStr.self, from: decodedData) == value) } - func testToZinc() throws { - XCTAssertEqual( - try XStr(type: "Span", val: "today").toZinc(), - #"Span("today")"# - ) + @Test func toZinc() throws { + #expect(try XStr(type: "Span", val: "today").toZinc() == #"Span("today")"#) } }