The scalar event now carries the scanner token's end mark, so a located scalar's range spans the whole token (end exclusive, one past the last character) instead of being start-anchored. This gives consumers exact highlight widths and is the prerequisite for surgical, position-based text edits. Only the scalar event case gained a field; the value tree, its equality, and the event dump (which compares value and style, not marks) are unchanged, so the canonical-event and whole-suite gates stay green (343 tests).
224 lines
8.6 KiB
Swift
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, endMark: 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)")
|
|
}
|
|
}
|