Replace the line-based --- heuristic with a scan over PureYAML's resumable token scanner, so a --- inside a quoted scalar can no longer misfire as a document boundary and directives stay attached to the document they introduce. Add a differential suite that checks the new scanner against the old line-based reference across generated streams and the full Geekbench corpus.
163 lines
7.1 KiB
Swift
163 lines
7.1 KiB
Swift
import Foundation
|
|
import PureYAML
|
|
import Testing
|
|
@testable import PureYAMLStreaming
|
|
|
|
@Suite("Resumable document scanner")
|
|
struct DocumentScannerTests {
|
|
/// Grammar-valid streams the scanner must split without loss for any
|
|
/// chunking. Each exercises a distinct boundary shape: explicit markers,
|
|
/// no markers, comments around markers, marker with trailing comment,
|
|
/// block scalar bodies, explicit document ends, empty documents, CRLF,
|
|
/// a non-newline break character, comment-only input, and empty input.
|
|
static let chunkingInputs: [String] = [
|
|
"---\na: 1\n---\nb: 2\n",
|
|
"name: only\n",
|
|
"# note\n---\na: 1\n",
|
|
"---\na: 1\n# trailing\n---\nb: 2\n",
|
|
"a: 1\n--- # comment\nb: 2\n",
|
|
"a: |\n ---\n not a boundary\n---\nb: 2\n",
|
|
"a: 1\n...\nb: 2\n",
|
|
"---\na: 1\n...\n---\nb: 2\n",
|
|
"---\n---\n",
|
|
"a: 1\r\n---\r\nb: 2\r\n",
|
|
"a: \"x\u{2028} y\"\n---\nb: 2\n",
|
|
"# only comments\n\n",
|
|
"",
|
|
]
|
|
|
|
@Test("splits explicit documents at --- markers")
|
|
func splitsExplicitDocuments() throws {
|
|
let segments = try rewiredSegments("---\na: 1\n---\nb: 2\n", chunkSize: 1024)
|
|
#expect(segments == ["---\na: 1\n", "---\nb: 2\n"])
|
|
}
|
|
|
|
@Test("emits identical lossless segments for any chunking", arguments: chunkingInputs)
|
|
func chunkingDoesNotChangeSegments(input: String) throws {
|
|
let wholeFeed = try rewiredSegments(input, chunkSize: max(input.unicodeScalars.count, 1))
|
|
#expect(wholeFeed.joined() == input)
|
|
for chunkSize in [1, 2, 3, 7, 64] {
|
|
let segments = try rewiredSegments(input, chunkSize: chunkSize)
|
|
#expect(segments == wholeFeed, "chunk size \(chunkSize) changed the segments for: \(input.debugDescription)")
|
|
}
|
|
}
|
|
|
|
@Test("attaches leading directives to their document")
|
|
func attachesLeadingDirectives() throws {
|
|
let input = "%YAML 1.2\n---\na: 1\n"
|
|
#expect(try rewiredSegments(input, chunkSize: 3) == [input])
|
|
|
|
let documents = try PureYAMLStreaming.Parser(chunkSize: 3).collectDocuments(data: Data(input.utf8))
|
|
#expect(documents.count == 1)
|
|
#expect(intValue("a", in: documents[0].value) == 1)
|
|
}
|
|
|
|
@Test("splits before directives that follow an explicit document end")
|
|
func splitsBeforeDirectivesAfterDocumentEnd() throws {
|
|
let input = "---\na: 1\n...\n%YAML 1.2\n---\nb: 2\n"
|
|
let segments = try rewiredSegments(input, chunkSize: 5)
|
|
#expect(segments == ["---\na: 1\n...\n", "%YAML 1.2\n---\nb: 2\n"])
|
|
|
|
let documents = try PureYAMLStreaming.Parser(chunkSize: 5).collectDocuments(data: Data(input.utf8))
|
|
#expect(documents.count == 2)
|
|
}
|
|
|
|
@Test("keeps a directive without a preceding document end in the erroneous segment")
|
|
func keepsInvalidDirectiveInSegment() throws {
|
|
// Batch parsing rejects a directive that follows content without an
|
|
// explicit `...`, so the splitter must not carve out a boundary that
|
|
// would make both halves individually valid.
|
|
let input = "a: 1\n%YAML 1.2\n---\nb: 2\n"
|
|
#expect(throws: (any Error).self) {
|
|
try PureYAML.Parsing.Parser().parseStream(input)
|
|
}
|
|
|
|
let segments = try rewiredSegments(input, chunkSize: 4)
|
|
#expect(segments == [input])
|
|
#expect(throws: (any Error).self) {
|
|
try PureYAML.Parsing.Parser().parseStream(segments[0])
|
|
}
|
|
}
|
|
|
|
@Test("rejects a grammar-internal --- like batch parsing instead of splitting")
|
|
func rejectsDocumentMarkerInsideQuotedScalar() throws {
|
|
// The old line-based heuristic cut this quoted scalar in half. The
|
|
// grammar rejects a document indicator inside a quoted scalar, so the
|
|
// token-driven scanner must surface the same rejection batch parsing
|
|
// does, not fabricate a boundary.
|
|
let input = "a: \"x\n--- y\"\nb: 2\n"
|
|
#expect(throws: (any Error).self) {
|
|
try PureYAML.Parsing.Parser().parseStream(input)
|
|
}
|
|
#expect(throws: (any Error).self) {
|
|
_ = try rewiredSegments(input, chunkSize: 4)
|
|
}
|
|
}
|
|
|
|
@Test("emits a boundary confirmable only at end of input")
|
|
func emitsBoundaryAtEndOfInput() throws {
|
|
// A trailing `---` line without a newline is a document start only if
|
|
// no fourth `-` follows, which end of input alone can confirm. The old
|
|
// scanner dropped the first document here.
|
|
var scanner = PureYAMLStreaming.ResumableDocumentScanner()
|
|
let consumed = try scanner.consume("a: 1\n---")
|
|
let finished = try scanner.finish()
|
|
#expect(consumed + finished == ["a: 1\n", "---"])
|
|
|
|
let documents = try PureYAMLStreaming.Parser(chunkSize: 3).collectDocuments(data: Data("a: 1\n---".utf8))
|
|
#expect(documents.count == 2)
|
|
#expect(intValue("a", in: documents[0].value) == 1)
|
|
#expect(documents[1].value == .null)
|
|
}
|
|
|
|
@Test("preserves CRLF line endings across boundaries")
|
|
func preservesCRLF() throws {
|
|
let segments = try rewiredSegments("a: 1\r\n---\r\nb: 2\r\n", chunkSize: 2)
|
|
#expect(segments == ["a: 1\r\n", "---\r\nb: 2\r\n"])
|
|
}
|
|
|
|
@Test("counts non-newline break characters like the token scanner")
|
|
func countsUnicodeBreaks() throws {
|
|
// U+2028 is a line break to the scanner's reader, so the boundary
|
|
// line's index must account for it when mapping back to text. The
|
|
// continuation after the break is indented, as the grammar requires
|
|
// for a multi-line quoted scalar.
|
|
let segments = try rewiredSegments("a: \"x\u{2028} y\"\n---\nb: 2\n", chunkSize: 3)
|
|
#expect(segments == ["a: \"x\u{2028} y\"\n", "---\nb: 2\n"])
|
|
}
|
|
|
|
@Test("lumps a bare document after an explicit end into one segment")
|
|
func lumpsBareDocumentAfterEnd() throws {
|
|
let input = "a: 1\n...\nb: 2\n"
|
|
#expect(try rewiredSegments(input, chunkSize: 4) == [input])
|
|
|
|
let documents = try PureYAMLStreaming.Parser(chunkSize: 4).collectDocuments(data: Data(input.utf8))
|
|
#expect(documents.count == 2)
|
|
}
|
|
|
|
@Test("returns nothing for empty input")
|
|
func returnsNothingForEmptyInput() throws {
|
|
var scanner = PureYAMLStreaming.ResumableDocumentScanner()
|
|
#expect(try scanner.consume("") == [])
|
|
#expect(try scanner.finish() == [])
|
|
}
|
|
|
|
@Test("returns the raw remainder for comment-only input")
|
|
func returnsRemainderForCommentOnlyInput() throws {
|
|
let input = "# only\n\n"
|
|
#expect(try rewiredSegments(input, chunkSize: 2) == [input])
|
|
|
|
let documents = try PureYAMLStreaming.Parser(chunkSize: 2).collectDocuments(data: Data(input.utf8))
|
|
#expect(documents.isEmpty)
|
|
}
|
|
|
|
@Test("wraps document sources in streaming tokens")
|
|
func wrapsDocumentSourcesInStreamingTokens() throws {
|
|
var scanner = PureYAMLStreaming.ResumableDocumentScanner()
|
|
var tokens: [PureYAMLStreaming.StreamingToken] = []
|
|
tokens += try scanner.consumeToken("---\na: 1\n---\n")
|
|
tokens += try scanner.finish().map(PureYAMLStreaming.StreamingToken.documentSource)
|
|
#expect(tokens == [.documentSource("---\na: 1\n"), .documentSource("---\n")])
|
|
}
|
|
}
|