Skip to content

Commit 3f02b4e

Browse files
committed
Kick the code coverage up a bunch, because why not
1 parent 1c0a506 commit 3f02b4e

File tree

1 file changed

+195
-13
lines changed

1 file changed

+195
-13
lines changed

Tests/FluentKitExtrasTests/FluentKitExtrasTests.swift

Lines changed: 195 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import FluentKit
22
import FluentKitExtras
33
import Foundation
4+
import Logging
45
import NIOCore
6+
import NIOEmbedded
57
import Testing
68

79
@Suite("FluentKit Extras")
@@ -58,7 +60,7 @@ struct FluentKitExtrasTests {
5860
let now = Date()
5961
model.timestamp = now
6062

61-
#expect(model.timestamp == TimestampFormatFactory<ISO8601TimestampFormat>.iso8601.makeFormat().parse(TimestampFormatFactory<ISO8601TimestampFormat>.iso8601.makeFormat().serialize(now)!))
63+
#expect(model.timestamp == fluentIso8601Date(fluentIso8601String(now)))
6264
#expect(model.$timestamp.description == "@BarModel.RequiredTimestamp(key: timestamp)")
6365
#expect(model.$timestamp.path == [.string("timestamp")])
6466
#expect(model.$timestamp.keys == [.string("timestamp")])
@@ -70,7 +72,7 @@ struct FluentKitExtrasTests {
7072
}
7173

7274
@Test
73-
func pointerProperties() throws {
75+
func pointerProperties() async throws {
7476
let model = BazModel()
7577
model.$bar.ref = "a"
7678
model.$optionalBar.ref = "b"
@@ -98,15 +100,35 @@ struct FluentKitExtrasTests {
98100
case let filter:
99101
Issue.record("Incorrect filter for model.$bar.query(on:): \(String(describing: filter))")
100102
}
103+
104+
// Everything from here down is just code coverage junk
105+
model.$bar.ref = "b"
106+
model.$bar.value = .init()
107+
#expect(model.bar.id == nil)
108+
#expect(model.$bar.description == "Pointer<BazModel, BarModel, identifier>(key: baridentifier)")
109+
await #expect(throws: Never.self) { try await model.$bar.get(reload: true, on: MockFluentDatabase([[["id": "1", "identifier": "b", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "recursive_baridentifier": "b", "blooey": "", "phooey": ""] as QuickOutput]])) }
110+
#expect(model.$bar.anyQueryableProperty === model.$bar.$ref)
111+
#expect(model.$bar.queryablePath == [.string("baridentifier")])
112+
#expect(model.$bar.queryableProperty === model.$bar.$ref)
113+
model.$optionalBar.ref = "b"
114+
model.$optionalBar.value = .some(nil)
115+
#expect(model.optionalBar == nil)
116+
#expect(model.$optionalBar.description == "OptionalPointer<BazModel, BarModel, identifier>(key: optional_baridentifier)")
117+
await #expect(throws: Never.self) { try await model.$optionalBar.get(reload: true, on: MockFluentDatabase()) }
118+
#expect(model.$optionalBar.anyQueryableProperty === model.$optionalBar.$ref)
119+
#expect(model.$optionalBar.queryablePath == [.string("optional_baridentifier")])
120+
#expect(model.$optionalBar.queryableProperty === model.$optionalBar.$ref)
121+
122+
#expect(throws: Never.self) { try JSONEncoder().encode(model) }
101123
}
102124

103125
@Test
104-
func referenceProperties() throws {
126+
func referenceProperties() async throws {
105127
let model = BarModel()
106-
#expect(throws: Never.self) { try model.$bazs.output(from: ["identifier": "a"] as QuickOutput) }
107-
#expect(throws: Never.self) { try model.$optionalBaz.output(from: ["identifier": "b"] as QuickOutput) }
108-
#expect(throws: Never.self) { try model.$recursiveBars.output(from: ["identifier": "c"] as QuickOutput) }
109-
#expect(throws: Never.self) { try model.$optionalRecursiveBar.output(from: ["identifier": "d"] as QuickOutput) }
128+
model.$bazs.fromValue = "a"
129+
model.$optionalBaz.fromValue = "b"
130+
model.$recursiveBars.fromValue = "c"
131+
model.$optionalRecursiveBar.fromValue = "d"
110132
let query1 = model.$bazs.query(on: MockFluentDatabase()), filter1 = try #require(query1.query.filters.first)
111133
switch filter1 {
112134
case .value(.extendedPath(let path, "bazs", nil), .equality(false), .bind(let bind)):
@@ -135,6 +157,64 @@ struct FluentKitExtrasTests {
135157
#expect(String(describing: bind) == #"Optional("d")"#)
136158
case let filter: Issue.record("Incorrect filter for model.$optionalRecursiveBar.query(on:): \(String(describing: filter))")
137159
}
160+
161+
// Everything from here down is just code coverage junk
162+
model.$bazs.value = []
163+
model.$optionalBaz.value = .some(nil)
164+
model.$baz.value = .some(nil)
165+
model.$baz.fromValue = "a"
166+
model.$optionalBazs.value = []
167+
model.$optionalBazs.fromValue = "b"
168+
#expect(model.$bazs.fromValue == "a")
169+
#expect(model.bazs.isEmpty)
170+
#expect(model.$bazs.description == "@References<BarModel, BazModel>(for: required(\\BazModel.$bar))")
171+
#expect(model.$bazs.keys == [])
172+
#expect(model.$optionalBaz.fromValue == "b")
173+
#expect(model.optionalBaz == nil)
174+
#expect(model.$optionalBaz.description == "@OptionalReference<BarModel, BazModel>(for: optional(\\BazModel.$optionalBar))")
175+
#expect(model.$optionalBaz.keys == [])
176+
#expect(model.$optionalBazs.query(on: MockFluentDatabase()).query.description == #"query read bazs filters=[bazs[optional_baridentifier] = Optional("b")]"#)
177+
#expect(model.$baz.query(on: MockFluentDatabase()).query.description == #"query read bazs filters=[bazs[baridentifier] = "a"]"#)
178+
model.$bazs.input(to: QuickInput())
179+
model.$optionalBaz.input(to: QuickInput())
180+
#expect(throws: Never.self) { try model.$bazs.output(from: ["identifier": "a", "baridentifier": "a"] as QuickOutput) }
181+
#expect(throws: Never.self) { try model.$optionalBazs.output(from: ["identifier": "b", "optional_baridentifier": "b"] as QuickOutput) }
182+
#expect(throws: Never.self) { try model.$optionalBaz.output(from: ["identifier": "b", "optional_baridentifier": "b"] as QuickOutput) }
183+
#expect(throws: Never.self) { try model.$baz.output(from: ["identifier": "a", "baridentifier": "a"] as QuickOutput) }
184+
model.identifier = "a"
185+
model.$bar.ref = "b"
186+
model.timestamp = .init()
187+
model.another = .init()
188+
model.group.phooey = "phooey"
189+
model.group.blooey = "blooey"
190+
#expect(throws: Never.self) { try JSONEncoder().encode(model) }
191+
await #expect(throws: Never.self) { try await model.$bazs.get(reload: true, on: MockFluentDatabase()) }
192+
await #expect(throws: Never.self) { try await model.$bazs.create(BazModel(), on: MockFluentDatabase([[["id": "1"] as QuickOutput]])).get() }
193+
await #expect(throws: Never.self) { try await model.$optionalBazs.create(BazModel(), on: MockFluentDatabase([[["id": "1"] as QuickOutput]])).get() }
194+
await #expect(throws: Never.self) { try await model.$bazs.create([.init(), .init()], on: MockFluentDatabase([[["id": "1"] as QuickOutput, ["id": "2"] as QuickOutput]])).get() }
195+
await #expect(throws: Never.self) { try await model.$optionalBazs.create([.init(), .init()], on: MockFluentDatabase([[["id": "1"] as QuickOutput, ["id": "2"] as QuickOutput]])).get() }
196+
await #expect(throws: Never.self) { try await model.$optionalBaz.get(reload: true, on: MockFluentDatabase()) }
197+
await #expect(throws: Never.self) { try await model.$baz.get(reload: true, on: MockFluentDatabase()) }
198+
await #expect(throws: Never.self) { try await model.$optionalBaz.create(.init(), on: MockFluentDatabase([[["id": "1"] as QuickOutput]])).get() }
199+
await #expect(throws: Never.self) { try await model.$baz.create(.init(), on: MockFluentDatabase([[["id": "1"] as QuickOutput]])).get() }
200+
await #expect(throws: Never.self) { try await BarModel.query(on: MockFluentDatabase([
201+
[["id": "1", "identifier": "a", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "b"] as QuickOutput],
202+
[["id": "2", "identifier": "b", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "a"] as QuickOutput],
203+
[["id": "1", "baridentifier": "a", "optional_baridentifier": "a"] as QuickOutput],
204+
[["id": "1", "baridentifier": "a", "optional_baridentifier": "a"] as QuickOutput],
205+
[["id": "1", "baridentifier": "a", "optional_baridentifier": "a"] as QuickOutput],
206+
[["id": "1", "baridentifier": "a", "optional_baridentifier": "a"] as QuickOutput],
207+
[["id": "1", "identifier": "a", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "a"] as QuickOutput],
208+
[["id": "1", "identifier": "a", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "a", "optional_recursive_baridentifier": "a"] as QuickOutput],
209+
[["id": "1", "identifier": "a", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "a", "optional_recursive_baridentifier": "a"] as QuickOutput],
210+
[["id": "1", "identifier": "a", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "a"] as QuickOutput],
211+
[["id": "2", "identifier": "b", "timestamp": fluentIso8601String(), "another": fluentIso8601String(), "phooey": "", "blooey": "", "recursive_baridentifier": "a"] as QuickOutput],
212+
]))
213+
.withDeleted()
214+
.with(\.$bar).with(\.$optionalBar).with(\.$bazs).with(\.$optionalBazs).with(\.$optionalBaz).with(\.$baz).with(\.$recursiveBars).with(\.$optionalRecursiveBars).with(\.$optionalRecursiveBar).with(\.$recursiveBar)
215+
.with(\.$bar, withDeleted: true).with(\.$optionalBar, withDeleted: true).with(\.$bazs, withDeleted: true).with(\.$optionalBazs, withDeleted: true).with(\.$optionalBaz, withDeleted: true).with(\.$baz, withDeleted: true).with(\.$recursiveBars, withDeleted: true).with(\.$optionalRecursiveBars, withDeleted: true).with(\.$optionalRecursiveBar, withDeleted: true).with(\.$recursiveBar, withDeleted: true)
216+
.all()
217+
}
138218
}
139219
}
140220

@@ -177,15 +257,27 @@ final class BarModel: FluentKit.Model, @unchecked Sendable {
177257
@References(for: \.$bar)
178258
var bazs: [BazModel]
179259

260+
@References(for: \.$optionalBar)
261+
var optionalBazs: [BazModel]
262+
180263
@OptionalReference(for: \.$optionalBar)
181264
var optionalBaz: BazModel?
182265

266+
@OptionalReference(for: \.$bar)
267+
var baz: BazModel?
268+
183269
@References(forRecursive: \.$bar, referencedBy: \.$identifier)
184270
var recursiveBars: [BarModel]
185271

272+
@References(forRecursive: \.$optionalBar, referencedBy: \.$identifier)
273+
var optionalRecursiveBars: [BarModel]
274+
186275
@OptionalReference(forRecursive: \.$optionalBar, referencedBy: \.$baridentifier)
187276
var optionalRecursiveBar: BarModel?
188277

278+
@OptionalReference(forRecursive: \.$bar, referencedBy: \.$baridentifier)
279+
var recursiveBar: BarModel?
280+
189281
init() {}
190282
}
191283

@@ -214,20 +306,35 @@ final class BopFields: FluentKit.Fields, @unchecked Sendable {
214306
init() {}
215307
}
216308

217-
struct MockFluentDatabase: Database {
309+
final class MockFluentDatabase: Database {
218310
struct MockConfiguration: DatabaseConfiguration {
219311
var middleware: [any AnyModelMiddleware] = []
220312
func makeDriver(for databases: Databases) -> any DatabaseDriver { fatalError() }
221313
}
222314

223315
let context: DatabaseContext = .init(
224316
configuration: MockConfiguration(),
225-
logger: .init(label: "", factory: SwiftLogNoOpLogHandler.init(_:)),
226-
eventLoop: NIOSingletons.posixEventLoopGroup.any()
317+
logger: .init(label: "mockdb", factory: { l in
318+
var h = ModifiedStreamLogHandler.standardOutput(label: l)
319+
h.logLevel = .debug
320+
return h
321+
}),
322+
eventLoop: EmbeddedEventLoop()
227323
)
228324
let inTransaction = false
229325

230-
func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) -> EventLoopFuture<Void> { self.eventLoop.makeSucceededVoidFuture() }
326+
nonisolated(unsafe) var outputs: [[any DatabaseOutput]]
327+
328+
init(_ outputs: [[any DatabaseOutput]] = []) {
329+
self.outputs = outputs
330+
}
331+
332+
func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) -> EventLoopFuture<Void> {
333+
if !self.outputs.isEmpty {
334+
for output in self.outputs.removeFirst() { onOutput(output) }
335+
}
336+
return self.eventLoop.makeSucceededVoidFuture()
337+
}
231338
func execute(schema: DatabaseSchema) -> EventLoopFuture<Void> { self.eventLoop.makeSucceededVoidFuture() }
232339
func execute(enum: DatabaseEnum) -> EventLoopFuture<Void> { self.eventLoop.makeSucceededVoidFuture() }
233340
func transaction<T>(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>) -> EventLoopFuture<T> { closure(self) }
@@ -243,8 +350,31 @@ final class QuickOutput: DatabaseOutput, ExpressibleByDictionaryLiteral {
243350
func contains(_ key: FieldKey) -> Bool { self.content[key.description] != nil }
244351
func decodeNil(_ key: FieldKey) throws -> Bool { self.content[key.description] == nil }
245352
func decode<T: Decodable>(_ key: FieldKey, as type: T.Type) throws -> T {
246-
guard let value = self.content[key.description] as? T else { throw FluentError.missingField(name: key.description) }
247-
return value
353+
switch type {
354+
case is String.Type:
355+
guard let rawValue = self.content[key.description] else { throw FluentError.missingField(name: key.description) }
356+
return rawValue as! T
357+
case is String?.Type:
358+
return self.content[key.description] as! T
359+
case is Int.Type:
360+
guard let rawValue = self.content[key.description] else { throw FluentError.missingField(name: key.description) }
361+
guard let result = Int(rawValue) else { throw FluentError.invalidField(name: key.description, valueType: type, error: CancellationError()) }
362+
return result as! T
363+
case is Int?.Type:
364+
guard let rawValue = self.content[key.description] else { return Int?.none as! T }
365+
guard let result = Int(rawValue) else { throw FluentError.invalidField(name: key.description, valueType: type, error: CancellationError()) }
366+
return result as! T
367+
case is Date.Type:
368+
guard let rawValue = self.content[key.description] else { throw FluentError.missingField(name: key.description) }
369+
guard let result = TimestampFormatFactory<ISO8601TimestampFormat>.iso8601.makeFormat().parse(rawValue) else { throw FluentError.invalidField(name: key.description, valueType: type, error: CancellationError()) }
370+
return result as! T
371+
case is Date?.Type:
372+
guard let rawValue = self.content[key.description] else { return Date?.none as! T }
373+
guard let result = TimestampFormatFactory<ISO8601TimestampFormat>.iso8601.makeFormat().parse(rawValue) else { throw FluentError.invalidField(name: key.description, valueType: type, error: CancellationError()) }
374+
return result as! T
375+
default:
376+
throw FluentError.invalidField(name: key.description, valueType: type, error: CancellationError())
377+
}
248378
}
249379
}
250380

@@ -260,3 +390,55 @@ final class QuickInput: DatabaseInput {
260390
self.content[key.description] = str
261391
}
262392
}
393+
394+
func fluentIso8601Date(_ str: String) -> Date? {
395+
TimestampFormatFactory<ISO8601TimestampFormat>.iso8601.makeFormat().parse(str)
396+
}
397+
398+
func fluentIso8601String(_ date: Date = .init()) -> String {
399+
TimestampFormatFactory<ISO8601TimestampFormat>.iso8601.makeFormat().serialize(date)!
400+
}
401+
402+
public struct ModifiedStreamLogHandler: LogHandler {
403+
public static func standardOutput(label: String) -> Self { .init(label: label) }
404+
private let label: String
405+
public var logLevel: Logger.Level = .info, metadataProvider = LoggingSystem.metadataProvider, metadata = Logger.Metadata()
406+
public subscript(metadataKey key: String) -> Logger.Metadata.Value? { get { self.metadata[key] } set { self.metadata[key] = newValue } }
407+
internal init(label: String) { self.label = label }
408+
public func log(level: Logger.Level, message: Logger.Message, metadata explicitMetadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) {
409+
let prettyMetadata = self.prettify(Self.prepareMetadata(base: self.metadata, provider: self.metadataProvider, explicit: explicitMetadata))
410+
print("\(self.timestamp()) \(level) \(self.label) :\(prettyMetadata.map { " \($0)" } ?? "") [\(source)] \(message)")
411+
}
412+
internal static func prepareMetadata(base: Logger.Metadata, provider: Logger.MetadataProvider?, explicit: Logger.Metadata?) -> Logger.Metadata {
413+
var metadata = base
414+
if let provided = provider?.get(), !provided.isEmpty { metadata.merge(provided, uniquingKeysWith: { $1 }) }
415+
if let explicit = explicit, !explicit.isEmpty { metadata.merge(explicit, uniquingKeysWith: { $1 }) }
416+
return metadata
417+
}
418+
private func prettify(_ metadata: Logger.Metadata) -> String? {
419+
metadata.isEmpty ? nil : metadata.lazy.sorted { $0.0 < $1.0 }.map { "\($0)=\($1.prettyDescription)" }.joined(separator: " ")
420+
}
421+
private func timestamp() -> String {
422+
.init(unsafeUninitializedCapacity: 255) { buffer in
423+
var timestamp = time(nil)
424+
guard let localTime = localtime(&timestamp) else { return buffer.initialize(fromContentsOf: "<unknown>".utf8) }
425+
return strftime(buffer.baseAddress!, buffer.count, "%Y-%m-%dT%H:%M:%S%z", localTime)
426+
}
427+
}
428+
}
429+
extension Logger.MetadataValue {
430+
public var prettyDescription: String {
431+
switch self {
432+
case .dictionary(let dict): "[\(dict.mapValues(\.prettyDescription).lazy.sorted { $0.0 < $1.0 }.map { "\($0): \($1)" }.joined(separator: ", "))]"
433+
case .array(let list): "[\(list.map(\.prettyDescription).joined(separator: ", "))]"
434+
case .string(let str): #""\#(str)""#
435+
case .stringConvertible(let repr):
436+
switch repr {
437+
case let repr as Bool: "\(repr)"
438+
case let repr as any FixedWidthInteger: "\(repr)"
439+
case let repr as any BinaryFloatingPoint: "\(repr)"
440+
default: #""\#(repr.description)""#
441+
}
442+
}
443+
}
444+
}

0 commit comments

Comments
 (0)