Zum Inhalt

apple/PhysarumApp/Tests/RemoteFramingTests.swift

import XCTest
import PhysarumEngine
@testable import Physarum

/// The link layer reads arbitrary byte chunks off the TCP stream; the framing decoder must
/// reassemble whole messages regardless of how the bytes are split across reads.
final class RemoteFramingTests: XCTestCase {
    private var sample: [RemoteMessage] {
        let p = PresetCatalog.loadAll()[0].params
        var f = AudioFactors(); f[.moveSpeed] = 1.2; f[.exposure] = 0.7
        return [.hello(.init(selectedID: "a", params: p, cycling: false)), .intent(.next), .params(p),
                .audioFactors(AudioFactorFrame(f))]
    }

    func testEncodeThenDecodeStreamYieldsMessages() throws {
        let msgs = sample
        var stream = Data()
        for m in msgs { stream.append(try RemoteFraming.encode(m)) }
        var decoder = RemoteFraming.Decoder()
        var out: [RemoteMessage] = []
        // Feed the stream in two arbitrary chunks to prove buffering across reads.
        out += try decoder.consume(stream.prefix(stream.count / 3))
        out += try decoder.consume(stream.suffix(stream.count - stream.count / 3))
        XCTAssertEqual(out, msgs)
    }

    func testByteByByteFeedReassembles() throws {
        let msgs = sample
        var stream = Data()
        for m in msgs { stream.append(try RemoteFraming.encode(m)) }
        var decoder = RemoteFraming.Decoder()
        var out: [RemoteMessage] = []
        // Worst-case fragmentation: one byte per read. Nothing emits until a full frame lands.
        for byte in stream { out += try decoder.consume(Data([byte])) }
        XCTAssertEqual(out, msgs)
    }

    // A frame with a valid length header but a non-decodable body must be skipped, not left in the
    // buffer to be misread as the next length header — a single bad frame must not poison the stream.
    func testMalformedFrameIsSkippedNotPoisoning() throws {
        let p = PresetCatalog.loadAll()[0].params
        var stream = Data()
        stream.append(try RemoteFraming.encode(.intent(.next)))   // valid
        var badLen = UInt32(3).bigEndian                          // 3-byte body of garbage (not JSON)
        stream.append(Data(bytes: &badLen, count: 4))
        stream.append(Data([0xFF, 0xFE, 0xFD]))
        stream.append(try RemoteFraming.encode(.params(p)))       // valid
        var decoder = RemoteFraming.Decoder()
        let out = try decoder.consume(stream)
        XCTAssertEqual(out, [.intent(.next), .params(p)])         // bad frame dropped, both valid frames recovered
    }

    func testZeroLengthFrameIsSkipped() throws {
        var stream = Data()
        stream.append(try RemoteFraming.encode(.intent(.previous)))
        stream.append(Data([0, 0, 0, 0]))                         // zero-length frame
        stream.append(try RemoteFraming.encode(.intent(.next)))
        var decoder = RemoteFraming.Decoder()
        let out = try decoder.consume(stream)
        XCTAssertEqual(out, [.intent(.previous), .intent(.next)])
    }

    // A corrupt/oversized length prefix can't be resynced (we don't know where the next frame starts);
    // the decoder throws so the link tears the connection down and reconnects.
    func testOversizedLengthThrows() {
        var huge = UInt32(64 * 1024 * 1024).bigEndian            // 64 MB, far above the frame ceiling
        let header = Data(bytes: &huge, count: 4)
        var decoder = RemoteFraming.Decoder()
        XCTAssertThrowsError(try decoder.consume(header))
    }
}