diff --git a/Alchemy/AlchemyX/Application+AlchemyX.swift b/Alchemy/AlchemyX/Application+AlchemyX.swift index 84e18890..91a94328 100644 --- a/Alchemy/AlchemyX/Application+AlchemyX.swift +++ b/Alchemy/AlchemyX/Application+AlchemyX.swift @@ -17,7 +17,7 @@ extension Application { private struct AuthController: Controller, AuthAPI { func route(_ router: Router) { - registerHandlers(on: router) + // registerHandlers(on: router) } func signUp(email: String, password: String) async throws -> AuthResponse { diff --git a/Alchemy/Application/Application.swift b/Alchemy/Application/Application.swift index b1a6c7d1..89b87840 100644 --- a/Alchemy/Application/Application.swift +++ b/Alchemy/Application/Application.swift @@ -2,15 +2,16 @@ import HummingbirdCore /// The core type for an Alchemy application. /// -/// @Application -/// struct App { -/// -/// @GET("/hello") -/// func sayHello(name: String) -> String { -/// "Hello, \(name)!" -/// } +/// ```swift +/// @Application +/// struct App { +/// +/// @GET("/hello") +/// func hello(name: String) -> String { +/// "Hello, \(name)!" /// } -/// +/// } +/// ``` public protocol Application: Router { /// The container in which all services of this application are registered. var container: Container { get } @@ -69,7 +70,7 @@ public extension Application { // MARK: Running extension Application { - // @main support + /// @main support public static func main() async throws { let app = Self() do { @@ -83,13 +84,18 @@ extension Application { /// Runs the application with the given arguments. public func run(_ args: String...) async throws { - try await lifecycle.start(args: args.isEmpty ? nil : args) + try await run(args) + } + + /// Runs the application with the given arguments. + public func run(_ args: [String]) async throws { + try await commander.run(args: args.isEmpty ? nil : args) } /// Sets up the app for running. public func willRun() async throws { let lifecycle = Lifecycle(app: self) - try await lifecycle.start() + try await lifecycle.boot() (self as? Controller)?.route(self) try boot() } @@ -100,7 +106,7 @@ extension Application { try await lifecycle.shutdown() } - /// Stops the application. + /// Stops a currently running application. public func stop() async { await lifecycle.stop() } diff --git a/Alchemy/Application/Lifecycle.swift b/Alchemy/Application/Lifecycle.swift index f0edb70c..af253458 100644 --- a/Alchemy/Application/Lifecycle.swift +++ b/Alchemy/Application/Lifecycle.swift @@ -4,17 +4,15 @@ import ServiceLifecycle /// Manages the startup and shutdown of an Application as well as it's various /// services and configurations. public final class Lifecycle { - typealias Action = () async throws -> Void + public typealias Action = () async throws -> Void - fileprivate var startTasks: [Action] = [] - fileprivate var shutdownTasks: [Action] = [] - - let app: Application - let plugins: [Plugin] - - private var group: ServiceGroup? - private var services: [Service] = [] + private let app: Application + private let plugins: [Plugin] private let lock = NIOLock() + private var startTasks: [Action] = [] + private var shutdownTasks: [Action] = [] + private var services: [Service] = [] + private var group: ServiceGroup? = nil init(app: Application) { self.app = app @@ -31,7 +29,24 @@ public final class Lifecycle { ] + app.plugins } - public func start() async throws { + public func runServices() async throws { + group = ServiceGroup( + configuration: ServiceGroupConfiguration( + services: services.map { + .init( + service: $0, + successTerminationBehavior: .gracefullyShutdownGroup, + failureTerminationBehavior: .gracefullyShutdownGroup + ) + }, + gracefulShutdownSignals: [.sigterm, .sigint], + logger: Log + ) + ) + try await group?.run() + } + + public func boot() async throws { app.container.register(self).singleton() for plugin in plugins { @@ -57,42 +72,21 @@ public final class Lifecycle { } } - public func onStart(action: @escaping () async throws -> Void) { + public func stop() async { + await group?.triggerGracefulShutdown() + } + + public func onStart(action: @escaping Action) { lock.withLock { startTasks.append(action) } } - public func onShutdown(action: @escaping () async throws -> Void) { + public func onShutdown(action: @escaping Action) { lock.withLock { shutdownTasks.append(action) } } public func addService(_ service: Service) { lock.withLock { services.append(service) } } - - public func start(args: [String]? = nil) async throws { - try await Container.require(Commander.self).runCommand(args: args) - } - - public func runServices() async throws { - group = ServiceGroup( - configuration: ServiceGroupConfiguration( - services: services.map { - .init( - service: $0, - successTerminationBehavior: .gracefullyShutdownGroup, - failureTerminationBehavior: .gracefullyShutdownGroup - ) - }, - gracefulShutdownSignals: [.sigterm, .sigint], - logger: Log - ) - ) - try await group?.run() - } - - public func stop() async { - await group?.triggerGracefulShutdown() - } } extension Application { diff --git a/Alchemy/Command/Commander.swift b/Alchemy/Command/Commander.swift index d96e93e7..0f0b76d9 100644 --- a/Alchemy/Command/Commander.swift +++ b/Alchemy/Command/Commander.swift @@ -22,21 +22,11 @@ final class Commander { private var commands: [Command.Type] = [] private var defaultCommand: Command.Type = ServeCommand.self - // MARK: Registering Commands - - func register(command: (some Command).Type) { - commands.append(command) - } - - func setDefault(command: (some Command).Type) { - defaultCommand = command - } - // MARK: Running Commands /// Runs a command based on the given arguments. Returns the command that /// ran, after it is finished running. - func runCommand(args: [String]? = nil) async throws { + func run(args: [String]? = nil) async throws { // When running a command with no arguments during a test, send an empty // array of arguments to swift-argument-parser. Otherwise, it will @@ -52,6 +42,16 @@ final class Commander { func exit(error: Error) { Launch.exit(withError: error) } + + // MARK: Registering Commands + + func register(command: (some Command).Type) { + commands.append(command) + } + + func setDefault(command: (some Command).Type) { + defaultCommand = command + } } extension Logger.Level: ExpressibleByArgument {} diff --git a/Alchemy/Database/Providers/MySQL/MySQLDatabaseProvider.swift b/Alchemy/Database/Providers/MySQL/MySQLDatabaseProvider.swift index ff6222bd..d2ae4e86 100644 --- a/Alchemy/Database/Providers/MySQL/MySQLDatabaseProvider.swift +++ b/Alchemy/Database/Providers/MySQL/MySQLDatabaseProvider.swift @@ -1,6 +1,6 @@ import AsyncKit -import NIOSSL import MySQLNIO +import NIOSSL public final class MySQLDatabaseProvider: DatabaseProvider { public var type: DatabaseType { .mysql } @@ -14,20 +14,22 @@ public final class MySQLDatabaseProvider: DatabaseProvider { } // MARK: Database - + public func query(_ sql: String, parameters: [SQLValue]) async throws -> [SQLRow] { try await withConnection { try await $0.query(sql, parameters: parameters) } } - + public func raw(_ sql: String) async throws -> [SQLRow] { try await withConnection { try await $0.raw(sql) } } - - public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + + public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await withConnection { try await $0.mysqlTransaction(action) } @@ -37,7 +39,9 @@ public final class MySQLDatabaseProvider: DatabaseProvider { try await pool.asyncShutdownGracefully() } - private func withConnection(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + private func withConnection(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await pool.withConnection(logger: Log, on: Loop) { try await action($0) } @@ -59,7 +63,9 @@ extension MySQLConnection: DatabaseProvider, ConnectionPoolItem { } @discardableResult - public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await mysqlTransaction(action) } @@ -69,7 +75,9 @@ extension MySQLConnection: DatabaseProvider, ConnectionPoolItem { } extension DatabaseProvider { - fileprivate func mysqlTransaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + fileprivate func mysqlTransaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await raw("START TRANSACTION;") do { let val = try await action(self) @@ -85,12 +93,13 @@ extension DatabaseProvider { extension MySQLRow { fileprivate var _row: SQLRow { - SQLRow(fields: columnDefinitions.map { - guard let value = column($0.name) else { - preconditionFailure("MySQLRow had a key but no value for column `\($0.name)`!") - } + SQLRow( + fields: columnDefinitions.map { + guard let value = column($0.name) else { + preconditionFailure("MySQLRow had a key but no value for column `\($0.name)`!") + } - return (column: $0.name, value: value) - }) + return (column: $0.name, value: value) + }) } } diff --git a/Alchemy/Database/Providers/Postgres/PostgresDatabaseProvider.swift b/Alchemy/Database/Providers/Postgres/PostgresDatabaseProvider.swift index 320124a6..ec94e694 100644 --- a/Alchemy/Database/Providers/Postgres/PostgresDatabaseProvider.swift +++ b/Alchemy/Database/Providers/Postgres/PostgresDatabaseProvider.swift @@ -19,30 +19,34 @@ public final class PostgresDatabaseProvider: DatabaseProvider { } // MARK: Database - + public func query(_ sql: String, parameters: [SQLValue]) async throws -> [SQLRow] { try await withConnection { try await $0.query(sql, parameters: parameters) } } - + public func raw(_ sql: String) async throws -> [SQLRow] { try await withConnection { try await $0.raw(sql) } } - - public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + + public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await withConnection { try await $0.postgresTransaction(action) } } - + public func shutdown() async throws { try await pool.asyncShutdownGracefully() } - - private func withConnection(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + + private func withConnection(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await pool.withConnection(logger: Log, on: Loop) { try await action($0) } @@ -51,7 +55,7 @@ public final class PostgresDatabaseProvider: DatabaseProvider { extension PostgresConnection: DatabaseProvider, ConnectionPoolItem { public var type: DatabaseType { .postgres } - + @discardableResult public func query(_ sql: String, parameters: [SQLValue]) async throws -> [SQLRow] { let statement = sql.positionPostgresBinds() @@ -70,17 +74,21 @@ extension PostgresConnection: DatabaseProvider, ConnectionPoolItem { } @discardableResult - public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await action(self) } - + public func shutdown() async throws { try await close().get() } } extension DatabaseProvider { - fileprivate func postgresTransaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + fileprivate func postgresTransaction( + _ action: @escaping (DatabaseProvider) async throws -> T + ) async throws -> T { try await raw("START TRANSACTION;") do { let val = try await action(self) @@ -109,10 +117,11 @@ extension String { // TODO: Move this to SQLGrammar replaceAll(matching: "(\\?)") { (index, _) in "$\(index + 1)" } } - + private func replaceAll(matching pattern: String, callback: (Int, String) -> String) -> String { let expression = try! NSRegularExpression(pattern: pattern, options: []) - let matches = expression + let matches = + expression .matches(in: self, options: [], range: NSRange(startIndex.. [SQLRow] { try await withConnection { try await $0.raw(sql) } } - - public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + + public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await withConnection { try await $0.sqliteTransaction(action) } } - + public func shutdown() async throws { try await pool.asyncShutdownGracefully() } - - private func withConnection(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + + private func withConnection(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await pool.withConnection(logger: Log, on: Loop) { try await action($0) } @@ -55,7 +59,9 @@ extension SQLiteConnection: DatabaseProvider, ConnectionPoolItem { try await query(sql).get().map(\._row) } - public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + public func transaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await sqliteTransaction(action) } @@ -65,7 +71,9 @@ extension SQLiteConnection: DatabaseProvider, ConnectionPoolItem { } extension DatabaseProvider { - fileprivate func sqliteTransaction(_ action: @escaping (DatabaseProvider) async throws -> T) async throws -> T { + fileprivate func sqliteTransaction(_ action: @escaping (DatabaseProvider) async throws -> T) + async throws -> T + { try await raw("BEGIN;") do { let val = try await action(self) diff --git a/Alchemy/HTTP/Client.swift b/Alchemy/HTTP/Client.swift index 5d751aea..5c7d662b 100644 --- a/Alchemy/HTTP/Client.swift +++ b/Alchemy/HTTP/Client.swift @@ -296,14 +296,21 @@ public final class Client: IdentifiedService { } else { let deadline: NIODeadline? = req.timeout.map { .now() + $0 } let httpClientOverride = req.config.map { HTTPClient(eventLoopGroupProvider: .shared(httpClient.eventLoopGroup), configuration: $0) } - defer { try? httpClientOverride?.syncShutdown() } - let _request = try req._request - let loop = LoopGroup.next() - let promise = loop.makePromise(of: Response.self) - let delegate = ResponseDelegate(request: req, promise: promise, allowStreaming: req.streamResponse) - let client = httpClientOverride ?? httpClient - _ = client.execute(request: _request, delegate: delegate, eventLoop: .delegateAndChannel(on: loop), deadline: deadline, logger: Log) - return try await promise.futureResult.get() + + do { + let _request = try req._request + let loop = LoopGroup.next() + let promise = loop.makePromise(of: Response.self) + let delegate = ResponseDelegate(request: req, promise: promise, allowStreaming: req.streamResponse) + let client = httpClientOverride ?? httpClient + _ = client.execute(request: _request, delegate: delegate, eventLoop: .delegateAndChannel(on: loop), deadline: deadline, logger: Log) + let response = try await promise.futureResult.get() + try await httpClientOverride?.shutdown() + return response + } catch { + try await httpClientOverride?.shutdown() + throw error + } } } } diff --git a/Alchemy/HTTP/Serving/Application+HTTPListener.swift b/Alchemy/HTTP/Serving/Application+HTTPListener.swift index 91e4e4a5..26a9f1d0 100644 --- a/Alchemy/HTTP/Serving/Application+HTTPListener.swift +++ b/Alchemy/HTTP/Serving/Application+HTTPListener.swift @@ -29,7 +29,7 @@ extension Application { }(), serverName: nil, backlog: 256, - reuseAddress: false + reuseAddress: true ), onServerRunning: { onServerStart(channel: $0, address: address) }, eventLoopGroupProvider: .shared(LoopGroup), diff --git a/Alchemy/Queue/Commands/WorkCommand.swift b/Alchemy/Queue/Commands/WorkCommand.swift index 2a33f283..8ac740b5 100644 --- a/Alchemy/Queue/Commands/WorkCommand.swift +++ b/Alchemy/Queue/Commands/WorkCommand.swift @@ -19,17 +19,14 @@ struct WorkCommand: Command { // MARK: Command func run() async throws { - let queue: Queue = name.map { Container.require(id: $0) } ?? Q - for _ in 0.. Self { XCTAssertEqual(status, .created, file: file, line: line) return self } - + @discardableResult public func assertForbidden(file: StaticString = #filePath, line: UInt = #line) -> Self { XCTAssertEqual(status, .forbidden, file: file, line: line) return self } - + @discardableResult public func assertNotFound(file: StaticString = #filePath, line: UInt = #line) -> Self { XCTAssertEqual(status, .notFound, file: file, line: line) return self } - + @discardableResult public func assertOk(file: StaticString = #filePath, line: UInt = #line) -> Self { XCTAssertEqual(status, .ok, file: file, line: line) return self } - + @discardableResult - public func assertRedirect(to uri: String? = nil, file: StaticString = #filePath, line: UInt = #line) -> Self { + public func assertRedirect( + to uri: String? = nil, file: StaticString = #filePath, line: UInt = #line + ) -> Self { XCTAssertTrue((300...399).contains(status.code), file: file, line: line) - + if let uri = uri { assertLocation(uri, file: file, line: line) } - + return self } - + @discardableResult - public func assertStatus(_ status: HTTPResponse.Status, file: StaticString = #filePath, line: UInt = #line) -> Self { + public func assertStatus( + _ status: HTTPResponse.Status, file: StaticString = #filePath, line: UInt = #line + ) -> Self { XCTAssertEqual(self.status, status, file: file, line: line) return self } - + @discardableResult - public func assertStatus(_ code: Int, file: StaticString = #filePath, line: UInt = #line) -> Self { + public func assertStatus(_ code: Int, file: StaticString = #filePath, line: UInt = #line) + -> Self + { XCTAssertEqual(status.code, code, file: file, line: line) return self } - + @discardableResult public func assertSuccessful(file: StaticString = #filePath, line: UInt = #line) -> Self { XCTAssertTrue((200...299).contains(status.code), file: file, line: line) return self } - + @discardableResult public func assertUnauthorized(file: StaticString = #filePath, line: UInt = #line) -> Self { XCTAssertEqual(status, .unauthorized, file: file, line: line) diff --git a/AlchemyTest/Utilities/ByteBuffer+ExpressibleByStringLiteral.swift b/AlchemyTest/Utilities/ByteBuffer+ExpressibleByStringLiteral.swift deleted file mode 100644 index db55e42d..00000000 --- a/AlchemyTest/Utilities/ByteBuffer+ExpressibleByStringLiteral.swift +++ /dev/null @@ -1,5 +0,0 @@ -extension ByteBuffer: ExpressibleByStringLiteral { - public init(stringLiteral value: StringLiteralType) { - self.init(string: value) - } -} diff --git a/Tests/Encryption/EncryptionTests.swift b/Tests/Encryption/EncryptionTests.swift index 24e8e464..ee208bf2 100644 --- a/Tests/Encryption/EncryptionTests.swift +++ b/Tests/Encryption/EncryptionTests.swift @@ -1,18 +1,19 @@ -@testable import Alchemy import AlchemyTest import Crypto +@testable import Alchemy + final class EncryptionTests: XCTestCase { func testEncrypter() throws { let initialKey = SymmetricKey(size: .bits256) let initialEncryptor = Encrypter(key: initialKey) let initialCipher = try initialEncryptor.encrypt(string: "FOO") - + let keyString = initialKey.withUnsafeBytes { Data($0) }.base64EncodedString() guard let keyData = Data(base64Encoded: keyString) else { return XCTFail("couldn't decode") } - + let recreatedKey = SymmetricKey(data: keyData) let encrypter = Encrypter(key: recreatedKey) let cipher = try encrypter.encrypt(string: "FOO") @@ -21,7 +22,7 @@ final class EncryptionTests: XCTestCase { XCTAssertEqual("FOO", decrypted) XCTAssertEqual("FOO", initialDecrypted) } - + func testDecryptStringNotBase64Throws() { let key = SymmetricKey(size: .bits256) let encrypter = Encrypter(key: key) @@ -46,7 +47,7 @@ final class EncryptionTests: XCTestCase { let decrypted = try Crypt.decrypt(base64Encoded: storedValue) XCTAssertEqual(decrypted, string) } - + func testEncryptedNotBase64Throws() { let reader: SQLRowReader = ["foo": "bar"] XCTAssertThrowsError(try Encrypted(key: "foo", on: reader)) diff --git a/Tests/Filesystem/FileTests.swift b/Tests/Filesystem/FileTests.swift index b2ed904d..c64558ca 100644 --- a/Tests/Filesystem/FileTests.swift +++ b/Tests/Filesystem/FileTests.swift @@ -4,14 +4,14 @@ import AlchemyTest final class FileTests: XCTestCase { func testFile() { - let file = File(name: "foo.html", source: .raw, content: .buffer("

foo

"), size: 10) + let file = File(name: "foo.html", source: .raw, content: "

foo

", size: 10) XCTAssertEqual(file.extension, "html") XCTAssertEqual(file.size, 10) XCTAssertEqual(file.contentType, .html) } func testInvalidURL() { - let file = File(name: "", source: .raw, content: .buffer("foo"), size: 3) + let file = File(name: "", source: .raw, content: "foo", size: 3) XCTAssertEqual(file.extension, "") } } diff --git a/Tests/Filesystem/FilesystemTests.swift b/Tests/Filesystem/FilesystemTests.swift index ac659c98..df9c4926 100644 --- a/Tests/Filesystem/FilesystemTests.swift +++ b/Tests/Filesystem/FilesystemTests.swift @@ -36,7 +36,7 @@ final class FilesystemTests: TestCase { AssertTrue(try await Storage.exists(filePath)) let file = try await Storage.get(filePath) AssertEqual(file.name, filePath) - AssertEqual(try await file.getContent().collect(), "1;2;3") + AssertEqual(try await file.getContent().collect(), ByteBuffer(string: "1;2;3")) } @@ -64,7 +64,7 @@ final class FilesystemTests: TestCase { AssertTrue(try await Storage.exists("foo/bar/baz/\(filePath)")) let file = try await Storage.get("foo/bar/baz/\(filePath)") AssertEqual(file.name, filePath) - AssertEqual(try await file.getContent().collect(), "foo") + AssertEqual(try await file.getContent().collect(), ByteBuffer(string: "foo")) try await Storage.delete("foo/bar/baz/\(filePath)") AssertFalse(try await Storage.exists("foo/bar/baz/\(filePath)")) } diff --git a/Tests/HTTP/ClientTests.swift b/Tests/HTTP/ClientTests.swift index 088c19dc..d0aed21e 100644 --- a/Tests/HTTP/ClientTests.swift +++ b/Tests/HTTP/ClientTests.swift @@ -61,9 +61,9 @@ final class ClientTests: TestCase { func testStreaming() async throws { let streamResponse: Client.Response = .stub(body: .stream { - $0.write("foo") - $0.write("bar") - $0.write("baz") + $0.write(ByteBuffer(string: "foo")) + $0.write(ByteBuffer(string: "bar")) + $0.write(ByteBuffer(string: "baz")) }) Http.stub(["example.com/*": streamResponse]) diff --git a/Tests/HTTP/Commands/ServeCommandTests.swift b/Tests/HTTP/Commands/ServeCommandTests.swift index 15256398..b39781b4 100644 --- a/Tests/HTTP/Commands/ServeCommandTests.swift +++ b/Tests/HTTP/Commands/ServeCommandTests.swift @@ -11,7 +11,7 @@ final class ServeCommandTests: TestCase { func testServe() async throws { app.get("/foo", use: { _ in "hello" }) - Task { try await ServeCommand.parse(["--port", "3000"]).run() } + Task { try await app.run("--port", "3000") } try await Http.get("http://127.0.0.1:3000/foo") .assertBody("hello") @@ -21,7 +21,7 @@ final class ServeCommandTests: TestCase { func testServeWithSideEffects() async throws { app.get("/foo", use: { _ in "hello" }) - Task { try await ServeCommand.parse(["--workers", "2", "--schedule", "--migrate"]).run() } + Task { try await app.run("--workers", "2", "--schedule", "--migrate") } try await Http.get("http://127.0.0.1:3000/foo") .assertBody("hello") diff --git a/Tests/HTTP/Content/ContentTests.swift b/Tests/HTTP/Content/ContentTests.swift index 37e1290f..5b93adcd 100644 --- a/Tests/HTTP/Content/ContentTests.swift +++ b/Tests/HTTP/Content/ContentTests.swift @@ -1,9 +1,9 @@ -@testable -import Alchemy import AlchemyTest import Hummingbird import MultipartKit +@testable import Alchemy + final class ContentTests: XCTestCase { private lazy var allTests = [ _testAccess, @@ -12,7 +12,7 @@ final class ContentTests: XCTestCase { _testFlatten, _testDecode, ] - + func testDict() throws { let content = Content(value: .dictionary(Fixtures.dictContent)) for test in allTests { @@ -21,14 +21,15 @@ final class ContentTests: XCTestCase { try _testNestedArray(content: content) try _testNestedDecode(content: content) } - + func testMultipart() throws { let buffer = ByteBuffer(string: Fixtures.multipartContent) - let content = FormDataDecoder().content(from: buffer, contentType: .multipart(boundary: Fixtures.multipartBoundary)) + let content = FormDataDecoder().content( + from: buffer, contentType: .multipart(boundary: Fixtures.multipartBoundary)) try _testAccess(content: content, allowsNull: false) try _testMultipart(content: content) } - + func testJson() throws { let buffer = ByteBuffer(string: Fixtures.jsonContent) let content = JSONDecoder().content(from: buffer, contentType: .json) @@ -38,7 +39,7 @@ final class ContentTests: XCTestCase { try _testNestedArray(content: content) try _testNestedDecode(content: content) } - + func testUrl() throws { let buffer = ByteBuffer(string: Fixtures.urlContent) let content = URLEncodedFormDecoder().content(from: buffer, contentType: .urlForm) @@ -47,7 +48,7 @@ final class ContentTests: XCTestCase { } try _testNestedDecode(content: content) } - + func _testAccess(content: Content, allowsNull: Bool) throws { AssertTrue(content["foo"] == nil) AssertEqual(try content["string"].stringThrowing, "string") @@ -56,7 +57,7 @@ final class ContentTests: XCTestCase { AssertEqual(try content["bool"].boolThrowing, true) AssertEqual(try content["double"].doubleThrowing, 1.23) } - + func _testNestedAccess(content: Content, allowsNull: Bool) throws { AssertTrue(content.object.four.isNull) XCTAssertThrowsError(try content["array"].stringThrowing) @@ -69,31 +70,31 @@ final class ContentTests: XCTestCase { AssertEqual(try content["object"]["two"].stringThrowing, "two") AssertEqual(try content["object"]["three"].stringThrowing, "three") } - + func _testEnumAccess(content: Content, allowsNull: Bool) throws { enum Test: String, Decodable { case one, two, three } - + var expectedDict: [String: Test?] = ["one": .one, "two": .two, "three": .three] if allowsNull { expectedDict = ["one": .one, "two": .two, "three": .three, "four": nil] } - + AssertEqual(try content.object.one.decode(Test?.self), .one) AssertEqual(try content.object.decode([String: Test?].self), expectedDict) } - + func _testMultipart(content: Content) throws { let file = try content["file"].fileThrowing AssertEqual(file.name, "a.txt") AssertEqual(file.content?.buffer.string, "Content of a.txt.\n") } - + func _testFlatten(content: Content, allowsNull: Bool) throws { var expectedArray: [String?] = ["one", "three", "two"] if allowsNull { expectedArray.append(nil) } AssertEqual(try content["object"][*].decodeEach(String?.self).sorted(), expectedArray) } - + func _testDecode(content: Content, allowsNull: Bool) throws { struct TopLevelType: Codable, Equatable { var string: String = "string" @@ -101,49 +102,49 @@ final class ContentTests: XCTestCase { var bool: Bool = true var double: Double = 1.23 } - + AssertEqual(try content.decode(TopLevelType.self), TopLevelType()) } - + func _testNestedDecode(content: Content) throws { struct NestedType: Codable, Equatable { let one: String let two: String let three: String } - + let expectedStruct = NestedType(one: "one", two: "two", three: "three") AssertEqual(try content["object"].decode(NestedType.self), expectedStruct) AssertEqual(try content["array"].decode([Int].self), [1, 2, 3]) AssertEqual(try content["array"].decode([Int8].self), [1, 2, 3]) } - + func _test(content: Content, allowsNull: Bool) throws { struct DecodableType: Codable, Equatable { let one: String let two: String let three: String } - + struct TopLevelType: Codable, Equatable { var string: String = "string" var int: Int = 0 var bool: Bool = false var double: Double = 1.23 } - + let expectedStruct = DecodableType(one: "one", two: "two", three: "three") AssertEqual(try content.decode(TopLevelType.self), TopLevelType()) AssertEqual(try content["object"].decode(DecodableType.self), expectedStruct) AssertEqual(try content["array"].decode([Int].self), [1, 2, 3]) AssertEqual(try content["array"].decode([Int8].self), [1, 2, 3]) } - + func _testNestedArray(content: Content) throws { struct ArrayType: Codable, Equatable { let foo: String } - + AssertEqual(try content["objectArray"][*]["foo"].stringThrowing, ["bar", "baz", "tiz"]) let expectedArray = [ArrayType(foo: "bar"), ArrayType(foo: "baz"), ArrayType(foo: "tiz")] AssertEqual(try content.objectArray.decode([ArrayType].self), expectedArray) @@ -159,13 +160,13 @@ private struct Fixtures { "array": [ 1, 2, - 3 + 3, ], "object": [ "one": "one", "two": "two", "three": "three", - "four": nil + "four": nil, ], "objectArray": [ [ @@ -176,13 +177,13 @@ private struct Fixtures { ], [ "foo": "tiz" - ] - ] + ], + ], ] - + static let multipartBoundary = "---------------------------9051914041544843365972754266" static let multipartContent = """ - + -----------------------------9051914041544843365972754266\r Content-Disposition: form-data; name="string"\r \r @@ -206,43 +207,43 @@ private struct Fixtures { Content of a.txt. \r -----------------------------9051914041544843365972754266--\r - + """ - + static let jsonContent = """ - { - "string": "string", - "int": 0, - "bool": true, - "double": 1.23, - "array": [ - 1, - 2, - 3 - ], - "object": { - "one": "one", - "two": "two", - "three": "three", - "four": null - }, - "objectArray": [ - { - "foo": "bar" - }, - { - "foo": "baz" + { + "string": "string", + "int": 0, + "bool": true, + "double": 1.23, + "array": [ + 1, + 2, + 3 + ], + "object": { + "one": "one", + "two": "two", + "three": "three", + "four": null }, - { - "foo": "tiz" - } - ] - } - """ - + "objectArray": [ + { + "foo": "bar" + }, + { + "foo": "baz" + }, + { + "foo": "tiz" + } + ] + } + """ + static let urlContent = """ - string=string&int=0&bool=true&double=1.23&array[]=1&array[]=2&array[]=3&object[one]=one&object[two]=two&object[three]=three - """ + string=string&int=0&bool=true&double=1.23&array[]=1&array[]=2&array[]=3&object[one]=one&object[two]=two&object[three]=three + """ } extension Optional: Comparable where Wrapped == String { diff --git a/Tests/Queues/Commands/WorkCommandTests.swift b/Tests/Queues/Commands/WorkCommandTests.swift index fd789168..64a1ee28 100644 --- a/Tests/Queues/Commands/WorkCommandTests.swift +++ b/Tests/Queues/Commands/WorkCommandTests.swift @@ -9,11 +9,10 @@ final class WorkCommandTests: TestCase { } func testRun() async throws { - Task { try await WorkCommand.parse(["--workers", "5"]).run() } + Task { try await app.run("queue:work", "--workers", "5") } - // hack to wait for the queue to boot up - should find a way to hook - // into the command finishing. - try await Task.sleep(for: .milliseconds(100)) + // wait for services to boot up + try await Task.sleep(for: .milliseconds(10)) XCTAssertEqual(Q.workers.count, 5) XCTAssertFalse(Schedule.isStarted) @@ -21,11 +20,10 @@ final class WorkCommandTests: TestCase { func testRunName() async throws { Queue.fake("a") - Task { try await WorkCommand.parse(["--name", "a", "--workers", "5"]).run() } + Task { try await app.run("queue:work", "--name", "a", "--workers", "5") } - // hack to wait for the queue to boot up - should find a way to hook - // into the command finishing. - try await Task.sleep(for: .milliseconds(100)) + // wait for services to boot up + try await Task.sleep(for: .milliseconds(10)) XCTAssertEqual(Q.workers.count, 0) XCTAssertEqual(Q("a").workers.count, 5) @@ -35,9 +33,8 @@ final class WorkCommandTests: TestCase { func testRunCLI() async throws { Task { try await app.run("queue:work", "--workers", "3", "--schedule") } - // hack to wait for the queue to boot up - should find a way to hook - // into the command finishing. - try await Task.sleep(for: .milliseconds(100)) + // wait for services to boot up + try await Task.sleep(for: .milliseconds(10)) XCTAssertEqual(Q.workers.count, 3) XCTAssertTrue(Schedule.isStarted) diff --git a/Tests/Routing/RouterTests.swift b/Tests/Routing/RouterTests.swift index 1d960122..20279c41 100644 --- a/Tests/Routing/RouterTests.swift +++ b/Tests/Routing/RouterTests.swift @@ -156,9 +156,9 @@ final class RouterTests: TestCase { func testServerResponseStream() async throws { app.get("/stream") { _ in Response { - $0.write("foo") - $0.write("bar") - $0.write("baz") + $0.write(ByteBuffer(string: "foo")) + $0.write(ByteBuffer(string: "bar")) + $0.write(ByteBuffer(string: "baz")) } } @@ -171,9 +171,9 @@ final class RouterTests: TestCase { func testEndToEndStream() async throws { app.get("/stream", options: .stream) { _ in Response { - $0.write("foo") - $0.write("bar") - $0.write("baz") + $0.write(ByteBuffer(string: "foo")) + $0.write(ByteBuffer(string: "bar")) + $0.write(ByteBuffer(string: "baz")) } } @@ -197,9 +197,9 @@ final class RouterTests: TestCase { func testFileRequest() { app.get("/stream") { _ in Response { - $0.write("foo") - $0.write("bar") - $0.write("baz") + $0.write(ByteBuffer(string: "foo")) + $0.write(ByteBuffer(string: "bar")) + $0.write(ByteBuffer(string: "baz")) } } }