#if os(macOS) import Foundation import XCTest import RastrilloApple import RastrilloTestSupport private actor Collected { var lines: [String] = [] func append(_ line: String) { lines.append(line) } } /// A scripted stream must deliver exactly what a real one does for the same /// bytes, or a test passes against framing no server produces. final class StreamParityTests: XCTestCase { private let bodies: [String: Data] = [ "lf": Data("event: update\ndata: 1\n\n".utf8), "crlf": Data("event: update\r\ndata: 2\r\n\r\n".utf8), "cr": Data("event: update\rdata: 3\r\r".utf8), "mixed": Data("a\r\nb\rc\n\r\n".utf8), "unterminated": Data("data: 4\n\ndata: partial".utf8), "bom": Data("\u{FEFF}data: 5\n\n".utf8), ] private func server(_ bodies: [String: Data]) throws -> (Process, URL) { let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) for (name, body) in bodies { try body.write(to: directory.appendingPathComponent(name)) } let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/python3") process.arguments = ["-u", "-c", #""" import http.server, pathlib, sys root = pathlib.Path(sys.argv[1]) class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): body = (root / self.path.strip('/')).read_bytes() self.send_response(200) self.send_header('Content-Type', 'text/event-stream') self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler) print(server.server_port, flush=True) server.serve_forever() """#, directory.path] let output = Pipe() process.standardOutput = output try process.run() var port = Data() while port.count < 12 { let byte = output.fileHandleForReading.readData(ofLength: 1) if byte.isEmpty || byte == Data([10]) { break } port.append(byte) } guard let number = String(data: port, encoding: .utf8).flatMap(Int.init), let origin = URL(string: "http://127.0.0.1:\(number)") else { process.terminate() throw NativeClientError.message("The parity fixture could not start.") } return (process, origin) } private func lines(_ transport: NativeTransport, _ path: String) async throws -> [String] { let collected = Collected() try await transport.stream(path) { await collected.append($0) } return await collected.lines } func testScriptedStreamFramesLikeNativeHTTP() async throws { let (process, origin) = try server(bodies) defer { process.terminate() } let real = NativeHTTP(origin: origin) for (name, body) in bodies.sorted(by: { $0.key < $1.key }) { let expected = try await lines(real, "/\(name)") let scripted = try await lines(ScriptedTransport([.respond(status: 200, body: body)]), "/\(name)") XCTAssertEqual(scripted, expected, "\(name): scripted framing differs from NativeHTTP") } } func testScriptedStreamRefusesWhatNativeHTTPRefuses() async throws { let invalid = Data([0x64, 0x61, 0x74, 0x61, 0x3A, 0xFF, 0x0A]) let long = Data(repeating: 0x61, count: 256 * 1024 + 1) + Data("\n".utf8) let (process, origin) = try server(["invalid": invalid, "long": long]) defer { process.terminate() } let real = NativeHTTP(origin: origin) for (name, body) in [("invalid", invalid), ("long", long)] { var realError: String? do { _ = try await lines(real, "/\(name)") } catch { realError = error.localizedDescription } XCTAssertNotNil(realError, "\(name): NativeHTTP accepted it") do { _ = try await lines(ScriptedTransport([.respond(status: 200, body: body)]), "/\(name)") XCTFail("\(name): ScriptedTransport accepted what NativeHTTP refuses") } catch { XCTAssertEqual(error.localizedDescription, realError, name) } } } } #endif