open-standards/wai/wai-sink-swift/Tests/WaiSinkTests/TestOrigin.swift
Transaction Science aa607a60a5 wai-sink-swift: TestHost, an application the tests run inside on a phone or tablet
A device runs no tool-hosted test bundle, so the package's tests run on one hosted in
TestHost/: a minimal application (an empty window) and an Xcode project whose unit-test
target compiles the same test files against this package by its path, with the corpus in
its resources and local networking allowed for the loopback origin and relay. The package
itself is unchanged for consumers. The tests import WaiSink plainly (they use only its
public interface) and read the corpus from Bundle.module in the package, from their own
bundle in the host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 09:59:34 -04:00

86 lines
3.6 KiB
Swift

// An origin for the tests: an HTTP/1.1 server on the loopback interface serving fixed bodies by
// path. Nothing here leaves the host.
import Foundation
import Network
final class TestOrigin: @unchecked Sendable {
private let queue = DispatchQueue(label: "wai.sink.test-origin")
private var listener: NWListener?
private let lock = NSLock()
private var files: [String: (Data, String)] = [:]
private(set) var port: UInt16 = 0
private(set) var requests: [String] = []
func put(_ path: String, _ body: Data, type: String = "application/octet-stream") {
lock.lock(); files[path] = (body, type); lock.unlock()
}
func url(_ path: String) -> URL { URL(string: "http://127.0.0.1:\(port)\(path)")! }
func start() throws {
let params = NWParameters.tcp
params.requiredLocalEndpoint = NWEndpoint.hostPort(host: .ipv4(.loopback), port: .any)
let l = try NWListener(using: params)
let ready = DispatchSemaphore(value: 0)
l.stateUpdateHandler = { s in if case .ready = s { ready.signal() } }
l.newConnectionHandler = { [weak self] c in self?.serve(c) }
l.start(queue: queue)
_ = ready.wait(timeout: .now() + 5)
port = l.port?.rawValue ?? 0
listener = l
}
func stop() { listener?.cancel() }
private func serve(_ c: NWConnection) {
c.start(queue: queue)
c.receive(minimumIncompleteLength: 1, maximumLength: 65_536) { [weak self] d, _, _, _ in
guard let self, let d else { return c.cancel() }
let line = String(decoding: d, as: UTF8.self).components(separatedBy: "\r\n").first ?? ""
let path = line.split(separator: " ").dropFirst().first.map(String.init) ?? "/"
self.lock.lock()
self.requests.append(path)
let f = self.files[path]
self.lock.unlock()
let (status, body, type) = f.map { (200, $0.0, $0.1) } ?? (404, Data(), "text/plain")
var out = Data("HTTP/1.1 \(status) \(status == 200 ? "OK" : "Not Found")\r\nContent-Type: \(type)\r\nContent-Length: \(body.count)\r\nConnection: close\r\n\r\n".utf8)
out.append(body)
c.send(content: out, completion: .contentProcessed { _ in c.cancel() })
}
}
}
/// The repository's directory `wai/`, found from this file's path.
func waiDir() -> URL {
URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
}
func hexData(_ s: String) -> Data {
var d = Data(capacity: s.count / 2)
var i = s.startIndex
while i < s.endIndex {
let j = s.index(i, offsetBy: 2)
d.append(UInt8(s[i..<j], radix: 16)!)
i = j
}
return d
}
func canonical(_ v: Any) -> String {
String(decoding: try! JSONSerialization.data(withJSONObject: v, options: [.sortedKeys, .fragmentsAllowed]), as: UTF8.self)
}
/// The test bundle, where the tests are built outside the package (the device host, TestHost/).
final class TestBundleMarker {}
/// The native sink corpus: the copy staged into the test bundle's resources (a device has no
/// repository), else the repository's.
func corpus() throws -> [String: Any] {
#if SWIFT_PACKAGE
let staged = Bundle.module.url(forResource: "vectors", withExtension: "json", subdirectory: "Resources")
#else
let staged = Bundle(for: TestBundleMarker.self).url(forResource: "vectors", withExtension: "json")
#endif
let d = try Data(contentsOf: staged ?? waiDir().appendingPathComponent("live-conformance/native-sink/vectors.json"))
return try JSONSerialization.jsonObject(with: d) as! [String: Any]
}