← poker-tutor
import Foundation
/// A very small test harness.
///
/// Apple's test frameworks (XCTest and swift-testing) both require a full Xcode install.
/// This project needs to be testable before Xcode is present, so suites are plain functions
/// collected here and run by `pokerlab test`.
public final class TestRun: @unchecked Sendable {
public struct Failure {
public let suite: String
public let test: String
public let message: String
public let file: String
public let line: Int
}
public private(set) var failures: [Failure] = []
public private(set) var checks = 0
public private(set) var testCount = 0
private var currentSuite = ""
private var currentTest = ""
private var suiteFailuresAtStart = 0
private let verbose: Bool
private let filter: String?
public init(verbose: Bool = false, filter: String? = nil) {
self.verbose = verbose
self.filter = filter
}
public var passed: Bool { failures.isEmpty }
/// Run before every suite's body — a place to put shared state back where the suites expect it.
public var beforeEachSuite: (() -> Void)?
public func suite(_ name: String, _ body: (TestRun) -> Void) {
beforeEachSuite?()
if let filter, !name.lowercased().contains(filter.lowercased()) { return }
currentSuite = name
suiteFailuresAtStart = failures.count
let start = Date()
body(self)
let elapsed = Date().timeIntervalSince(start)
let failed = failures.count - suiteFailuresAtStart
let mark = failed == 0 ? " ok " : " FAIL "
let timing = elapsed > 0.5 ? String(format: " (%.1fs)", elapsed) : ""
print("\(mark) \(name)\(timing)")
}
public func test(_ name: String, _ body: () -> Void) {
currentTest = name
testCount += 1
body()
}
public func expect(_ condition: Bool, _ message: @autoclosure () -> String = "", file: String = #fileID, line: Int = #line) {
checks += 1
guard !condition else { return }
let text = message()
record(text.isEmpty ? "expectation failed" : text, file: file, line: line)
}
public func equal<T: Equatable>(_ actual: T, _ expected: T, _ note: @autoclosure () -> String = "", file: String = #fileID, line: Int = #line) {
checks += 1
guard actual != expected else { return }
let suffix = note().isEmpty ? "" : " — \(note())"
record("expected \(expected), got \(actual)\(suffix)", file: file, line: line)
}
public func notEqual<T: Equatable>(_ actual: T, _ unexpected: T, _ note: @autoclosure () -> String = "", file: String = #fileID, line: Int = #line) {
checks += 1
guard actual == unexpected else { return }
let suffix = note().isEmpty ? "" : " — \(note())"
record("expected something other than \(unexpected)\(suffix)", file: file, line: line)
}
public func close(_ actual: Double, _ expected: Double, _ tolerance: Double, _ note: @autoclosure () -> String = "", file: String = #fileID, line: Int = #line) {
checks += 1
guard abs(actual - expected) > tolerance else { return }
let suffix = note().isEmpty ? "" : " — \(note())"
record(String(format: "expected %.4f ± %.4f, got %.4f", expected, tolerance, actual) + suffix, file: file, line: line)
}
public func lessThan<T: Comparable>(_ a: T, _ b: T, _ note: @autoclosure () -> String = "", file: String = #fileID, line: Int = #line) {
checks += 1
guard !(a < b) else { return }
let suffix = note().isEmpty ? "" : " — \(note())"
record("expected \(a) < \(b)\(suffix)", file: file, line: line)
}
public func greaterThan<T: Comparable>(_ a: T, _ b: T, _ note: @autoclosure () -> String = "", file: String = #fileID, line: Int = #line) {
checks += 1
guard !(a > b) else { return }
let suffix = note().isEmpty ? "" : " — \(note())"
record("expected \(a) > \(b)\(suffix)", file: file, line: line)
}
private func record(_ message: String, file: String, line: Int) {
failures.append(Failure(suite: currentSuite, test: currentTest, message: message, file: file, line: line))
if verbose { print(" ✗ \(currentTest): \(message)") }
}
public func report() -> Bool {
print("")
if failures.isEmpty {
print("PASSED — \(testCount) tests, \(checks) checks")
return true
}
print("FAILED — \(failures.count) of \(checks) checks in \(testCount) tests")
print("")
for f in failures {
print(" \(f.suite) › \(f.test)")
print(" \(f.message)")
print(" \(f.file):\(f.line)")
}
return false
}
}