PureYAML/Tests/DiagnosticValidationReportTests.swift
Mihaela Mihaljevic 208b892019 feat(parser): harden diagnostics and streaming
Add structured validation diagnostics, JSON and Yams comparison gates, and lazy token/event parsing so production behavior is measured without retaining full intermediate parser arrays.
2026-06-06 07:57:48 +02:00

230 lines
8.7 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Diagnostic Validation Reports")
struct DiagnosticValidationReportTests {
@Test("Valid YAML returns parsed artifacts and an empty diagnostic report")
func test_validYAMLReturnsParsedArtifactsAndEmptyDiagnosticReport() throws {
let yaml = """
title: Ready
metadata:
owner: Tools
"""
let report = PureYAML.diagnosticValidationReport(yaml, file: "valid.yaml")
let value = try PureYAML.parseValidated(yaml, file: "valid.yaml")
#expect(report == PureYAML.Validation.Report())
#expect(value == .mapping(.init([
.init(key: "title", value: .string("Ready")),
.init(key: "metadata", value: .mapping(.init([
.init(key: "owner", value: .string("Tools")),
]))),
])))
}
@Test("Diagnostic reports combine preflight warnings with model validation errors")
func test_diagnosticReportsCombinePreflightWarningsWithModelValidationErrors() {
let report = PureYAML.diagnosticValidationReport(
[
"title: First",
"title: Second",
"note: ok ",
].joined(separator: "\n"),
file: "duplicates.yaml",
)
#expect(report.diagnostics == [
.init(
kind: .parse,
code: "trailingWhitespace",
severity: .warning,
file: "duplicates.yaml",
line: 3,
column: 9,
reason: "trailing whitespace",
),
.init(
kind: .validation,
severity: .error,
file: "duplicates.yaml",
documentIndex: 0,
path: .init([.key("title")]),
reason: "Duplicate mapping key 'title'",
),
])
#expect(!report.isValid)
#expect(report.warnings.count == 1)
#expect(report.errors.count == 1)
}
@Test("Diagnostic reports keep scanning after lines that will make parsing fail")
func test_diagnosticReportsKeepScanningAfterLinesThatWillMakeParsingFail() {
let report = PureYAML.diagnosticValidationReport(
[
"title: \"open",
"summary:Missing",
"\tmetadata:",
].joined(separator: "\n"),
file: "broken.yaml",
)
#expect(report.diagnostics == [
.init(
kind: .parse,
code: "missingMappingSpace",
severity: .error,
file: "broken.yaml",
line: 2,
column: 9,
reason: "missing space after ':' in mapping entry",
),
.init(
kind: .parse,
code: "tabIndentation",
severity: .error,
file: "broken.yaml",
line: 3,
column: 1,
reason: "tab used for indentation; YAML indentation must use spaces",
),
.init(
kind: .parse,
code: "unterminatedQuotedString",
severity: .error,
file: "broken.yaml",
line: 1,
reason: "unterminated quoted string at line 1",
),
])
#expect(report.description == [
"broken.yaml: line 2, column 9: error: parse: missing space after ':' in mapping entry",
"broken.yaml: line 3, column 1: error: parse: tab used for indentation; YAML indentation must use spaces",
"broken.yaml: line 1: error: parse: unterminated quoted string at line 1",
].joined(separator: "\n"))
}
@Test("Report errors carry machine-readable validation bodies")
func test_reportErrorsCarryMachineReadableValidationBodies() throws {
do {
_ = try PureYAML.parseValidated(
[
"title: \"open",
"summary:Missing",
"\tmetadata:",
].joined(separator: "\n"),
file: "broken.yaml",
)
recordIssue("expected validation report error")
} catch let error as PureYAML.Validation.ReportError {
let json = error.report.jsonDescription(title: "Production YAML Validation")
let yaml = error.report.yamlDescription(title: "Production YAML Validation")
#expect(error.report.diagnostics.count == 3)
#expect(json.contains(#""title": "Production YAML Validation""#))
#expect(json.contains(#""code": "missingMappingSpace""#))
#expect(json.contains(#""line": 2"#))
#expect(json.contains(#""column": 9"#))
#expect(json.contains(#""reason": "missing space after ':' in mapping entry""#))
let yamlRoot = try requireMapping(PureYAML.parse(yaml))
let diagnostics = yamlRoot?.sequence("diagnostics")
let firstDiagnostic = diagnostics?.first?.mapping
#expect(yamlRoot?["title"] == PureYAML.Model.Value.string("Production YAML Validation"))
#expect(yamlRoot?["summary"]?.mapping?["valid"] == PureYAML.Model.Value.bool(false))
#expect(yamlRoot?["summary"]?.mapping?["diagnostics"] == PureYAML.Model.Value.int(3))
#expect(yamlRoot?["summary"]?.mapping?["errors"] == PureYAML.Model.Value.int(3))
#expect(yamlRoot?["summary"]?.mapping?["warnings"] == PureYAML.Model.Value.int(0))
#expect(firstDiagnostic?["code"] == PureYAML.Model.Value.string("missingMappingSpace"))
#expect(firstDiagnostic?["line"] == PureYAML.Model.Value.int(2))
#expect(firstDiagnostic?["column"] == PureYAML.Model.Value.int(9))
#expect(firstDiagnostic?["reason"] == PureYAML.Model.Value.string("missing space after ':' in mapping entry"))
} catch {
recordIssue("unexpected error \(error)")
}
}
@Test("Diagnostic batch reports can treat preflight warnings as failures")
func test_diagnosticBatchReportsCanTreatPreflightWarningsAsFailures() {
let report = PureYAML.diagnosticValidationReports(
[
.init(name: "valid.yaml", yaml: "title: Ready"),
.init(name: "warning.yaml", yaml: "title: Ready "),
],
failOnWarnings: true,
)
#expect(report.fileCount == 2)
#expect(report.validCount == 1)
#expect(report.invalidCount == 1)
#expect(report.diagnosticCount == 1)
#expect(report.sourceReports.map(\.invalid) == [false, true])
#expect(report.diagnostics == [
.init(
kind: .parse,
code: "trailingWhitespace",
severity: .warning,
file: "warning.yaml",
line: 1,
column: 13,
reason: "trailing whitespace",
),
])
}
@Test("Malformed production-shaped YAML reports ten or more structured diagnostics")
func test_malformedProductionShapedYAMLReportsTenOrMoreStructuredDiagnostics() {
let report = PureYAML.diagnosticValidationReport(
[
"apiVersion:v1",
"\tmetadata:",
"-bad",
"name: demo ",
"image:nginx",
"\t- item",
"-item",
"version:1",
"ports: [80]\t",
"owner:team",
"title: \"open",
].joined(separator: "\n"),
file: "production-broken.yaml",
)
#expect(report.diagnostics.count == 11)
#expect(report.errors.count == 9)
#expect(report.warnings.count == 2)
#expect(report.diagnostics.map(\.code) == [
"missingMappingSpace",
"tabIndentation",
"missingSequenceSpace",
"trailingWhitespace",
"missingMappingSpace",
"tabIndentation",
"missingSequenceSpace",
"missingMappingSpace",
"trailingWhitespace",
"missingMappingSpace",
"unterminatedQuotedString",
])
#expect(report.diagnostics.first == .init(
kind: .parse,
code: "missingMappingSpace",
severity: .error,
file: "production-broken.yaml",
line: 1,
column: 12,
reason: "missing space after ':' in mapping entry",
))
#expect(report.diagnostics.last == .init(
kind: .parse,
code: "unterminatedQuotedString",
severity: .error,
file: "production-broken.yaml",
line: 11,
reason: "unterminated quoted string at line 11",
))
}
}