rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import Foundation
3import XCTest
4import RastrilloApple
5import RastrilloTestSupport
6
7private actor Collected {
8 var lines: [String] = []
9 func append(_ line: String) { lines.append(line) }
10}
11
12/// A scripted stream must deliver exactly what a real one does for the same
13/// bytes, or a test passes against framing no server produces.
14final class StreamParityTests: XCTestCase {
15 private let bodies: [String: Data] = [
16 "lf": Data("event: update\ndata: 1\n\n".utf8),
17 "crlf": Data("event: update\r\ndata: 2\r\n\r\n".utf8),
18 "cr": Data("event: update\rdata: 3\r\r".utf8),
19 "mixed": Data("a\r\nb\rc\n\r\n".utf8),
20 "unterminated": Data("data: 4\n\ndata: partial".utf8),
21 "bom": Data("\u{FEFF}data: 5\n\n".utf8),
22 ]
23
24 private func server(_ bodies: [String: Data]) throws -> (Process, URL) {
25 let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
26 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
27 for (name, body) in bodies { try body.write(to: directory.appendingPathComponent(name)) }
28 let process = Process()
29 process.executableURL = URL(fileURLWithPath: "/usr/bin/python3")
30 process.arguments = ["-u", "-c", #"""
31import http.server, pathlib, sys
32root = pathlib.Path(sys.argv[1])
33class Handler(http.server.BaseHTTPRequestHandler):
34 def log_message(self, *args): pass
35 def do_GET(self):
36 body = (root / self.path.strip('/')).read_bytes()
37 self.send_response(200)
38 self.send_header('Content-Type', 'text/event-stream')
39 self.send_header('Content-Length', str(len(body)))
40 self.end_headers()
41 self.wfile.write(body)
42server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
43print(server.server_port, flush=True)
44server.serve_forever()
45"""#, directory.path]
46 let output = Pipe()
47 process.standardOutput = output
48 try process.run()
49 var port = Data()
50 while port.count < 12 {
51 let byte = output.fileHandleForReading.readData(ofLength: 1)
52 if byte.isEmpty || byte == Data([10]) { break }
53 port.append(byte)
54 }
55 guard let number = String(data: port, encoding: .utf8).flatMap(Int.init),
56 let origin = URL(string: "http://127.0.0.1:\(number)") else {
57 process.terminate()
58 throw NativeClientError.message("The parity fixture could not start.")
59 }
60 return (process, origin)
61 }
62
63 private func lines(_ transport: NativeTransport, _ path: String) async throws -> [String] {
64 let collected = Collected()
65 try await transport.stream(path) { await collected.append($0) }
66 return await collected.lines
67 }
68
69 func testScriptedStreamFramesLikeNativeHTTP() async throws {
70 let (process, origin) = try server(bodies)
71 defer { process.terminate() }
72 let real = NativeHTTP(origin: origin)
73 for (name, body) in bodies.sorted(by: { $0.key < $1.key }) {
74 let expected = try await lines(real, "/\(name)")
75 let scripted = try await lines(ScriptedTransport([.respond(status: 200, body: body)]), "/\(name)")
76 XCTAssertEqual(scripted, expected, "\(name): scripted framing differs from NativeHTTP")
77 }
78 }
79
80 func testScriptedStreamRefusesWhatNativeHTTPRefuses() async throws {
81 let invalid = Data([0x64, 0x61, 0x74, 0x61, 0x3A, 0xFF, 0x0A])
82 let long = Data(repeating: 0x61, count: 256 * 1024 + 1) + Data("\n".utf8)
83 let (process, origin) = try server(["invalid": invalid, "long": long])
84 defer { process.terminate() }
85 let real = NativeHTTP(origin: origin)
86 for (name, body) in [("invalid", invalid), ("long", long)] {
87 var realError: String?
88 do { _ = try await lines(real, "/\(name)") } catch { realError = error.localizedDescription }
89 XCTAssertNotNil(realError, "\(name): NativeHTTP accepted it")
90 do {
91 _ = try await lines(ScriptedTransport([.respond(status: 200, body: body)]), "/\(name)")
92 XCTFail("\(name): ScriptedTransport accepted what NativeHTTP refuses")
93 } catch { XCTAssertEqual(error.localizedDescription, realError, name) }
94 }
95 }
96}
97#endif
98