import Foundation @testable import PureYAML import Testing /// Differential proof for the resumable `Scanner.scanStep()` (issue #4's first /// increment: a checkpoint-and-retry wrapper around the existing batch /// `fetchNextToken()`, see `LibYAML.Scanner.Streaming.swift`). The scanner is a /// pure value type, so `scanStep()` reuses every fetch function unmodified; the /// only new behavior is (1) `Reader` flags `ranDry` instead of asserting end of /// input when `!isFinal`, and (2) tokens are released one at a time only when no /// possible simple key still targets the queue front. Neither is exercised by /// `scanAll()`, so this is genuinely new code path, not a refactor, and needs its /// own proof: driving the same input through `scanStep()` at several chunk sizes /// must reproduce `scanAll()`'s token array (or thrown error) exactly. @Suite("Streaming scanner differential") struct StreamingScannerDifferentialTests { /// A handful of representative always-on cases so `swift test` gets real /// coverage without the opt-in whole-suite flag: a plain scalar split /// mid-token, a block mapping, a flow collection, an anchor/alias pair, and /// a deliberately malformed input (an unterminated quoted scalar). @Test("Representative cases match scanAll() at several chunk sizes", arguments: [ "title: Example API", """ title: Example servers: - url: / """, "flow: {a: 1, b: [2, 3]}", """ base: &anchor value: 1 derived: *anchor """, "block: |\n line one\n line two\n", "title: \"unterminated", "&a a: b\nc: &d d\n", ]) func test_representativeCasesMatchScanAllAtSeveralChunkSizes(yaml: String) throws { try assertStreamingMatchesBatch(yaml) } @Test("Every yaml-test-suite case matches scanAll() at several chunk sizes") func test_wholeSuiteMatchesScanAllAtSeveralChunkSizes() throws { guard ProcessInfo.processInfo.environment["PUREYAML_RUN_YAML_TEST_SUITE"] == "1" else { return } let manifest = try YAMLTestSuiteManifest.load() var mismatches: [String] = [] for testCase in manifest.cases { let yaml = try testCase.loadInput() do { try assertStreamingMatchesBatch(yaml) } catch { mismatches.append("\(testCase.id): \(error)") } } #expect(mismatches.isEmpty, "streaming/batch mismatches (\(mismatches.count)): \(mismatches.prefix(10).joined(separator: "; "))") } } /// Runs the batch scanner once as the reference, then the streaming scanner at /// several chunk sizes (including feeding the whole input as a single "chunk", /// which should degenerate to one dry run followed by success), and asserts each /// streaming run reproduces the reference token array or thrown error exactly. private func assertStreamingMatchesBatch(_ yaml: String) throws { let reference = batchScan(yaml) for chunkSize in [1, 2, 3, 7, 64, yaml.unicodeScalars.count + 1] { let observed = try streamingScan(yaml, chunkSize: chunkSize) #expect(observed == reference, "chunk size \(chunkSize) diverged from scanAll() for: \(yaml.debugDescription)") } } private enum ScanResult: Equatable { case tokens([PureYAML.Parsing.LibYAML.Token]) case threw(String) } private func batchScan(_ yaml: String) -> ScanResult { var scanner = PureYAML.Parsing.LibYAML.Scanner(yaml) do { return try .tokens(scanner.scanAll()) } catch { return .threw("\(error)") } } /// Drives `scanStep()` over `yaml` split into `chunkSize`-scalar pieces, feeding /// one chunk whenever the scanner reports `.needMoreInput`, and marking the /// reader final once every chunk has been appended. private func streamingScan(_ yaml: String, chunkSize: Int) throws -> ScanResult { let scalars = Array(yaml.unicodeScalars) var chunks: [String] = [] var index = 0 while index < scalars.count { let end = min(index + chunkSize, scalars.count) chunks.append(String(String.UnicodeScalarView(scalars[index ..< end]))) index = end } var scanner = PureYAML.Parsing.LibYAML.Scanner("", isFinal: false) var nextChunk = 0 var tokens: [PureYAML.Parsing.LibYAML.Token] = [] do { while true { switch try scanner.scanStep() { case let .token(token): tokens.append(token) case .done: return .tokens(tokens) case .needMoreInput: if nextChunk < chunks.count { scanner.reader.append(chunks[nextChunk]) nextChunk += 1 } else { scanner.reader.markFinal() } } } } catch { return .threw("\(error)") } }