PureYAML/Tests/LibYAMLParserTests.swift
Mihaela Mihaljevic 521062d974 feat(parsing): port libyaml parser state machine to Swift
Ports parser.c: the one-event-at-a-time state machine that turns the
scanner's token stream into the YAML event stream. Covers stream and
document framing, directive processing with %TAG handle resolution and
default handles, the node property/tag/anchor production, block and
indentless sequences, block mappings, and flow sequences and mappings
including the single-pair `?` mapping forms.

Output verified against the libyaml reference event dumper: the empty
mapping value, %TAG resolution, and shorthand tags all match byte for
byte. 331 tests, no regression.
2026-06-24 17:05:59 +02:00

159 lines
5.1 KiB
Swift

@testable import PureYAML
import Testing
/// End-to-end event production through the ported parser state machine. Events are
/// rendered to the YAML test-suite "yeast" format (mark-independent) so the
/// expectations read like the canonical `test.event` files.
@Suite("LibYAML Parser")
struct LibYAMLParserTests {
private func events(_ input: String) throws -> [String] {
var parser = try PureYAML.Parsing.LibYAML.Parser(input)
return try parser.parse().map(render)
}
private func render(_ event: PureYAML.Parsing.Event) -> String {
switch event {
case .streamStart:
"+STR"
case .streamEnd:
"-STR"
case let .documentStart(explicit, _):
explicit ? "+DOC ---" : "+DOC"
case let .documentEnd(explicit, _):
explicit ? "-DOC ..." : "-DOC"
case let .sequenceStart(anchor, tag, style, _):
"+SEQ" + (style == .flow ? " []" : "") + properties(anchor, tag)
case .sequenceEnd:
"-SEQ"
case let .mappingStart(anchor, tag, style, _):
"+MAP" + (style == .flow ? " {}" : "") + properties(anchor, tag)
case .mappingEnd:
"-MAP"
case let .scalar(value, anchor, tag, style, _):
"=VAL" + properties(anchor, tag) + " " + styleChar(style) + escape(value)
case let .alias(anchor, _):
"=ALI *" + anchor
}
}
private func properties(_ anchor: String?, _ tag: String?) -> String {
(anchor.map { " &" + $0 } ?? "") + (tag.map { " <" + $0 + ">" } ?? "")
}
private func styleChar(_ style: PureYAML.Parsing.ScalarStyle) -> String {
switch style {
case .plain: ":"
case .singleQuoted: "'"
case .doubleQuoted: "\""
case .literal: "|"
case .folded: ">"
}
}
private func escape(_ value: String) -> String {
var output = ""
for scalar in value.unicodeScalars {
switch scalar {
case "\\": output += "\\\\"
case "\n": output += "\\n"
case "\t": output += "\\t"
case "\r": output += "\\r"
default: output.unicodeScalars.append(scalar)
}
}
return output
}
@Test("a bare root scalar")
func test_rootScalar() throws {
#expect(try events("x\n") == ["+STR", "+DOC", "=VAL :x", "-DOC", "-STR"])
}
@Test("a block sequence")
func test_blockSequence() throws {
#expect(try events("- a\n- b\n") == [
"+STR", "+DOC", "+SEQ", "=VAL :a", "=VAL :b", "-SEQ", "-DOC", "-STR",
])
}
@Test("a block mapping")
func test_blockMapping() throws {
#expect(try events("a: b\n") == [
"+STR", "+DOC", "+MAP", "=VAL :a", "=VAL :b", "-MAP", "-DOC", "-STR",
])
}
@Test("a mapping nested in a sequence entry with an empty value")
func test_nestedMappingEmptyValue() throws {
// The original block-mapping bug: `- a:` / ` b: c` is {a: null, b: c}.
#expect(try events("- a:\n b: c\n") == [
"+STR", "+DOC", "+SEQ", "+MAP",
"=VAL :a", "=VAL :",
"=VAL :b", "=VAL :c",
"-MAP", "-SEQ", "-DOC", "-STR",
])
}
@Test("a flow sequence")
func test_flowSequence() throws {
#expect(try events("[a, b]\n") == [
"+STR", "+DOC", "+SEQ []", "=VAL :a", "=VAL :b", "-SEQ", "-DOC", "-STR",
])
}
@Test("a flow mapping")
func test_flowMapping() throws {
#expect(try events("{a: b}\n") == [
"+STR", "+DOC", "+MAP {}", "=VAL :a", "=VAL :b", "-MAP", "-DOC", "-STR",
])
}
@Test("an anchor and an alias")
func test_anchorAlias() throws {
#expect(try events("a: &x 1\nb: *x\n") == [
"+STR", "+DOC", "+MAP",
"=VAL :a", "=VAL &x :1",
"=VAL :b", "=ALI *x",
"-MAP", "-DOC", "-STR",
])
}
@Test("a shorthand tag resolves against the default !! handle")
func test_shorthandTag() throws {
#expect(try events("!!str x\n") == [
"+STR", "+DOC", "=VAL <tag:yaml.org,2002:str> :x", "-DOC", "-STR",
])
}
@Test("a custom %TAG directive resolves a shorthand")
func test_tagDirective() throws {
#expect(try events("%TAG !e! tag:example.com,2000:\n---\n!e!foo x\n") == [
"+STR", "+DOC ---", "=VAL <tag:example.com,2000:foo> :x", "-DOC", "-STR",
])
}
@Test("a version directive opens an explicit document")
func test_versionDirective() throws {
#expect(try events("%YAML 1.2\n---\nx\n") == [
"+STR", "+DOC ---", "=VAL :x", "-DOC", "-STR",
])
}
@Test("multiple explicit documents")
func test_multipleDocuments() throws {
#expect(try events("---\na\n---\nb\n") == [
"+STR",
"+DOC ---", "=VAL :a", "-DOC",
"+DOC ---", "=VAL :b", "-DOC",
"-STR",
])
}
@Test("a quoted scalar is not plain-implicit")
func test_quotedScalar() throws {
#expect(try events("\"x\"\n") == [
"+STR", "+DOC", "=VAL \"x", "-DOC", "-STR",
])
}
}