Add Scanner.scanStep(), the first increment of true chunked input streaming (#4): Reader gains isFinal/append(_:)/markFinal(), and peek(_:) flags ranDry instead of asserting end of input when the buffer runs out mid-fetch and more chunks may still arrive. scanStep() snapshots the scanner before each fetchNextToken() and restores it whenever a fetch runs dry, so a construct spanning a chunk boundary is retried after more input lands rather than misread as an early end of input. Every existing call site keeps the default isFinal: true, so scanAll() and batch parsing are unchanged; ranDry can never be set when isFinal is true, by construction. A token already queued is not always safe to release, since libyaml's simple-key mechanism can still splice a KEY token in front of it, so scanStep() ports libyaml's yaml_parser_fetch_more_tokens release condition. Proof: StreamingScannerDifferentialTests.swift drives scanStep() at several chunk sizes over representative cases always, and over the entire yaml-test-suite manifest (402 cases) behind PUREYAML_RUN_YAML_TEST_SUITE=1, asserting the token sequence matches scanAll() exactly. That proof caught a real bug: lastTokenForbidsInlineSequence() read indentEngine.tokens.last, which goes empty once tokens are released incrementally, silently breaking the key: - a rejection. Fixed by tracking IndentEngine.lastToken explicitly, kept in sync with tokens.last so batch behavior (which never dequeues) is unchanged. Not yet wired into PureYAMLStreaming; scanStep() and its supporting types stay internal until that integration is designed. Also broaden the real-seed mutation property suite (#6) with the three mutation classes it named but didn't yet cover: invalidControlCharacter, mixedTabSpaceIndentation, and truncatedFlowCollection. The test now asserts diagnostic quality, not just pass/fail: line must fall within the mutated document, and column, when present, must be 1-based (some ParseError cases, like tabIndentation, are line-only by design).
122 lines
4.8 KiB
Swift
122 lines
4.8 KiB
Swift
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",
|
|
])
|
|
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)")
|
|
}
|
|
}
|