Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ let package = Package(
name: "SharingGRDBCore",
targets: ["SharingGRDBCore"]
),
.library(
name: "SharingGRDBTestSupport",
targets: ["SharingGRDBTestSupport"]
),
.library(
name: "StructuredQueriesGRDB",
targets: ["StructuredQueriesGRDB"]
Expand All @@ -29,10 +33,12 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"),
.package(url: "https://github.com/groue/GRDB.swift", from: "7.4.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"),
.package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"),
.package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.13.0"),
],
targets: [
Expand All @@ -55,10 +61,20 @@ let package = Package(
name: "SharingGRDBTests",
dependencies: [
"SharingGRDB",
"SharingGRDBTestSupport",
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
.product(name: "StructuredQueries", package: "swift-structured-queries"),
]
),
.target(
name: "SharingGRDBTestSupport",
dependencies: [
"SharingGRDB",
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "StructuredQueriesTestSupport", package: "swift-structured-queries"),
]
),
.target(
name: "StructuredQueriesGRDBCore",
dependencies: [
Expand Down
271 changes: 271 additions & 0 deletions Sources/SharingGRDBTestSupport/AssertQuery.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import CustomDump
import Dependencies
import Foundation
import GRDB
import InlineSnapshotTesting
import StructuredQueriesCore
import StructuredQueriesGRDBCore
import StructuredQueriesTestSupport

/// An end-to-end snapshot testing helper for database content.
///
/// This helper can be used to generate snapshots of both the given query and the results of the
/// query decoded back into Swift.
///
/// ```swift
/// assertQuery(
/// Reminder.select(\.title).order(by: \.title)
/// } results: {
/// """
/// ┌────────────────────────────┐
/// │ "Buy concert tickets" │
/// │ "Call accountant" │
/// │ "Doctor appointment" │
/// │ "Get laundry" │
/// │ "Groceries" │
/// │ "Haircut" │
/// │ "Pick up kids from school" │
/// │ "Send weekly emails" │
/// │ "Take a walk" │
/// │ "Take out trash" │
/// └────────────────────────────┘
/// """
/// }
/// ```
///
/// - Parameters:
/// - includeSQL: Whether to snapshot the SQL fragment in addition to the results.
/// - query: A statement.
/// - database: The database to read from. A value of `nil` will use
/// `@Dependency(\.defaultDatabase)`.
/// - sql: A snapshot of the SQL produced by the statement.
/// - results: A snapshot of the results.
/// to `1` for invoking this helper directly, but if you write a wrapper function that automates
/// the `execute` trailing closure, you should pass `0` instead.
/// - fileID: The source `#fileID` associated with the assertion.
/// - filePath: The source `#filePath` associated with the assertion.
/// - function: The source `#function` associated with the assertion
/// - line: The source `#line` associated with the assertion.
/// - column: The source `#column` associated with the assertion.
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@_disfavoredOverload
public func assertQuery<each V: QueryRepresentable, S: StructuredQueriesCore.Statement<(repeat each V)>>(
includeSQL: Bool = false,
_ query: S,
database: (any DatabaseReader)? = nil,
sql: (() -> String)? = nil,
results: (() -> String)? = nil,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
function: StaticString = #function,
line: UInt = #line,
column: UInt = #column
) {
if includeSQL {
assertInlineSnapshot(
of: query,
as: .sql,
message: "Query did not match",
syntaxDescriptor: InlineSnapshotSyntaxDescriptor(
trailingClosureLabel: "sql",
trailingClosureOffset: 0
),
matches: sql,
fileID: fileID,
file: filePath,
function: function,
line: line,
column: column
)
}
do {
@Dependency(\.defaultDatabase) var defaultDatabase
let rows = try (database ?? defaultDatabase).read { try query.fetchAll($0) }
var table = ""
printTable(rows, to: &table)
if !table.isEmpty {
assertInlineSnapshot(
of: table,
as: .lines,
message: "Results did not match",
syntaxDescriptor: InlineSnapshotSyntaxDescriptor(
trailingClosureLabel: "results",
trailingClosureOffset: includeSQL ? 1 : 0
),
matches: includeSQL ? results : sql,
fileID: fileID,
file: filePath,
function: function,
line: line,
column: column
)
} else if results != nil {
assertInlineSnapshot(
of: table,
as: .lines,
message: "Results expected to be empty",
syntaxDescriptor: InlineSnapshotSyntaxDescriptor(
trailingClosureLabel: "results",
trailingClosureOffset: includeSQL ? 1 : 0
),
matches: includeSQL ? results : sql,
fileID: fileID,
file: filePath,
function: function,
line: line,
column: column
)
}
} catch {
assertInlineSnapshot(
of: error.localizedDescription,
as: .lines,
message: "Results did not match",
syntaxDescriptor: InlineSnapshotSyntaxDescriptor(
trailingClosureLabel: "results",
trailingClosureOffset: includeSQL ? 1 : 0
),
matches: includeSQL ? results : sql,
fileID: fileID,
file: filePath,
function: function,
line: line,
column: column
)
}
}

/// An end-to-end snapshot testing helper for database content.
///
/// This helper can be used to generate snapshots of both the given query and the results of the
/// query decoded back into Swift.
///
/// ```swift
/// assertQuery(
/// Reminder.select(\.title).order(by: \.title)
/// } results: {
/// """
/// ┌────────────────────────────┐
/// │ "Buy concert tickets" │
/// │ "Call accountant" │
/// │ "Doctor appointment" │
/// │ "Get laundry" │
/// │ "Groceries" │
/// │ "Haircut" │
/// │ "Pick up kids from school" │
/// │ "Send weekly emails" │
/// │ "Take a walk" │
/// │ "Take out trash" │
/// └────────────────────────────┘
/// """
/// }
/// ```
///
/// - Parameters:
/// - includeSQL: Whether to snapshot the SQL fragment in addition to the results.
/// - query: A statement.
/// - sql: A snapshot of the SQL produced by the statement.
/// - database: The database to read from. A value of `nil` will use
/// `@Dependency(\.defaultDatabase)`.
/// - results: A snapshot of the results.
/// to `1` for invoking this helper directly, but if you write a wrapper function that automates
/// the `execute` trailing closure, you should pass `0` instead.
/// - fileID: The source `#fileID` associated with the assertion.
/// - filePath: The source `#filePath` associated with the assertion.
/// - function: The source `#function` associated with the assertion
/// - line: The source `#line` associated with the assertion.
/// - column: The source `#column` associated with the assertion.
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
public func assertQuery<S: SelectStatement, each J: StructuredQueriesCore.Table>(
includeSQL: Bool = false,
_ query: S,
database: (any DatabaseReader)? = nil,
sql: (() -> String)? = nil,
results: (() -> String)? = nil,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
function: StaticString = #function,
line: UInt = #line,
column: UInt = #column
) where S.QueryValue == (), S.Joins == (repeat each J) {
assertQuery(
includeSQL: includeSQL,
query.selectStar(),
database: database,
sql: sql,
results: results,
fileID: fileID,
filePath: filePath,
function: function,
line: line,
column: column
)
}

private func printTable<each C>(_ rows: [(repeat each C)], to output: inout some TextOutputStream) {
var maxColumnSpan: [Int] = []
var hasMultiLineRows = false
for _ in repeat (each C).self {
maxColumnSpan.append(0)
}
var table: [([[Substring]], maxRowSpan: Int)] = []
for row in rows {
var columns: [[Substring]] = []
var index = 0
var maxRowSpan = 0
for column in repeat each row {
defer { index += 1 }
var cell = ""
customDump(column, to: &cell)
let lines = cell.split(separator: "\n")
hasMultiLineRows = hasMultiLineRows || lines.count > 1
maxRowSpan = max(maxRowSpan, lines.count)
maxColumnSpan[index] = max(maxColumnSpan[index], lines.map(\.count).max() ?? 0)
columns.append(lines)
}
table.append((columns, maxRowSpan))
}
guard !table.isEmpty else { return }
output.write("┌─")
output.write(
maxColumnSpan
.map { String(repeating: "─", count: $0) }
.joined(separator: "─┬─")
)
output.write("─┐\n")
for (offset, rowAndMaxRowSpan) in table.enumerated() {
let (row, maxRowSpan) = rowAndMaxRowSpan
for rowOffset in 0..<maxRowSpan {
output.write("│ ")
var line: [String] = []
for (columns, maxColumnSpan) in zip(row, maxColumnSpan) {
if columns.count <= rowOffset {
line.append(String(repeating: " ", count: maxColumnSpan))
} else {
line.append(
columns[rowOffset]
+ String(repeating: " ", count: maxColumnSpan - columns[rowOffset].count)
)
}
}
output.write(line.joined(separator: " │ "))
output.write(" │\n")
}
if hasMultiLineRows, offset != table.count - 1 {
output.write("├─")
output.write(
maxColumnSpan
.map { String(repeating: "─", count: $0) }
.joined(separator: "─┼─")
)
output.write("─┤\n")
}
}
output.write("└─")
output.write(
maxColumnSpan
.map { String(repeating: "─", count: $0) }
.joined(separator: "─┴─")
)
output.write("─┘")
}
Loading