PureYAML/Tests/ParsingEventTests.swift
Mihaela Mihaljevic 561921df42 feat(parsing): route the event API through the ported libyaml engine
parseEvents now runs on the ported scanner+parser instead of the
hand-rolled approximation. Adds 1-based mark conversion at the boundary
(the port stays faithfully 0-based internally), translates the port's
scanner and parser errors into the diagnostic-aware ParseError with
precise codes (unterminatedQuotedString, unterminatedTag, tabIndentation)
and a readable malformed fallback, and retargets the canonical event gate
to track the port's gap set.

The port matches the canonical test.event on 253 of 308 valid cases;
the remaining gaps match libyaml's own behaviour and are the targets for
surpassing libyaml. Golden event and diagnostic expectations are rebased
onto the port where it is faithful or more correct (resolved verbatim
tags, libyaml's stream-end marks, reaching the real unterminated quote).
333 tests pass.
2026-06-24 17:32:29 +02:00

224 lines
8.6 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Parsing Events")
struct ParsingEventTests {
@Test("Event model represents aliases anchors tags and flow style")
func test_eventModelRepresentsAliasesAnchorsTagsAndFlowStyle() {
let mark = PureYAML.Parsing.Mark(line: 2, column: 3, index: 12)
let events: [PureYAML.Parsing.Event] = [
.scalar(value: "value", anchor: "root", tag: "!Thing", style: .singleQuoted, mark: mark),
.sequenceStart(anchor: "items", tag: "!seq", style: .flow, mark: mark),
.alias(anchor: "root", mark: mark),
]
#expect(events.map(\.description) == [
"scalar value=\"value\" anchor=root tag=!Thing style=singleQuoted @2:3@12",
"sequenceStart anchor=items tag=!seq style=flow @2:3@12",
"alias anchor=root @2:3@12",
])
}
@Test("Emits golden events for mapping scalars and scalar styles")
func test_mappingScalarsAndScalarStyles() throws {
let events = try PureYAML.Parsing.Parser().parseEvents(
"""
name: "Example"
single: 'it''s'
missing:
""",
)
#expect(events.map(\.description) == [
"streamStart @1:1@0",
"documentStart explicit=false @1:1@0",
"mappingStart anchor=- tag=- style=block @1:1@0",
"scalar value=\"name\" anchor=- tag=- style=plain @1:1@0",
"scalar value=\"Example\" anchor=- tag=- style=doubleQuoted @1:7@6",
"scalar value=\"single\" anchor=- tag=- style=plain @2:1@16",
"scalar value=\"it's\" anchor=- tag=- style=singleQuoted @2:9@24",
"scalar value=\"missing\" anchor=- tag=- style=plain @3:1@32",
"scalar value=\"\" anchor=- tag=- style=plain @3:9@40",
"mappingEnd @4:1@40",
"documentEnd explicit=false @4:1@40",
"streamEnd @4:1@40",
])
}
@Test("Emits golden events for nested sequences and mappings")
func test_nestedSequencesAndMappings() throws {
let events = try PureYAML.Parsing.Parser().parseEvents(
"""
servers:
- url: /
description: Default
""",
)
#expect(events.map(\.description) == [
"streamStart @1:1@0",
"documentStart explicit=false @1:1@0",
"mappingStart anchor=- tag=- style=block @1:1@0",
"scalar value=\"servers\" anchor=- tag=- style=plain @1:1@0",
"sequenceStart anchor=- tag=- style=block @2:3@11",
"mappingStart anchor=- tag=- style=block @2:5@13",
"scalar value=\"url\" anchor=- tag=- style=plain @2:5@13",
"scalar value=\"/\" anchor=- tag=- style=plain @2:10@18",
"scalar value=\"description\" anchor=- tag=- style=plain @3:5@24",
"scalar value=\"Default\" anchor=- tag=- style=plain @3:18@37",
"mappingEnd @4:1@44",
"sequenceEnd @4:1@44",
"mappingEnd @4:1@44",
"documentEnd explicit=false @4:1@44",
"streamEnd @4:1@44",
])
#expect(!events.map(\.description).contains("scalar value=\"\" anchor=- tag=- style=plain @1:1@0"))
}
@Test("Emits golden events for flow collections aliases anchors and tags")
func test_flowCollectionsAliasesAnchorsAndTags() throws {
let events = try PureYAML.Parsing.Parser().parseEvents(
"""
flow: [!Thing &item "value", *item, {tagged: !<tag:example.com,2026:thing> 'it''s'}]
""",
)
let descriptions = events.map(\.description)
#expect(descriptions == [
"streamStart @1:1@0",
"documentStart explicit=false @1:1@0",
"mappingStart anchor=- tag=- style=block @1:1@0",
"scalar value=\"flow\" anchor=- tag=- style=plain @1:1@0",
"sequenceStart anchor=- tag=- style=flow @1:7@6",
"scalar value=\"value\" anchor=item tag=!Thing style=doubleQuoted @1:8@7",
"alias anchor=item @1:30@29",
"mappingStart anchor=- tag=- style=flow @1:37@36",
"scalar value=\"tagged\" anchor=- tag=- style=plain @1:38@37",
"scalar value=\"it's\" anchor=- tag=tag:example.com,2026:thing style=singleQuoted @1:46@45",
"mappingEnd @1:83@82",
"sequenceEnd @1:84@83",
"mappingEnd @2:1@84",
"documentEnd explicit=false @2:1@84",
"streamEnd @2:1@84",
])
#expect(!descriptions.contains("sequenceStart anchor=- tag=- style=block @1:7@6"))
#expect(!descriptions.contains { $0.contains("it''s") })
}
@Test("Parses JSON-style flow mapping pairs")
func test_jsonStyleFlowMappingPairs() throws {
let root = try requireMapping(PureYAML.parse(
"""
value:
{
"version" : "7",
"fields" : [
{
"key" : "productType",
"required" : true
}
]
}
""",
))
let value = root?.mapping("value")
let field = value?.sequence("fields")?.first?.mapping
#expect(value?["version"] == .string("7"))
#expect(field?["key"] == .string("productType"))
#expect(field?["required"] == .bool(true))
#expect(value?["missing"] == nil)
}
@Test("Emits golden events for block scalar headers")
func test_blockScalarHeaders() throws {
let events = try PureYAML.Parsing.Parser().parseEvents(
"""
text: |
one
two
stripped: |-
one
two
folded: >
one
two
""",
)
let descriptions = events.map(\.description)
#expect(descriptions == [
"streamStart @1:1@0",
"documentStart explicit=false @1:1@0",
"mappingStart anchor=- tag=- style=block @1:1@0",
"scalar value=\"text\" anchor=- tag=- style=plain @1:1@0",
"scalar value=\"one\\ntwo\\n\" anchor=- tag=- style=literal @1:7@6",
"scalar value=\"stripped\" anchor=- tag=- style=plain @4:1@20",
"scalar value=\"one\\ntwo\" anchor=- tag=- style=literal @4:11@30",
"scalar value=\"folded\" anchor=- tag=- style=plain @7:1@45",
"scalar value=\"one two\\n\" anchor=- tag=- style=folded @7:9@53",
"mappingEnd @10:1@66",
"documentEnd explicit=false @10:1@66",
"streamEnd @10:1@66",
])
#expect(!descriptions.contains { $0.contains("one\\ntwo\\n") && $0.contains("@4:11@30") })
}
@Test("Reports parse-event errors")
func test_errorReporting() {
expectError(PureYAML.Parsing.ParseError.malformed(
reason: "while parsing a block mapping, did not find expected key",
line: 1,
column: 1,
)) {
try PureYAML.Parsing.Parser().parseEvents("root:\n child: yes\n wrong: no")
}
expectError(PureYAML.Parsing.ParseError.malformed(
reason: "while parsing a block mapping, did not find expected key",
line: 2,
column: 3,
)) {
try PureYAML.Parsing.Parser().parseEvents("root:\n child: value\n dangling")
}
expectParseEventError(
"items: [one, two",
PureYAML.Parsing.ParseError.malformed(
reason: "while parsing a flow sequence, did not find expected ',' or ']'",
line: 1,
column: 8,
),
description: "while parsing a flow sequence, did not find expected ',' or ']' at line 1, column 8",
)
}
@Test("Emits no document for empty input")
func test_emptyInputEmitsNoDocument() throws {
// YAML 1.2: a stream with no content (empty or comment-only) has zero
// documents, matching libyaml and the test suite. The value API maps this
// empty stream to a single null value separately.
let events = try PureYAML.Parsing.Parser().parseEvents("")
#expect(events.map(\.description) == [
"streamStart @1:1@0",
"streamEnd @1:1@0",
])
}
}
private func expectParseEventError(
_ yaml: String,
_ expected: PureYAML.Parsing.ParseError,
description: String,
) {
do {
_ = try PureYAML.Parsing.Parser().parseEvents(yaml)
recordIssue("expected error \(expected)")
} catch let error as PureYAML.Parsing.ParseError {
#expect(error == expected)
#expect(error.description == description)
} catch {
recordIssue("unexpected error \(error)")
}
}