Skip to content

Commit

Permalink
Pass Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawright11 committed Jul 15, 2024
1 parent 12e73e7 commit 5b02277
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 79 deletions.
2 changes: 1 addition & 1 deletion Alchemy/AlchemyX/Router+Resource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension Application {
) -> Self where R.Identifier: SQLValueConvertible & LosslessStringConvertible {
use(ResourceController<R>(db: db, tableName: table))
if updateTable {
Container.main.services.append(ResourceMigrationService<R>(db: db))
Container.main.lifecycleServices.append(ResourceMigrationService<R>(db: db))
}

return self
Expand Down
62 changes: 47 additions & 15 deletions Alchemy/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,20 @@ public extension Application {
]

let allPlugins = alchemyPlugins + plugins

for plugin in alchemyPlugins + plugins {
plugin.registerServices(in: self)
}

for plugin in allPlugins {
try await plugin.boot(app: self)
Container.onStart {
for plugin in allPlugins {
try await plugin.boot(app: self)
}
}

Container.onShutdown {
for plugin in allPlugins.reversed() {
try await plugin.shutdownServices(in: self)
}
}
}

Expand Down Expand Up @@ -124,21 +131,21 @@ public extension Application {
@MainActor
func start(args: [String]? = nil, waitOrShutdown: Bool = true) async throws {

// 0. Start the application lifecycle.
// 0. Add service

try await serviceGroup.run()
Container.main
.lifecycleServices
.append(
ApplicationService(
args: args,
waitOrShutdown: waitOrShutdown,
app: self
)
)

// 1. Parse and run a `Command` based on the application arguments.
// 1. Start the application lifecycle.

let command = try await commander.runCommand(args: args)
guard waitOrShutdown else { return }

// 2. Wait for lifecycle or immediately shut down depending on if the
// command should run indefinitely.

if !command.runUntilStopped {
await stop()
}
try await serviceGroup.run()
}

/// Stops the application.
Expand All @@ -152,6 +159,31 @@ public extension Application {
}
}

private struct ApplicationService: ServiceLifecycle.Service {
var args: [String]? = nil
var waitOrShutdown: Bool = true
let app: Application

func run() async throws {

// 0. Parse and run a `Command` based on the application arguments.

let command = try await app.commander.runCommand(args: args)
guard waitOrShutdown else { return }

// 1. Wait for lifecycle or immediately shut down depending on if the
// command should run indefinitely.

if !command.runUntilStopped {
await app.stop()
}

// 2. this is required to not throw service lifecycle errors

try await gracefulShutdown()
}
}

fileprivate extension ParsableCommand {
var runUntilStopped: Bool {
(Self.self as? Command.Type)?.runUntilStopped ?? false
Expand Down
51 changes: 51 additions & 0 deletions Alchemy/Application/Lifecycle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import ServiceLifecycle

final class Lifecycle: ServiceLifecycle.Service {
fileprivate var startTasks: [() async throws -> Void] = []
fileprivate var shutdownTasks: [() async throws -> Void] = []

var didStart = false
var didStop = false

func run() async throws {
try await start()
try await gracefulShutdown()
try await shutdown()
}

func start() async throws {
guard !didStart else { return }
didStart = true
for start in startTasks {
try await start()
}
}

func shutdown() async throws {
guard !didStop else { return }
didStop = true
for shutdown in shutdownTasks.reversed() {
try await shutdown()
}
}
}

extension Application {
var lifecycle: Lifecycle {
container.require()
}
}

extension Container {
static var lifecycle: Lifecycle {
require()
}

public static func onStart(action: @escaping () async throws -> Void) {
lifecycle.startTasks.append(action)
}

public static func onShutdown(action: @escaping () async throws -> Void) {
lifecycle.shutdownTasks.append(action)
}
}
18 changes: 13 additions & 5 deletions Alchemy/Application/Plugins/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ struct Core: Plugin {
return current
}

app.container.register(Services()).singleton()
let lifecycle = Lifecycle()
let lifecycleServices = LifecycleServices(services: [lifecycle])

app.container.register(lifecycle).singleton()
app.container.register(lifecycleServices).singleton()

// 4. Register ServiceGroup

Expand All @@ -47,7 +51,7 @@ struct Core: Plugin {
}

return ServiceGroup(
services: container.services.services,
services: container.lifecycleServices.services,
logger: logger
)
}.singleton()
Expand All @@ -62,8 +66,12 @@ struct Core: Plugin {
}
}

public final class Services {
fileprivate var services: [ServiceLifecycle.Service] = []
public final class LifecycleServices {
fileprivate var services: [ServiceLifecycle.Service]

init(services: [ServiceLifecycle.Service] = []) {
self.services = services
}

func append(_ service: ServiceLifecycle.Service) {
services.append(service)
Expand All @@ -85,7 +93,7 @@ extension Container {
require()
}

var services: Services {
var lifecycleServices: LifecycleServices {
require()
}

Expand Down
38 changes: 17 additions & 21 deletions Alchemy/Filesystem/Providers/LocalFilesystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,23 @@ private struct LocalFilesystem: FilesystemProvider {
return File(
name: url.lastPathComponent,
source: .filesystem(path: filepath),
content: .stream(
AsyncStream { continuation in
Task {
// Load the file in chunks, streaming it.
let fileHandle = try NIOFileHandle(path: url.path)
defer { try? fileHandle.close() }
try await fileIO.readChunked(
fileHandle: fileHandle,
byteCount: fileSizeBytes,
chunkSize: NonBlockingFileIO.defaultChunkSize,
allocator: bufferAllocator,
eventLoop: Loop,
chunkHandler: { chunk in
Loop.submit {
continuation.yield(chunk)
}
}
).get()
content: .stream { writer in
// Load the file in chunks, streaming it.
let fileHandle = try NIOFileHandle(path: url.path)
defer { try? fileHandle.close() }
try await fileIO.readChunked(
fileHandle: fileHandle,
byteCount: fileSizeBytes,
chunkSize: NonBlockingFileIO.defaultChunkSize,
allocator: bufferAllocator,
eventLoop: Loop,
chunkHandler: { chunk in
Loop.submit {
writer.write(chunk)
}
}
}
),
).get()
},
size: fileSizeBytes)
}

Expand All @@ -72,7 +68,7 @@ private struct LocalFilesystem: FilesystemProvider {
guard try await !exists(filepath) else {
throw FileError.filenameAlreadyExists
}

let fileHandle = try NIOFileHandle(path: url.path, mode: .write, flags: .allowFileCreation())
defer { try? fileHandle.close() }

Expand Down
6 changes: 5 additions & 1 deletion Alchemy/HTTP/Bytes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ public enum Bytes: ExpressibleByStringLiteral {
case .stream(let stream):
return stream
case .buffer(let buffer):
return AsyncStream { buffer }
return AsyncStream { continuation in
continuation.yield(buffer)
continuation.finish()
}
}
}

Expand Down Expand Up @@ -100,6 +103,7 @@ public enum Bytes: ExpressibleByStringLiteral {
Task {
let writer = Writer(continuation: continuation)
try await streamer(writer)
writer.finish()
}
}
)
Expand Down
2 changes: 1 addition & 1 deletion Alchemy/Services/Aliases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public var DB: Database { Container.require() }
public func DB(_ id: Database.Identifier?) -> Database { Container.require(id: id) }

/// The application Lifecycle
public var Lifecycle: ServiceGroup { Container.require() }
public var Services: ServiceGroup { Container.require() }

/// The application Environment
public var Env: Environment { Container.require() }
Expand Down
3 changes: 2 additions & 1 deletion AlchemyTest/Fakes/Database+Fake.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ extension Database {
@discardableResult
public static func fake(_ id: Identifier? = nil, keyMapping: KeyMapping = .snakeCase, migrations: [Migration] = [], seeders: [Seeder] = []) async throws -> Database {
let db = Database.sqlite.keyMapping(keyMapping)
// TODO: shutdown database
Container.register(db, id: id).singleton()
Container.onShutdown(action: db.shutdown)
db.migrations = migrations
db.seeders = seeders
if !migrations.isEmpty { try await db.migrate() }
Expand Down
2 changes: 2 additions & 0 deletions AlchemyTest/TestCase/TestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ open class TestCase<A: Application>: XCTestCase {
try await super.setUp()
app = A()
try await app.bootPlugins()
try await app.lifecycle.start()
try app.boot()
}

open override func tearDown() async throws {
try await super.tearDown()
await app.stop()
try await app.lifecycle.shutdown()
app.container.reset()
}
}
Expand Down
45 changes: 20 additions & 25 deletions Tests/Filesystem/FilesystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,29 @@ import AlchemyTest

final class FilesystemTests: TestCase<TestApp> {
private var filePath: String = ""

private lazy var allTests = [
_testCreate,
_testDelete,
_testPut,
_testPathing,
_testFileStore,
_testInvalidURL,
]
private var root: String = ""

override func setUp() async throws {
try await super.setUp()
let root = NSTemporaryDirectory() + UUID().uuidString
Container.register(Filesystem.local(root: root)).singleton()
self.root = root
self.filePath = UUID().uuidString + ".txt"
}

func testPlugin() {
let plugin = Filesystems(default: 1, disks: [1: .local, 2: .local])
plugin.registerServices(in: app)
XCTAssertNotNil(Container.resolve(Filesystem.self))
XCTAssertNotNil(Container.resolve(Filesystem.self, id: 1))
XCTAssertNotNil(Container.resolve(Filesystem.self, id: 2))
}

func testLocal() async throws {
let root = NSTemporaryDirectory() + UUID().uuidString
Container.register(Filesystem.local(root: root)).singleton()

func testLocalRoot() {
XCTAssertEqual(root, Storage.root)
for test in allTests {
filePath = UUID().uuidString + ".txt"
try await test()
}
}
func _testCreate() async throws {

func testLocalCreate() async throws {
AssertFalse(try await Storage.exists(filePath))
do {
_ = try await Storage.get(filePath)
Expand All @@ -44,8 +38,9 @@ final class FilesystemTests: TestCase<TestApp> {
AssertEqual(file.name, filePath)
AssertEqual(try await file.getContent().collect(), "1;2;3")
}


func _testDelete() async throws {
func testLocalDelete() async throws {
do {
try await Storage.delete(filePath)
XCTFail("Should throw an error")
Expand All @@ -55,15 +50,15 @@ final class FilesystemTests: TestCase<TestApp> {
AssertFalse(try await Storage.exists(filePath))
}

func _testPut() async throws {
func testLocalPut() async throws {
let file = File(name: filePath, source: .raw, content: "foo", size: 3)
try await Storage.put(file, as: filePath)
AssertTrue(try await Storage.exists(filePath))
try await Storage.put(file, in: "foo/bar", as: filePath)
AssertTrue(try await Storage.exists("foo/bar/\(filePath)"))
}

func _testPathing() async throws {
func testLocalPathing() async throws {
try await Storage.create("foo/bar/baz/\(filePath)", content: "foo")
AssertFalse(try await Storage.exists(filePath))
AssertTrue(try await Storage.exists("foo/bar/baz/\(filePath)"))
Expand All @@ -74,12 +69,12 @@ final class FilesystemTests: TestCase<TestApp> {
AssertFalse(try await Storage.exists("foo/bar/baz/\(filePath)"))
}

func _testFileStore() async throws {
func testLocalFileStore() async throws {
try await File(name: filePath, source: .raw, content: "bar", size: 3).store(as: filePath)
AssertTrue(try await Storage.exists(filePath))
}

func _testInvalidURL() async throws {
func testLocalInvalidURL() async throws {
do {
let store: Filesystem = .local(root: "\\+https://www.apple.com")
_ = try await store.exists("foo")
Expand Down
Loading

0 comments on commit 5b02277

Please sign in to comment.