Content after a '...' document-end marker begins a new bare document, but the scanner rejected it as an unsupported multi-document stream and the parser expected the document to end at the marker. A stream like '--- scalar / ... / key: value' or 'a / ... / b' is two documents. The scanner now reopens the stream when content follows '...', and parseDocument returns at a '...' so the outer stream loop parses the next document, bare or marked. A trailing '...' with nothing after it still leaves a single document. The single-document composer still rejects a multi-document stream, so 'parse' is unchanged. Two tests that asserted content after '...' is an error are corrected, since the construct is valid per the grammar and the official test-suite. Measured against the official YAML test-suite, parse-level conformance rises from 288/333 (86.5%) to 289/333 (86.8%) with no regression.
324 lines
11 KiB
Swift
324 lines
11 KiB
Swift
@testable import PureYAML
|
|
import Testing
|
|
|
|
@Suite("Stream Parsing")
|
|
struct StreamParsingTests {
|
|
@Test("Parses one implicit document as an indexed stream")
|
|
func test_parseStreamReturnsOneImplicitDocument() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
title: Example
|
|
active: true
|
|
""",
|
|
)
|
|
|
|
#expect(documents == [
|
|
.init(index: 0, value: .mapping(.init([
|
|
.init(key: "title", value: .string("Example")),
|
|
.init(key: "active", value: .bool(true)),
|
|
]))),
|
|
])
|
|
#expect(documents.map(\.index) == [0])
|
|
#expect(!documents.contains(.init(index: 0, value: .string("---"))))
|
|
}
|
|
|
|
@Test("Parses empty explicit documents as null values")
|
|
func test_emptyExplicitDocumentsBecomeNullValues() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
# A document may be null.
|
|
---
|
|
---
|
|
empty:
|
|
canonical: ~
|
|
---
|
|
sparse:
|
|
- ~
|
|
-
|
|
- value
|
|
""",
|
|
)
|
|
|
|
#expect(documents == [
|
|
.init(index: 0, value: .null),
|
|
.init(index: 1, value: .mapping(.init([
|
|
.init(key: "empty", value: .null),
|
|
.init(key: "canonical", value: .null),
|
|
]))),
|
|
.init(index: 2, value: .mapping(.init([
|
|
.init(key: "sparse", value: .sequence([
|
|
.null,
|
|
.null,
|
|
.string("value"),
|
|
])),
|
|
]))),
|
|
])
|
|
#expect(documents.count == 3)
|
|
#expect(documents[0].value != .string(""))
|
|
}
|
|
|
|
@Test("Parses multiple sequence documents")
|
|
func test_multipleSequenceDocuments() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
# Ranking of 1998 home runs
|
|
---
|
|
- Mark McGwire
|
|
- Sammy Sosa
|
|
- Ken Griffey
|
|
|
|
# Team ranking
|
|
---
|
|
- Chicago Cubs
|
|
- St Louis Cardinals
|
|
""",
|
|
)
|
|
|
|
#expect(documents == [
|
|
.init(index: 0, value: .sequence([
|
|
.string("Mark McGwire"),
|
|
.string("Sammy Sosa"),
|
|
.string("Ken Griffey"),
|
|
])),
|
|
.init(index: 1, value: .sequence([
|
|
.string("Chicago Cubs"),
|
|
.string("St Louis Cardinals"),
|
|
])),
|
|
])
|
|
#expect(documents.map(\.index) == [0, 1])
|
|
#expect(!documents.map(\.value).contains(.string("# Team ranking")))
|
|
}
|
|
|
|
@Test("Parses document end markers trailing comments and following starts")
|
|
func test_documentEndMarkersTrailingCommentsAndFollowingStarts() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
---
|
|
time: "20:03:20"
|
|
player: Sammy Sosa
|
|
action: strike (miss)
|
|
...
|
|
# separator comment
|
|
---
|
|
time: "20:03:47"
|
|
player: Sammy Sosa
|
|
action: grand slam
|
|
...
|
|
""",
|
|
)
|
|
|
|
#expect(documents == [
|
|
.init(index: 0, value: .mapping(.init([
|
|
.init(key: "time", value: .string("20:03:20")),
|
|
.init(key: "player", value: .string("Sammy Sosa")),
|
|
.init(key: "action", value: .string("strike (miss)")),
|
|
]))),
|
|
.init(index: 1, value: .mapping(.init([
|
|
.init(key: "time", value: .string("20:03:47")),
|
|
.init(key: "player", value: .string("Sammy Sosa")),
|
|
.init(key: "action", value: .string("grand slam")),
|
|
]))),
|
|
])
|
|
#expect(!documents.map(\.value).contains(.string("separator comment")))
|
|
}
|
|
|
|
@Test("Single document parser accepts one explicit empty document")
|
|
func test_singleDocumentParserAcceptsOneExplicitEmptyDocument() throws {
|
|
#expect(try PureYAML.parse("---") == .null)
|
|
#expect(try PureYAML.parse("---\n...\n# trailing comment") == .null)
|
|
}
|
|
|
|
@Test("Single document parser rejects multiple documents exactly")
|
|
func test_singleDocumentParserRejectsMultipleDocuments() {
|
|
expectParseError(
|
|
"""
|
|
document 1
|
|
---
|
|
document 2
|
|
""",
|
|
.unsupportedMultiDocumentStream(line: 2),
|
|
)
|
|
}
|
|
|
|
@Test("Content after an explicit document end begins a new bare document")
|
|
func test_contentAfterExplicitDocumentEndBeginsBareDocument() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
name: First
|
|
...
|
|
name: Second
|
|
""",
|
|
)
|
|
#expect(documents.count == 2)
|
|
#expect(documents[0].value == .mapping(.init([.init(key: "name", value: .string("First"))])))
|
|
#expect(documents[1].value == .mapping(.init([.init(key: "name", value: .string("Second"))])))
|
|
}
|
|
|
|
@Test("Stream parser rejects extra root content without a document start")
|
|
func test_streamParserRejectsExtraRootContentWithoutDocumentStart() {
|
|
// A complete flow node followed by a bare scalar is extra content and needs an explicit
|
|
// `---` to start a new document. (Two bare plain lines such as `first` / `second` are not
|
|
// extra content; they correctly fold into a single multi-line plain scalar.)
|
|
expectStreamParseError(
|
|
"""
|
|
[1, 2]
|
|
extra
|
|
""",
|
|
.unexpectedToken(
|
|
expected: "document boundary or stream end",
|
|
actual: "scalar value=\"extra\" style=plain",
|
|
line: 2,
|
|
column: 1,
|
|
),
|
|
description: "expected document boundary or stream end at line 2, column 1, found scalar value=\"extra\" style=plain",
|
|
)
|
|
}
|
|
|
|
@Test("Stream parser rejects aliases that cross document boundaries")
|
|
func test_streamParserRejectsCrossDocumentAliases() {
|
|
expectStreamParseError(
|
|
"""
|
|
---
|
|
value: &shared first
|
|
---
|
|
copy: *shared
|
|
""",
|
|
.undefinedAlias(anchor: "shared", line: 4, column: 7),
|
|
description: "undefined alias 'shared' at line 4, column 7",
|
|
)
|
|
}
|
|
}
|
|
|
|
@Suite("Stream Validation")
|
|
struct StreamValidationTests {
|
|
@Test("Validates stream documents with exact document-indexed diagnostics")
|
|
func test_streamValidationPreservesDocumentIndexes() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
---
|
|
title: First
|
|
title: Second
|
|
---
|
|
routes:
|
|
- name: Users
|
|
name: People
|
|
""",
|
|
)
|
|
let expectedIssues = [
|
|
PureYAML.Stream.Issue(
|
|
documentIndex: 0,
|
|
issue: .init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("title")]),
|
|
),
|
|
),
|
|
PureYAML.Stream.Issue(
|
|
documentIndex: 1,
|
|
issue: .init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'name'",
|
|
path: .init([.key("routes"), .index(0), .key("name")]),
|
|
),
|
|
),
|
|
]
|
|
|
|
let result = PureYAML.Validation.Validator().collect(documents)
|
|
#expect(result.issues == expectedIssues)
|
|
#expect(result.errors == expectedIssues)
|
|
#expect(result.warnings.isEmpty)
|
|
#expect(!result.isValid)
|
|
expectStreamValidationError(documents) { collection in
|
|
#expect(collection.issues == expectedIssues)
|
|
#expect(collection.description == """
|
|
document[0]: error: Duplicate mapping key 'title' at $.title
|
|
document[1]: error: Duplicate mapping key 'name' at $.routes[0].name
|
|
""")
|
|
}
|
|
}
|
|
|
|
@Test("Valid stream documents produce no diagnostics")
|
|
func test_validStreamDocumentsProduceNoDiagnostics() throws {
|
|
let documents = try PureYAML.parseStream(
|
|
"""
|
|
---
|
|
title: First
|
|
---
|
|
- one
|
|
- two
|
|
""",
|
|
)
|
|
|
|
let result = PureYAML.Validation.Validator().collect(documents)
|
|
#expect(result == PureYAML.Stream.Result())
|
|
#expect(result.isValid)
|
|
#expect(result.errors.isEmpty)
|
|
#expect(result.warnings.isEmpty)
|
|
#expect(try PureYAML.validate(documents).isEmpty)
|
|
}
|
|
|
|
@Test("Stream validation preserves strict and non-strict warning behavior")
|
|
func test_streamValidationStrictAndNonStrictWarnings() throws {
|
|
let documents = [
|
|
PureYAML.Stream.Document(index: 4, value: .string("legacy")),
|
|
PureYAML.Stream.Document(index: 5, value: .string("modern")),
|
|
]
|
|
let validator = PureYAML.Validation.Validator.blank
|
|
.validating(legacyModeRule(severity: .warning))
|
|
let expectedIssues = [
|
|
PureYAML.Stream.Issue(
|
|
documentIndex: 4,
|
|
issue: .init(
|
|
severity: .warning,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .root,
|
|
),
|
|
),
|
|
]
|
|
|
|
expectStreamValidationError(documents, using: validator) { collection in
|
|
#expect(collection.issues == expectedIssues)
|
|
}
|
|
let warnings = try PureYAML.validate(documents, using: validator, strict: false)
|
|
#expect(warnings == expectedIssues)
|
|
|
|
let modernOnly = [PureYAML.Stream.Document(index: 6, value: .string("modern"))]
|
|
#expect(PureYAML.Validation.Validator.blank
|
|
.validating(legacyModeRule(severity: .warning))
|
|
.collect(modernOnly)
|
|
.issues
|
|
.isEmpty)
|
|
}
|
|
}
|
|
|
|
private func expectStreamParseError(
|
|
_ yaml: String,
|
|
_ expected: PureYAML.Parsing.ParseError,
|
|
description: String,
|
|
) {
|
|
do {
|
|
_ = try PureYAML.parseStream(yaml)
|
|
recordIssue("expected error \(expected)")
|
|
} catch let error as PureYAML.Parsing.ParseError {
|
|
#expect(error == expected)
|
|
#expect(error.description == description)
|
|
} catch {
|
|
recordIssue("unexpected error \(error)")
|
|
}
|
|
}
|
|
|
|
private func expectStreamValidationError(
|
|
_ documents: [PureYAML.Stream.Document],
|
|
using validator: PureYAML.Validation.Validator = .init(),
|
|
check: (PureYAML.Stream.Issue.Collection) -> Void,
|
|
) {
|
|
do {
|
|
try PureYAML.validate(documents, using: validator)
|
|
recordIssue("expected stream validation failure")
|
|
} catch let collection as PureYAML.Stream.Issue.Collection {
|
|
check(collection)
|
|
} catch {
|
|
recordIssue("unexpected error \(error)")
|
|
}
|
|
}
|