Harden parser against real YAML fixtures
This commit is contained in:
parent
906f0c454d
commit
bc54528353
1
.gitignore
vendored
1
.gitignore
vendored
@ -72,6 +72,7 @@
|
|||||||
# Tooling logs (cron, hooks, local scripts)
|
# Tooling logs (cron, hooks, local scripts)
|
||||||
*.log
|
*.log
|
||||||
**/*.log
|
**/*.log
|
||||||
|
.derived-data-log-*
|
||||||
|
|
||||||
# Local SQLite DB created by autoMigrate at first boot.
|
# Local SQLite DB created by autoMigrate at first boot.
|
||||||
# Codeweaver scaffolds (template-time/bookings/crm/inventory) all default
|
# Codeweaver scaffolds (template-time/bookings/crm/inventory) all default
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
--wrapparameters before-first
|
--wrapparameters before-first
|
||||||
--wrapcollections before-first
|
--wrapcollections before-first
|
||||||
--self remove
|
--self remove
|
||||||
|
--exclude .build,DerivedData
|
||||||
|
|
||||||
# PureYAML uses OpenAPIKit-style `test_...` method names with Swift Testing.
|
# PureYAML uses OpenAPIKit-style `test_...` method names with Swift Testing.
|
||||||
--disable swiftTestingTestCaseNames
|
--disable swiftTestingTestCaseNames
|
||||||
|
|||||||
@ -7,6 +7,7 @@ opt_in_rules:
|
|||||||
- force_unwrapping
|
- force_unwrapping
|
||||||
excluded:
|
excluded:
|
||||||
- .build
|
- .build
|
||||||
|
- DerivedData
|
||||||
force_unwrapping: error
|
force_unwrapping: error
|
||||||
force_try: error
|
force_try: error
|
||||||
force_cast: error
|
force_cast: error
|
||||||
|
|||||||
@ -30,11 +30,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Add first-class complex mapping keys through `Model.Pair.keyNode`, including
|
- Add first-class complex mapping keys through `Model.Pair.keyNode`, including
|
||||||
parsing, deterministic validation paths, duplicate-key checks, and block/flow
|
parsing, deterministic validation paths, duplicate-key checks, and block/flow
|
||||||
dumping for sequence and mapping keys.
|
dumping for sequence and mapping keys.
|
||||||
|
- Add a real-world YAML fixture corpus with representative assertions for
|
||||||
|
short, medium, and very large documents, including OpenAPI and Kubernetes
|
||||||
|
examples.
|
||||||
|
- Add deliberately failing YAML fixtures that demonstrate exact validation
|
||||||
|
diagnostics, strict failures, and non-strict warning returns.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Tokenize YAML document start and end markers so scanner, event-parser, and
|
- Tokenize YAML document start and end markers so scanner, event-parser, and
|
||||||
stream tests can pin exact document-boundary behavior.
|
stream tests can pin exact document-boundary behavior.
|
||||||
|
- Harden block mapping and multiline plain-scalar parsing around nested
|
||||||
|
sequences, compact mapping entries, explicit complex keys, tagged keys, and
|
||||||
|
lower-column mapping siblings.
|
||||||
- Keep quoted `"<<":` and explicitly string-tagged `!!str <<` keys as ordinary
|
- Keep quoted `"<<":` and explicitly string-tagged `!!str <<` keys as ordinary
|
||||||
string keys while merge syntax is expanded during parsing.
|
string keys while merge syntax is expanded during parsing.
|
||||||
- Keep Foundation-backed tags such as `!!timestamp` and `!!binary` as ordinary
|
- Keep Foundation-backed tags such as `!!timestamp` and `!!binary` as ordinary
|
||||||
|
|||||||
@ -20,6 +20,9 @@ let package = Package(
|
|||||||
name: "PureYAMLTests",
|
name: "PureYAMLTests",
|
||||||
dependencies: ["PureYAML"],
|
dependencies: ["PureYAML"],
|
||||||
path: "Tests",
|
path: "Tests",
|
||||||
|
resources: [
|
||||||
|
.copy("Fixtures"),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -37,6 +37,13 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
|| character == "}"
|
|| character == "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isBlockMappingValueBoundary(_ character: Character?) -> Bool {
|
||||||
|
guard let character else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return character == " " || isLineBreak(character)
|
||||||
|
}
|
||||||
|
|
||||||
func isTokenTerminator(_ character: Character) -> Bool {
|
func isTokenTerminator(_ character: Character) -> Bool {
|
||||||
character == " "
|
character == " "
|
||||||
|| isLineBreak(character)
|
|| isLineBreak(character)
|
||||||
@ -48,6 +55,29 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
|| character == "#"
|
|| character == "#"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canStartAnchorName(_ character: Character?) -> Bool {
|
||||||
|
guard let character else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !isTokenTerminator(character)
|
||||||
|
&& character != "*"
|
||||||
|
&& character != "&"
|
||||||
|
&& character != "!"
|
||||||
|
&& character != "\""
|
||||||
|
&& character != "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasNodeOnSameLine(afterCurrentPositionIn reader: PureYAML.Parsing.Reader) -> Bool {
|
||||||
|
var offset = 0
|
||||||
|
while reader.peek(offset: offset) == " " {
|
||||||
|
offset += 1
|
||||||
|
}
|
||||||
|
guard let character = reader.peek(offset: offset) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !isLineBreak(character) && character != "#"
|
||||||
|
}
|
||||||
|
|
||||||
func isFlowDelimiter(_ character: Character) -> Bool {
|
func isFlowDelimiter(_ character: Character) -> Bool {
|
||||||
character == ","
|
character == ","
|
||||||
|| character == "["
|
|| character == "["
|
||||||
@ -74,6 +104,91 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
return String(value[..<end])
|
return String(value[..<end])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scanPlainScalarContinuations(
|
||||||
|
after firstLine: String,
|
||||||
|
state: inout State,
|
||||||
|
) -> String {
|
||||||
|
var value = firstLine
|
||||||
|
while true {
|
||||||
|
var probe = state.reader
|
||||||
|
guard isLineBreak(probe.peek()) else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
probe.advance()
|
||||||
|
var blankLineCount = 0
|
||||||
|
while true {
|
||||||
|
var blankProbe = probe
|
||||||
|
_ = blankProbe.consume { character in
|
||||||
|
character == " "
|
||||||
|
}
|
||||||
|
guard isLineBreak(blankProbe.peek()) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
blankProbe.advance()
|
||||||
|
probe = blankProbe
|
||||||
|
blankLineCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = probe.consume { character in
|
||||||
|
character == " "
|
||||||
|
}
|
||||||
|
let indentationWidth = probe.mark.column - 1
|
||||||
|
|
||||||
|
guard !probe.isAtEnd, !isLineBreak(probe.peek()), probe.peek() != "#" else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
guard !isBlockEntry(probe) else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseIndentation = plainScalarContinuationBaseIndentation(state: state)
|
||||||
|
guard indentationWidth > baseIndentation else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
var continuationState = state
|
||||||
|
continuationState.reader = probe
|
||||||
|
let continuation = scanPlainScalarContinuationLine(&continuationState)
|
||||||
|
guard !isAtMappingValueIndicator(state: continuationState) else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
state = continuationState
|
||||||
|
if blankLineCount == 0 {
|
||||||
|
value += " "
|
||||||
|
} else {
|
||||||
|
value += String(repeating: "\n", count: blankLineCount)
|
||||||
|
}
|
||||||
|
value += continuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainScalarContinuationBaseIndentation(state: State) -> Int {
|
||||||
|
state.indentation.last ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanPlainScalarContinuationLine(_ state: inout State) -> String {
|
||||||
|
var value = ""
|
||||||
|
while let character = state.reader.peek(), !isLineBreak(character) {
|
||||||
|
if character == " ", state.reader.peek(offset: 1) == "#" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if character == "#", value.isEmpty || value.last == " " {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if state.flowDepth > 0, isFlowDelimiter(character) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
value.append(character)
|
||||||
|
state.reader.advance()
|
||||||
|
}
|
||||||
|
return trimTrailingSpaces(value)
|
||||||
|
}
|
||||||
|
|
||||||
func skipLine(_ state: inout State) {
|
func skipLine(_ state: inout State) {
|
||||||
_ = state.reader.consume { character in
|
_ = state.reader.consume { character in
|
||||||
!isLineBreak(character)
|
!isLineBreak(character)
|
||||||
@ -108,7 +223,7 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
if character == "#", accumulated.isEmpty || accumulated.last == " " {
|
if character == "#", accumulated.isEmpty || accumulated.last == " " {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if character == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) {
|
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if state.flowDepth > 0, isFlowDelimiter(character) {
|
if state.flowDepth > 0, isFlowDelimiter(character) {
|
||||||
@ -116,4 +231,15 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldEndPlainScalarAtMappingValue(state: State) -> Bool {
|
||||||
|
if state.flowDepth > 0 {
|
||||||
|
return isMappingValueBoundary(state.reader.peek(offset: 1))
|
||||||
|
}
|
||||||
|
return isBlockMappingValueBoundary(state.reader.peek(offset: 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAtMappingValueIndicator(state: State) -> Bool {
|
||||||
|
state.reader.peek() == ":" && shouldEndPlainScalarAtMappingValue(state: state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
Sources/Parsing/Scanner.Indentation.swift
Normal file
153
Sources/Parsing/Scanner.Indentation.swift
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
extension PureYAML.Parsing.Scanner {
|
||||||
|
func scanIndentation(_ state: inout State) throws {
|
||||||
|
let start = state.reader.mark
|
||||||
|
let width = try scanIndentationWidth(&state)
|
||||||
|
guard shouldProcessIndentation(state) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = state.reader.mark
|
||||||
|
let current = state.indentation.last ?? 0
|
||||||
|
if consumeBlockScalarContentIndentation(width: width, state: &state) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let indicator = state.pendingBlockScalarIndentationIndicator {
|
||||||
|
try applyExplicitBlockScalarIndentation(
|
||||||
|
indicator: indicator,
|
||||||
|
width: width,
|
||||||
|
current: current,
|
||||||
|
marks: (start: start, end: end),
|
||||||
|
state: &state,
|
||||||
|
)
|
||||||
|
} else if width > current {
|
||||||
|
applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
|
||||||
|
} else if width < current {
|
||||||
|
try applyDedentation(width: width, start: start, end: end, state: &state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneImplicitIndentation(&state)
|
||||||
|
clearPendingBlockScalarIndentation(&state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanIndentationWidth(_ state: inout State) throws -> Int {
|
||||||
|
var width = 0
|
||||||
|
while let character = state.reader.peek() {
|
||||||
|
if character == " " {
|
||||||
|
width += 1
|
||||||
|
state.reader.advance()
|
||||||
|
} else if character == "\t" {
|
||||||
|
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
|
||||||
|
} else {
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldProcessIndentation(_ state: State) -> Bool {
|
||||||
|
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard state.reader.peek() != "#" else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return state.flowDepth == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeBlockScalarContentIndentation(
|
||||||
|
width: Int,
|
||||||
|
state: inout State,
|
||||||
|
) -> Bool {
|
||||||
|
guard let blockScalarIndentation = state.blockScalarIndentation else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard width < blockScalarIndentation else {
|
||||||
|
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
state.blockScalarIndentation = nil
|
||||||
|
state.blockScalarContentPrefix = ""
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyExplicitBlockScalarIndentation(
|
||||||
|
indicator: Int,
|
||||||
|
width: Int,
|
||||||
|
current: Int,
|
||||||
|
marks: (start: PureYAML.Parsing.Mark, end: PureYAML.Parsing.Mark),
|
||||||
|
state: inout State,
|
||||||
|
) throws {
|
||||||
|
let blockScalarIndentation = current + indicator
|
||||||
|
guard width >= blockScalarIndentation else {
|
||||||
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: marks.end.line)
|
||||||
|
}
|
||||||
|
state.indentation.append(blockScalarIndentation)
|
||||||
|
state.append(.indent(width: blockScalarIndentation), mark: marks.start, endMark: marks.end)
|
||||||
|
state.blockScalarIndentation = blockScalarIndentation
|
||||||
|
state.blockScalarContentPrefix = String(repeating: " ", count: width - blockScalarIndentation)
|
||||||
|
clearPendingBlockScalarIndentation(&state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyIncreasedIndentation(
|
||||||
|
width: Int,
|
||||||
|
start: PureYAML.Parsing.Mark,
|
||||||
|
end: PureYAML.Parsing.Mark,
|
||||||
|
state: inout State,
|
||||||
|
) {
|
||||||
|
state.indentation.append(width)
|
||||||
|
state.append(.indent(width: width), mark: start, endMark: end)
|
||||||
|
guard state.pendingBlockScalarIndentation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.blockScalarIndentation = width
|
||||||
|
state.blockScalarContentPrefix = ""
|
||||||
|
clearPendingBlockScalarIndentation(&state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDedentation(
|
||||||
|
width: Int,
|
||||||
|
start: PureYAML.Parsing.Mark,
|
||||||
|
end: PureYAML.Parsing.Mark,
|
||||||
|
state: inout State,
|
||||||
|
) throws {
|
||||||
|
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
|
||||||
|
state.indentation.removeLast()
|
||||||
|
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
|
||||||
|
}
|
||||||
|
if canUseImplicitIndentation(width: width, state: state) {
|
||||||
|
state.indentation.append(width)
|
||||||
|
state.append(.indent(width: width), mark: start, endMark: end)
|
||||||
|
}
|
||||||
|
guard width == state.indentation.last else {
|
||||||
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canUseImplicitIndentation(
|
||||||
|
width: Int,
|
||||||
|
state: State,
|
||||||
|
) -> Bool {
|
||||||
|
width != state.indentation.last
|
||||||
|
&& width > (state.indentation.last ?? 0)
|
||||||
|
&& state.validImplicitIndentation.contains(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneImplicitIndentation(_ state: inout State) {
|
||||||
|
let current = state.indentation.last ?? 0
|
||||||
|
state.validImplicitIndentation = state.validImplicitIndentation.filter { $0 <= current }
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearPendingBlockScalarIndentation(_ state: inout State) {
|
||||||
|
state.pendingBlockScalarIndentation = false
|
||||||
|
state.pendingBlockScalarIndentationIndicator = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeIndentation(_ state: inout State) {
|
||||||
|
let mark = state.reader.mark
|
||||||
|
while state.indentation.count > 1 {
|
||||||
|
state.indentation.removeLast()
|
||||||
|
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,11 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
var hasDocumentStartMarker = false
|
var hasDocumentStartMarker = false
|
||||||
var isDocumentClosed = false
|
var isDocumentClosed = false
|
||||||
var flowDepth = 0
|
var flowDepth = 0
|
||||||
|
var validImplicitIndentation: Set<Int> = []
|
||||||
|
var pendingBlockScalarIndentation = false
|
||||||
|
var pendingBlockScalarIndentationIndicator: Int?
|
||||||
|
var blockScalarIndentation: Int?
|
||||||
|
var blockScalarContentPrefix = ""
|
||||||
var tagHandles = [
|
var tagHandles = [
|
||||||
"!": "!",
|
"!": "!",
|
||||||
"!!": "tag:yaml.org,2002:",
|
"!!": "tag:yaml.org,2002:",
|
||||||
@ -79,60 +84,16 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension PureYAML.Parsing.Scanner {
|
extension PureYAML.Parsing.Scanner {
|
||||||
func scanIndentation(_ state: inout State) throws {
|
|
||||||
let start = state.reader.mark
|
|
||||||
var width = 0
|
|
||||||
while let character = state.reader.peek() {
|
|
||||||
if character == " " {
|
|
||||||
width += 1
|
|
||||||
state.reader.advance()
|
|
||||||
} else if character == "\t" {
|
|
||||||
throw PureYAML.Parsing.ParseError.tabIndentation(line: state.reader.mark.line)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard state.reader.peek() != "#" else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard state.flowDepth == 0 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = state.reader.mark
|
|
||||||
let current = state.indentation.last ?? 0
|
|
||||||
if width > current {
|
|
||||||
state.indentation.append(width)
|
|
||||||
state.append(.indent(width: width), mark: start, endMark: end)
|
|
||||||
} else if width < current {
|
|
||||||
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
|
|
||||||
state.indentation.removeLast()
|
|
||||||
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
|
|
||||||
}
|
|
||||||
guard width == state.indentation.last else {
|
|
||||||
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func closeIndentation(_ state: inout State) {
|
|
||||||
let mark = state.reader.mark
|
|
||||||
while state.indentation.count > 1 {
|
|
||||||
state.indentation.removeLast()
|
|
||||||
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanToken(_ state: inout State) throws {
|
func scanToken(_ state: inout State) throws {
|
||||||
skipSeparation(&state)
|
skipSeparation(&state)
|
||||||
guard let character = state.reader.peek(), !isLineBreak(character) else {
|
guard let character = state.reader.peek(), !isLineBreak(character) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.blockScalarIndentation != nil {
|
||||||
|
scanBlockScalarContent(&state)
|
||||||
|
return
|
||||||
|
}
|
||||||
if try scanLineIndicatorToken(character, state: &state) {
|
if try scanLineIndicatorToken(character, state: &state) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -261,6 +222,9 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
let start = state.reader.mark
|
let start = state.reader.mark
|
||||||
state.reader.advance()
|
state.reader.advance()
|
||||||
state.append(kind, mark: start, endMark: state.reader.mark)
|
state.append(kind, mark: start, endMark: state.reader.mark)
|
||||||
|
if case .blockEntry = kind, hasNodeOnSameLine(afterCurrentPositionIn: state.reader) {
|
||||||
|
state.validImplicitIndentation.insert(start.column + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanComment(_ state: inout State) {
|
func scanComment(_ state: inout State) {
|
||||||
@ -316,10 +280,13 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
) {
|
) {
|
||||||
let start = state.reader.mark
|
let start = state.reader.mark
|
||||||
var chomping = PureYAML.Parsing.BlockScalarChomping.clip
|
var chomping = PureYAML.Parsing.BlockScalarChomping.clip
|
||||||
|
var indentationIndicator: Int?
|
||||||
state.reader.advance()
|
state.reader.advance()
|
||||||
while let character = state.reader.peek(), isBlockScalarHeaderCharacter(character) {
|
while let character = state.reader.peek(), isBlockScalarHeaderCharacter(character) {
|
||||||
if character == "-" {
|
if character == "-" {
|
||||||
chomping = .strip
|
chomping = .strip
|
||||||
|
} else if character.isNumber {
|
||||||
|
indentationIndicator = Int(String(character))
|
||||||
}
|
}
|
||||||
state.reader.advance()
|
state.reader.advance()
|
||||||
}
|
}
|
||||||
@ -328,6 +295,17 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
mark: start,
|
mark: start,
|
||||||
endMark: state.reader.mark,
|
endMark: state.reader.mark,
|
||||||
)
|
)
|
||||||
|
state.pendingBlockScalarIndentation = true
|
||||||
|
state.pendingBlockScalarIndentationIndicator = indentationIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanBlockScalarContent(_ state: inout State) {
|
||||||
|
let start = state.reader.mark
|
||||||
|
let value = state.blockScalarContentPrefix + state.reader.consume { character in
|
||||||
|
!isLineBreak(character)
|
||||||
|
}
|
||||||
|
state.blockScalarContentPrefix = ""
|
||||||
|
state.append(.scalar(value: value, style: .plain), mark: start, endMark: state.reader.mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanNamedToken(
|
func scanNamedToken(
|
||||||
@ -383,18 +361,26 @@ extension PureYAML.Parsing.Scanner {
|
|||||||
value.append(character)
|
value.append(character)
|
||||||
state.reader.advance()
|
state.reader.advance()
|
||||||
}
|
}
|
||||||
let trimmed = trimTrailingSpaces(value)
|
var trimmed = trimTrailingSpaces(value)
|
||||||
appendMappingKeyIfNeeded(start: start, state: &state)
|
let isMappingKey = appendMappingKeyIfNeeded(start: start, state: &state)
|
||||||
|
if !isMappingKey {
|
||||||
|
trimmed = scanPlainScalarContinuations(
|
||||||
|
after: trimmed,
|
||||||
|
state: &state,
|
||||||
|
)
|
||||||
|
}
|
||||||
state.append(.scalar(value: trimmed, style: .plain), mark: start, endMark: state.reader.mark)
|
state.append(.scalar(value: trimmed, style: .plain), mark: start, endMark: state.reader.mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
func appendMappingKeyIfNeeded(
|
func appendMappingKeyIfNeeded(
|
||||||
start: PureYAML.Parsing.Mark,
|
start: PureYAML.Parsing.Mark,
|
||||||
state: inout State,
|
state: inout State,
|
||||||
) {
|
) -> Bool {
|
||||||
guard state.reader.peek() == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) else {
|
guard state.reader.peek() == ":", isMappingValueBoundary(state.reader.peek(offset: 1)) else {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
state.append(.mappingKey, mark: start, endMark: start)
|
state.append(.mappingKey, mark: start, endMark: start)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,14 @@ extension PureYAML.Parsing {
|
|||||||
return tokens[previousIndex]
|
return tokens[previousIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func peek(offset: Int = 1) -> Token? {
|
||||||
|
let targetIndex = index + offset
|
||||||
|
guard tokens.indices.contains(targetIndex) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tokens[targetIndex]
|
||||||
|
}
|
||||||
|
|
||||||
mutating func advance() -> Token? {
|
mutating func advance() -> Token? {
|
||||||
guard let token = current else {
|
guard let token = current else {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
extension PureYAML.Parsing.TokenEventParser {
|
extension PureYAML.Parsing.TokenEventParser {
|
||||||
mutating func parseBlockSequence(
|
mutating func parseBlockSequence(
|
||||||
properties: PureYAML.Parsing.NodeProperties,
|
properties: PureYAML.Parsing.NodeProperties,
|
||||||
|
allowMappingKeyTerminator: Bool = false,
|
||||||
) throws -> PureYAML.Parsing.NodeResult {
|
) throws -> PureYAML.Parsing.NodeResult {
|
||||||
let start = try expect("block sequence entry") { $0.isBlockEntry }
|
let start = try expect("block sequence entry") { $0.isBlockEntry }
|
||||||
let startMark = properties.mark ?? start.mark
|
let startMark = properties.mark ?? start.mark
|
||||||
@ -12,12 +13,24 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
mark: startMark,
|
mark: startMark,
|
||||||
))
|
))
|
||||||
|
|
||||||
try parseBlockSequenceItem(after: start, endMark: &endMark)
|
try parseBlockSequenceItem(
|
||||||
|
after: start,
|
||||||
|
endMark: &endMark,
|
||||||
|
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
||||||
|
)
|
||||||
|
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: start.mark.column - 1)
|
||||||
|
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: start.mark.column - 1)
|
||||||
while cursor.current?.kind.isBlockEntry == true {
|
while cursor.current?.kind.isBlockEntry == true {
|
||||||
let entry = try expect("block sequence entry") { $0.isBlockEntry }
|
let entry = try expect("block sequence entry") { $0.isBlockEntry }
|
||||||
try parseBlockSequenceItem(after: entry, endMark: &endMark)
|
try parseBlockSequenceItem(
|
||||||
|
after: entry,
|
||||||
|
endMark: &endMark,
|
||||||
|
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
||||||
|
)
|
||||||
|
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: entry.mark.column - 1)
|
||||||
|
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: entry.mark.column - 1)
|
||||||
}
|
}
|
||||||
if let current = cursor.current, current.kind.isMappingKey {
|
if let current = cursor.current, current.kind.isMappingKey, !allowMappingKeyTerminator {
|
||||||
throw PureYAML.Parsing.ParseError.mixedCollectionStyles(line: current.mark.line)
|
throw PureYAML.Parsing.ParseError.mixedCollectionStyles(line: current.mark.line)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,9 +38,28 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
|
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func consumeSequenceItemBoundaryDedent(sequenceIndentWidth: Int) throws {
|
||||||
|
guard dedentWidth(cursor.current) == sequenceIndentWidth,
|
||||||
|
cursor.peek()?.kind.isBlockEntry == true
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func consumeSequenceItemBoundaryIndent(sequenceIndentWidth: Int) throws {
|
||||||
|
guard indentWidth(cursor.current) == sequenceIndentWidth,
|
||||||
|
cursor.peek()?.kind.isBlockEntry == true
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = try expect("indent") { $0.isIndent }
|
||||||
|
}
|
||||||
|
|
||||||
mutating func parseBlockSequenceItem(
|
mutating func parseBlockSequenceItem(
|
||||||
after entry: PureYAML.Parsing.Token,
|
after entry: PureYAML.Parsing.Token,
|
||||||
endMark: inout PureYAML.Parsing.Mark,
|
endMark: inout PureYAML.Parsing.Mark,
|
||||||
|
allowMappingKeyTerminator: Bool,
|
||||||
) throws {
|
) throws {
|
||||||
if hasNodeOnSameLine(after: entry) {
|
if hasNodeOnSameLine(after: entry) {
|
||||||
let properties = consumeProperties()
|
let properties = consumeProperties()
|
||||||
@ -41,6 +73,12 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
endMark = try parseBlockMapping(
|
endMark = try parseBlockMapping(
|
||||||
properties: properties,
|
properties: properties,
|
||||||
includeIndentedContinuation: true,
|
includeIndentedContinuation: true,
|
||||||
|
minimumColumn: entry.mark.column + 1,
|
||||||
|
).endMark
|
||||||
|
} else if cursor.current?.kind.isBlockEntry == true {
|
||||||
|
endMark = try parseBlockSequence(
|
||||||
|
properties: properties,
|
||||||
|
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
||||||
).endMark
|
).endMark
|
||||||
} else {
|
} else {
|
||||||
endMark = try parseNode(properties: properties).endMark
|
endMark = try parseNode(properties: properties).endMark
|
||||||
@ -59,8 +97,12 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
mutating func parseBlockMapping(
|
mutating func parseBlockMapping(
|
||||||
properties: PureYAML.Parsing.NodeProperties,
|
properties: PureYAML.Parsing.NodeProperties,
|
||||||
includeIndentedContinuation: Bool,
|
includeIndentedContinuation: Bool,
|
||||||
|
minimumColumn: Int? = nil,
|
||||||
) throws -> PureYAML.Parsing.NodeResult {
|
) throws -> PureYAML.Parsing.NodeResult {
|
||||||
let startMark = properties.mark ?? cursor.current?.mark ?? .start
|
let startMark = properties.mark ?? cursor.current?.mark ?? .start
|
||||||
|
let tokenColumn = cursor.current?.mark.column ?? startMark.column
|
||||||
|
let contentColumn = min(startMark.column, tokenColumn)
|
||||||
|
let boundaryColumn = minimumColumn ?? contentColumn
|
||||||
var endMark = startMark
|
var endMark = startMark
|
||||||
events.append(.mappingStart(
|
events.append(.mappingStart(
|
||||||
anchor: properties.anchor,
|
anchor: properties.anchor,
|
||||||
@ -68,20 +110,13 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
style: .block,
|
style: .block,
|
||||||
mark: startMark,
|
mark: startMark,
|
||||||
))
|
))
|
||||||
while cursor.current?.kind.isMappingKey == true {
|
|
||||||
endMark = try parseMappingPair().endMark
|
try parseBlockMappingPairs(
|
||||||
}
|
boundaryColumn: boundaryColumn,
|
||||||
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
|
includeIndentedContinuation: includeIndentedContinuation,
|
||||||
_ = try expect("indent") { $0.isIndent }
|
endMark: &endMark,
|
||||||
guard cursor.current?.kind.isMappingKey == true else {
|
)
|
||||||
throw unexpectedToken(expected: "mapping key")
|
if let current = cursor.current, !canEndBlockMapping(at: current, minimumColumn: boundaryColumn) {
|
||||||
}
|
|
||||||
while cursor.current?.kind.isMappingKey == true {
|
|
||||||
endMark = try parseMappingPair().endMark
|
|
||||||
}
|
|
||||||
_ = try expect("dedent") { $0.isDedent }
|
|
||||||
}
|
|
||||||
if let current = cursor.current, !canEndBlockMapping(at: current) {
|
|
||||||
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
||||||
expected: "mapping key",
|
expected: "mapping key",
|
||||||
actual: current.kind.description,
|
actual: current.kind.description,
|
||||||
@ -93,6 +128,57 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func parseBlockMappingPairs(
|
||||||
|
boundaryColumn: Int,
|
||||||
|
includeIndentedContinuation: Bool,
|
||||||
|
endMark: inout PureYAML.Parsing.Mark,
|
||||||
|
) throws {
|
||||||
|
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
||||||
|
endMark = try parseMappingPairAndBoundaries(
|
||||||
|
boundaryColumn: boundaryColumn,
|
||||||
|
includeIndentedContinuation: includeIndentedContinuation,
|
||||||
|
).endMark
|
||||||
|
}
|
||||||
|
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
|
||||||
|
endMark = try parseIndentedBlockMappingPairs(boundaryColumn: boundaryColumn).endMark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func parseIndentedBlockMappingPairs(
|
||||||
|
boundaryColumn: Int,
|
||||||
|
) throws -> PureYAML.Parsing.NodeResult {
|
||||||
|
_ = try expect("indent") { $0.isIndent }
|
||||||
|
guard cursor.current?.kind.isMappingKey == true else {
|
||||||
|
throw unexpectedToken(expected: "mapping key")
|
||||||
|
}
|
||||||
|
var endMark = cursor.current?.mark ?? .start
|
||||||
|
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
||||||
|
endMark = try parseMappingPairAndBoundaries(
|
||||||
|
boundaryColumn: boundaryColumn,
|
||||||
|
includeIndentedContinuation: true,
|
||||||
|
).endMark
|
||||||
|
}
|
||||||
|
if !canEndBlockMapping(at: cursor.current, minimumColumn: boundaryColumn) {
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
}
|
||||||
|
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func parseMappingPairAndBoundaries(
|
||||||
|
boundaryColumn: Int,
|
||||||
|
includeIndentedContinuation: Bool,
|
||||||
|
) throws -> PureYAML.Parsing.NodeResult {
|
||||||
|
let result = try parseMappingPair()
|
||||||
|
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
||||||
|
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
||||||
|
if includeIndentedContinuation {
|
||||||
|
try consumeDedents(atOrAbove: boundaryColumn)
|
||||||
|
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
||||||
|
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
mutating func parseBlockScalar(
|
mutating func parseBlockScalar(
|
||||||
properties: PureYAML.Parsing.NodeProperties,
|
properties: PureYAML.Parsing.NodeProperties,
|
||||||
) throws -> PureYAML.Parsing.NodeResult {
|
) throws -> PureYAML.Parsing.NodeResult {
|
||||||
|
|||||||
@ -12,19 +12,71 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
let properties = consumeProperties()
|
let properties = consumeProperties()
|
||||||
if cursor.current?.kind.isIndent == true {
|
if cursor.current?.kind.isIndent == true {
|
||||||
_ = try expect("indent") { $0.isIndent }
|
_ = try expect("indent") { $0.isIndent }
|
||||||
let result = try parseNode(properties: properties)
|
let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true {
|
||||||
_ = try expect("dedent") { $0.isDedent }
|
try parseBlockSequence(
|
||||||
|
properties: properties,
|
||||||
|
allowMappingKeyTerminator: true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
try parseNode(properties: properties)
|
||||||
|
}
|
||||||
|
if cursor.current?.kind.isDedent == true {
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
} else if cursor.current?.kind.isMappingKey != true {
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
if properties == .none, canParsePlainScalarWithContinuation(after: valueIndicator) {
|
||||||
|
return try parsePlainScalarWithContinuation(properties: properties)
|
||||||
|
}
|
||||||
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
|
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
|
||||||
return try parseNode(properties: properties)
|
return try parseNode(properties: properties)
|
||||||
}
|
}
|
||||||
|
if cursor.current?.kind.isBlockEntry == true {
|
||||||
|
return try parseBlockSequence(
|
||||||
|
properties: properties,
|
||||||
|
allowMappingKeyTerminator: true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let mark = valueIndicator.endMark
|
let mark = valueIndicator.endMark
|
||||||
events.append(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
|
events.append(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
|
||||||
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func parsePlainScalarWithContinuation(
|
||||||
|
properties: PureYAML.Parsing.NodeProperties,
|
||||||
|
) throws -> PureYAML.Parsing.NodeResult {
|
||||||
|
let first = try expect("plain scalar") { kind in
|
||||||
|
if case .scalar(_, .plain) = kind {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var value = try decodedScalar(from: first)
|
||||||
|
var endMark = first.endMark
|
||||||
|
|
||||||
|
_ = try expect("indent") { $0.isIndent }
|
||||||
|
while isScalar(cursor.current) {
|
||||||
|
let token = cursor.advance() ?? first
|
||||||
|
let continuation = try plainContinuationText(from: token)
|
||||||
|
value += " " + continuation
|
||||||
|
endMark = token.endMark
|
||||||
|
}
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
|
||||||
|
let mark = properties.mark ?? first.mark
|
||||||
|
events.append(.scalar(
|
||||||
|
value: value,
|
||||||
|
anchor: properties.anchor,
|
||||||
|
tag: properties.tag,
|
||||||
|
style: .plain,
|
||||||
|
mark: mark,
|
||||||
|
))
|
||||||
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: endMark)
|
||||||
|
}
|
||||||
|
|
||||||
mutating func consumeProperties() -> PureYAML.Parsing.NodeProperties {
|
mutating func consumeProperties() -> PureYAML.Parsing.NodeProperties {
|
||||||
var properties = PureYAML.Parsing.NodeProperties.none
|
var properties = PureYAML.Parsing.NodeProperties.none
|
||||||
while let token = cursor.current {
|
while let token = cursor.current {
|
||||||
@ -61,6 +113,27 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodedScalar(from token: PureYAML.Parsing.Token) throws -> String {
|
||||||
|
guard case let .scalar(value, style) = token.kind else {
|
||||||
|
throw unexpectedToken(expected: "scalar")
|
||||||
|
}
|
||||||
|
return try decodeScalar(value, style: style, line: token.mark.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainContinuationText(from token: PureYAML.Parsing.Token) throws -> String {
|
||||||
|
guard case let .scalar(value, style) = token.kind else {
|
||||||
|
throw unexpectedToken(expected: "scalar")
|
||||||
|
}
|
||||||
|
switch style {
|
||||||
|
case .doubleQuoted:
|
||||||
|
return "\"\(value)\""
|
||||||
|
case .singleQuoted:
|
||||||
|
return "'\(value)'"
|
||||||
|
case .folded, .literal, .plain:
|
||||||
|
return try decodeScalar(value, style: style, line: token.mark.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
|
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
|
||||||
guard let current = cursor.current, !current.kind.isTerminator else {
|
guard let current = cursor.current, !current.kind.isTerminator else {
|
||||||
return false
|
return false
|
||||||
@ -68,15 +141,129 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
return current.mark.line == token.mark.line
|
return current.mark.line == token.mark.line
|
||||||
}
|
}
|
||||||
|
|
||||||
func canEndBlockMapping(at token: PureYAML.Parsing.Token) -> Bool {
|
func canParsePlainScalarWithContinuation(after token: PureYAML.Parsing.Token) -> Bool {
|
||||||
token.kind.isBlockEntry
|
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard cursor.current?.mark.line == token.mark.line else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard cursor.peek()?.kind.isIndent == true else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard isScalar(cursor.peek(offset: 2)) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func canParsePlainScalarContinuation() -> Bool {
|
||||||
|
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard cursor.peek()?.kind.isIndent == true else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard isScalar(cursor.peek(offset: 2)) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isScalar(_ token: PureYAML.Parsing.Token?) -> Bool {
|
||||||
|
guard case .scalar = token?.kind else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func consumeDedents(atOrAbove minimumColumn: Int) throws {
|
||||||
|
while let width = dedentWidth(cursor.current), width >= minimumColumn {
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func consumeMappingSiblingBoundaryDedents(minimumColumn: Int) throws {
|
||||||
|
while canConsumeMappingSiblingBoundaryDedent(minimumColumn: minimumColumn) {
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canConsumeMappingSiblingBoundaryDedent(minimumColumn: Int) -> Bool {
|
||||||
|
guard let width = dedentWidth(cursor.current),
|
||||||
|
let next = cursor.peek(),
|
||||||
|
next.kind.isMappingKey,
|
||||||
|
next.mark.column >= minimumColumn
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return width == next.mark.column - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func consumeMappingSiblingBoundaryReindent(minimumColumn: Int) throws {
|
||||||
|
guard let dedent = dedentWidth(cursor.current),
|
||||||
|
dedent < minimumColumn,
|
||||||
|
indentWidth(cursor.peek()) == minimumColumn,
|
||||||
|
let mappingKey = cursor.peek(offset: 2),
|
||||||
|
mappingKey.kind.isMappingKey,
|
||||||
|
mappingKey.mark.column == minimumColumn + 1
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = try expect("dedent") { $0.isDedent }
|
||||||
|
_ = try expect("indent") { $0.isIndent }
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
||||||
|
guard case let .dedent(width) = token?.kind else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func indentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
||||||
|
guard case let .indent(width) = token?.kind else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func canContinueBlockMapping(minimumColumn: Int?) -> Bool {
|
||||||
|
guard let current = cursor.current, current.kind.isMappingKey else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard let minimumColumn else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return current.mark.column >= minimumColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
func canEndBlockMapping(
|
||||||
|
at token: PureYAML.Parsing.Token?,
|
||||||
|
minimumColumn: Int? = nil,
|
||||||
|
) -> Bool {
|
||||||
|
guard let token else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return token.kind.isBlockEntry
|
||||||
|| token.kind.isDedent
|
|| token.kind.isDedent
|
||||||
|| token.kind.isDocumentEnd
|
|| token.kind.isDocumentEnd
|
||||||
|| token.kind.isDocumentStart
|
|| token.kind.isDocumentStart
|
||||||
|| token.kind.isFlowTerminator
|
|| token.kind.isFlowTerminator
|
||||||
|
|| isLowerColumnMappingKey(token, minimumColumn: minimumColumn)
|
||||||
|| token.kind.isStreamEnd
|
|| token.kind.isStreamEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isLowerColumnMappingKey(
|
||||||
|
_ token: PureYAML.Parsing.Token?,
|
||||||
|
minimumColumn: Int?,
|
||||||
|
) -> Bool {
|
||||||
|
guard let token, token.kind.isMappingKey, let minimumColumn else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return token.mark.column < minimumColumn
|
||||||
|
}
|
||||||
|
|
||||||
mutating func expect(
|
mutating func expect(
|
||||||
_ expected: String,
|
_ expected: String,
|
||||||
matching: (PureYAML.Parsing.TokenKind) -> Bool,
|
matching: (PureYAML.Parsing.TokenKind) -> Bool,
|
||||||
|
|||||||
@ -62,6 +62,10 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
|
|
||||||
let result = try parseNode()
|
let result = try parseNode()
|
||||||
var endMark = result.endMark
|
var endMark = result.endMark
|
||||||
|
while cursor.current?.kind.isDedent == true {
|
||||||
|
let dedent = try expect("dedent") { $0.isDedent }
|
||||||
|
endMark = dedent.endMark
|
||||||
|
}
|
||||||
if cursor.current?.kind.isDocumentEnd == true {
|
if cursor.current?.kind.isDocumentEnd == true {
|
||||||
let end = try expect("document end") { $0.isDocumentEnd }
|
let end = try expect("document end") { $0.isDocumentEnd }
|
||||||
endMark = end.endMark
|
endMark = end.endMark
|
||||||
@ -129,6 +133,9 @@ extension PureYAML.Parsing.TokenEventParser {
|
|||||||
includeIndentedContinuation: false,
|
includeIndentedContinuation: false,
|
||||||
)
|
)
|
||||||
case let .scalar(value, style):
|
case let .scalar(value, style):
|
||||||
|
if canParsePlainScalarContinuation() {
|
||||||
|
return try parsePlainScalarWithContinuation(properties: properties)
|
||||||
|
}
|
||||||
_ = cursor.advance()
|
_ = cursor.advance()
|
||||||
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
|
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
|
||||||
let mark = properties.mark ?? token.mark
|
let mark = properties.mark ?? token.mark
|
||||||
|
|||||||
38
Tests/Fixtures/MANIFEST.md
Normal file
38
Tests/Fixtures/MANIFEST.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Real YAML Fixture Manifest
|
||||||
|
|
||||||
|
These fixtures are vendored test inputs. They are pinned to immutable GitHub
|
||||||
|
commit SHAs so compatibility failures can be reproduced without network access.
|
||||||
|
The files are not implementation source and are used only by
|
||||||
|
`RealYAMLFixtureTests`.
|
||||||
|
|
||||||
|
| Local file | Size | Lines | Bytes | Upstream license | Pinned source |
|
||||||
|
|---|---:|---:|---:|---|---|
|
||||||
|
| `real-yaml/kubernetes-simple-pod.yaml` | very short | 12 | 186 | MIT | `ContainerSolutions/kubernetes-examples@fa6e84542f4350b97991bfffae553d248953e443/Pod/simple.yaml` |
|
||||||
|
| `real-yaml/kustomize-wordpress.yaml` | very short | 18 | 262 | Apache-2.0 | `kubernetes-sigs/kustomize@313aacedd3adcb6564fd362bcef52f5aa3abef85/examples/wordpress/kustomization.yaml` |
|
||||||
|
| `real-yaml/docker-compose-nginx-golang-postgres.yaml` | short | 48 | 852 | CC0-1.0 | `docker/awesome-compose@8ded149643ef0ba21fce5a5d39894b39fff6c7ee/nginx-golang-postgres/compose.yaml` |
|
||||||
|
| `real-yaml/github-actions-swift-format.yml` | short | 84 | 3591 | Apache-2.0 | `swiftlang/swift-format@fffd8df6153456838039aae4b9b901f6f31f5687/.github/workflows/pull_request.yml` |
|
||||||
|
| `real-yaml/openapi-petstore.yaml` | short | 119 | 2768 | Apache-2.0 | `OAI/OpenAPI-Specification@18c3204bc55c8c75fe25a039a3cf085f0e732208/_archive_/schemas/v3.0/pass/petstore.yaml` |
|
||||||
|
| `real-yaml/prow-cluster-api-presubmits.yaml` | medium | 427 | 14213 | Apache-2.0 | `kubernetes/test-infra@9001df901ba65b1b74d16be5cc910e68a5f35b91/config/jobs/kubernetes-sigs/cluster-api/cluster-api-release-1-13-presubmits.yaml` |
|
||||||
|
| `real-yaml/prometheus-conf-good.yml` | medium | 481 | 11562 | Apache-2.0 | `prometheus/prometheus@5e3c892bfc1209b739562f885a8cfa17c6cd7fa2/config/testdata/conf.good.yml` |
|
||||||
|
| `real-yaml/cert-manager-certificate-crd.yaml` | large | 883 | 44059 | Apache-2.0 | `cert-manager/cert-manager@59c5f5361a5b1cd2d0ca84cd9983a3beac3846bd/deploy/crds/cert-manager.io_certificates.yaml` |
|
||||||
|
| `real-yaml/cert-manager-values.yaml` | large | 1759 | 65536 | Apache-2.0 | `cert-manager/cert-manager@59c5f5361a5b1cd2d0ca84cd9983a3beac3846bd/deploy/charts/cert-manager/values.yaml` |
|
||||||
|
| `real-yaml/github-rest-api.yaml` | extreme | 257280 | 9549118 | MIT | `github/rest-api-description@0d4e436c347b444cd71b4eb1bd73948fd51c3402/descriptions-next/api.github.com/api.github.com.yaml` |
|
||||||
|
|
||||||
|
Selection covers GitHub Actions, Docker Compose, Kubernetes manifests,
|
||||||
|
Kustomize, OpenAPI, Prow jobs, Prometheus config, Helm values, CRDs, and one
|
||||||
|
extreme OpenAPI document. If a fixture is parseable by mature YAML tools, this
|
||||||
|
suite expects PureYAML to parse it or to pin an exact unsupported feature error
|
||||||
|
with a linked issue.
|
||||||
|
|
||||||
|
## Validation Failure Fixtures
|
||||||
|
|
||||||
|
These fixtures are intentionally valid YAML that should fail validation. They
|
||||||
|
exist to demonstrate exact validation output for user-facing diagnostic reports,
|
||||||
|
strict throwing behavior, non-strict warning returns, path rendering, required
|
||||||
|
presence checks, and required absence checks.
|
||||||
|
|
||||||
|
| Local file | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `validation-failures/duplicate-keys.yaml` | Default duplicate-key diagnostics with root and punctuation-heavy nested keys. |
|
||||||
|
| `validation-failures/missing-required-and-forbidden.yaml` | Custom required-presence plus required-absence diagnostics in one document. |
|
||||||
|
| `validation-failures/warnings.yaml` | Warning-only diagnostics that throw in strict mode and return in non-strict mode. |
|
||||||
883
Tests/Fixtures/real-yaml/cert-manager-certificate-crd.yaml
Normal file
883
Tests/Fixtures/real-yaml/cert-manager-certificate-crd.yaml
Normal file
@ -0,0 +1,883 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.21.0
|
||||||
|
name: certificates.cert-manager.io
|
||||||
|
spec:
|
||||||
|
group: cert-manager.io
|
||||||
|
names:
|
||||||
|
categories:
|
||||||
|
- cert-manager
|
||||||
|
kind: Certificate
|
||||||
|
listKind: CertificateList
|
||||||
|
plural: certificates
|
||||||
|
shortNames:
|
||||||
|
- cert
|
||||||
|
- certs
|
||||||
|
singular: certificate
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .status.conditions[?(@.type == "Ready")].status
|
||||||
|
name: Ready
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.secretName
|
||||||
|
name: Secret
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.issuerRef.name
|
||||||
|
name: Issuer
|
||||||
|
priority: 1
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.conditions[?(@.type == "Ready")].message
|
||||||
|
name: Status
|
||||||
|
priority: 1
|
||||||
|
type: string
|
||||||
|
- description: CreationTimestamp is a timestamp representing the server time when
|
||||||
|
this object was created. It is not guaranteed to be set in happens-before
|
||||||
|
order across separate operations. Clients may not set this value. It is represented
|
||||||
|
in RFC3339 form and is in UTC.
|
||||||
|
jsonPath: .metadata.creationTimestamp
|
||||||
|
name: Age
|
||||||
|
type: date
|
||||||
|
name: v1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: |-
|
||||||
|
A Certificate resource should be created to ensure an up to date and signed
|
||||||
|
X.509 certificate is stored in the Kubernetes Secret resource named in `spec.secretName`.
|
||||||
|
|
||||||
|
The stored certificate will be renewed before it expires (as configured by `spec.renewBefore`).
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
Specification of the desired state of the Certificate resource.
|
||||||
|
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
|
||||||
|
properties:
|
||||||
|
additionalOutputFormats:
|
||||||
|
description: |-
|
||||||
|
Defines extra output formats of the private key and signed certificate chain
|
||||||
|
to be written to this Certificate's target Secret.
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
CertificateAdditionalOutputFormat defines an additional output format of a
|
||||||
|
Certificate resource. These contain supplementary data formats of the signed
|
||||||
|
certificate chain and paired private key.
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
description: |-
|
||||||
|
Type is the name of the format type that should be written to the
|
||||||
|
Certificate's target Secret.
|
||||||
|
enum:
|
||||||
|
- DER
|
||||||
|
- CombinedPEM
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
commonName:
|
||||||
|
description: |-
|
||||||
|
Requested common name X509 certificate subject attribute.
|
||||||
|
More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6
|
||||||
|
NOTE: TLS clients will ignore this value when any subject alternative name is
|
||||||
|
set (see https://tools.ietf.org/html/rfc6125#section-6.4.4).
|
||||||
|
|
||||||
|
Should have a length of 64 characters or fewer to avoid generating invalid CSRs.
|
||||||
|
Cannot be set if the `literalSubject` field is set.
|
||||||
|
type: string
|
||||||
|
dnsNames:
|
||||||
|
description: Requested DNS subject alternative names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
duration:
|
||||||
|
description: |-
|
||||||
|
Requested 'duration' (i.e. lifetime) of the Certificate. Note that the
|
||||||
|
issuer may choose to ignore the requested duration, just like any other
|
||||||
|
requested attribute.
|
||||||
|
|
||||||
|
If unset, this defaults to 90 days.
|
||||||
|
Minimum accepted duration is 1 hour.
|
||||||
|
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
|
||||||
|
type: string
|
||||||
|
emailAddresses:
|
||||||
|
description: Requested email subject alternative names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
encodeUsagesInRequest:
|
||||||
|
description: |-
|
||||||
|
Whether the KeyUsage and ExtKeyUsage extensions should be set in the encoded CSR.
|
||||||
|
|
||||||
|
This option defaults to true, and should only be disabled if the target
|
||||||
|
issuer does not support CSRs with these X509 KeyUsage/ ExtKeyUsage extensions.
|
||||||
|
type: boolean
|
||||||
|
ipAddresses:
|
||||||
|
description: Requested IP address subject alternative names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
isCA:
|
||||||
|
description: |-
|
||||||
|
Requested basic constraints isCA value.
|
||||||
|
The isCA value is used to set the `isCA` field on the created CertificateRequest
|
||||||
|
resources. Note that the issuer may choose to ignore the requested isCA value, just
|
||||||
|
like any other requested attribute.
|
||||||
|
|
||||||
|
If true, this will automatically add the `cert sign` usage to the list
|
||||||
|
of requested `usages`.
|
||||||
|
type: boolean
|
||||||
|
issuerRef:
|
||||||
|
description: |-
|
||||||
|
Reference to the issuer responsible for issuing the certificate.
|
||||||
|
If the issuer is namespace-scoped, it must be in the same namespace
|
||||||
|
as the Certificate. If the issuer is cluster-scoped, it can be used
|
||||||
|
from any namespace.
|
||||||
|
|
||||||
|
The `name` field of the reference must always be specified.
|
||||||
|
properties:
|
||||||
|
group:
|
||||||
|
description: |-
|
||||||
|
Group of the issuer being referred to.
|
||||||
|
Defaults to 'cert-manager.io'.
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind of the issuer being referred to.
|
||||||
|
Defaults to 'Issuer'.
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: Name of the issuer being referred to.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
keystores:
|
||||||
|
description: Additional keystore output formats to be stored in the
|
||||||
|
Certificate's Secret.
|
||||||
|
properties:
|
||||||
|
jks:
|
||||||
|
description: |-
|
||||||
|
JKS configures options for storing a JKS keystore in the
|
||||||
|
`spec.secretName` Secret resource.
|
||||||
|
properties:
|
||||||
|
alias:
|
||||||
|
description: |-
|
||||||
|
Alias specifies the alias of the key in the keystore, required by the JKS format.
|
||||||
|
If not provided, the default alias `certificate` will be used.
|
||||||
|
type: string
|
||||||
|
create:
|
||||||
|
description: |-
|
||||||
|
Create enables JKS keystore creation for the Certificate.
|
||||||
|
If true, a file named `keystore.jks` will be created in the target
|
||||||
|
Secret resource, encrypted using the password stored in
|
||||||
|
`passwordSecretRef` or `password`.
|
||||||
|
The keystore file will be updated immediately.
|
||||||
|
If the issuer provided a CA certificate, a file named `truststore.jks`
|
||||||
|
will also be created in the target Secret resource, encrypted using the
|
||||||
|
password stored in `passwordSecretRef`
|
||||||
|
containing the issuing Certificate Authority
|
||||||
|
type: boolean
|
||||||
|
password:
|
||||||
|
description: |-
|
||||||
|
Password provides a literal password used to encrypt the JKS keystore.
|
||||||
|
Mutually exclusive with passwordSecretRef.
|
||||||
|
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||||
|
type: string
|
||||||
|
passwordSecretRef:
|
||||||
|
description: |-
|
||||||
|
PasswordSecretRef is a reference to a non-empty key in a Secret resource
|
||||||
|
containing the password used to encrypt the JKS keystore.
|
||||||
|
Mutually exclusive with password.
|
||||||
|
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: |-
|
||||||
|
The key of the entry in the Secret resource's `data` field to be used.
|
||||||
|
Some instances of this field may be defaulted, in others it may be
|
||||||
|
required.
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: |-
|
||||||
|
Name of the resource being referred to.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- create
|
||||||
|
type: object
|
||||||
|
pkcs12:
|
||||||
|
description: |-
|
||||||
|
PKCS12 configures options for storing a PKCS12 keystore in the
|
||||||
|
`spec.secretName` Secret resource.
|
||||||
|
properties:
|
||||||
|
create:
|
||||||
|
description: |-
|
||||||
|
Create enables PKCS12 keystore creation for the Certificate.
|
||||||
|
If true, a file named `keystore.p12` will be created in the target
|
||||||
|
Secret resource, encrypted using the password stored in
|
||||||
|
`passwordSecretRef` or in `password`.
|
||||||
|
The keystore file will be updated immediately.
|
||||||
|
If the issuer provided a CA certificate, a file named `truststore.p12` will
|
||||||
|
also be created in the target Secret resource, encrypted using the
|
||||||
|
password stored in `passwordSecretRef` containing the issuing Certificate
|
||||||
|
Authority
|
||||||
|
type: boolean
|
||||||
|
password:
|
||||||
|
description: |-
|
||||||
|
Password provides a literal password used to encrypt the PKCS#12 keystore.
|
||||||
|
Mutually exclusive with passwordSecretRef.
|
||||||
|
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||||
|
type: string
|
||||||
|
passwordSecretRef:
|
||||||
|
description: |-
|
||||||
|
PasswordSecretRef is a reference to a non-empty key in a Secret resource
|
||||||
|
containing the password used to encrypt the PKCS#12 keystore.
|
||||||
|
Mutually exclusive with password.
|
||||||
|
One of password or passwordSecretRef must provide a password with a non-zero length.
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: |-
|
||||||
|
The key of the entry in the Secret resource's `data` field to be used.
|
||||||
|
Some instances of this field may be defaulted, in others it may be
|
||||||
|
required.
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: |-
|
||||||
|
Name of the resource being referred to.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
profile:
|
||||||
|
description: |-
|
||||||
|
Profile specifies the key and certificate encryption algorithms and the HMAC algorithm
|
||||||
|
used to create the PKCS12 keystore. Default value is `LegacyRC2` for backward compatibility.
|
||||||
|
|
||||||
|
If provided, allowed values are:
|
||||||
|
`LegacyRC2`: Deprecated. Not supported by default in OpenSSL 3 or Java 20.
|
||||||
|
`LegacyDES`: Less secure algorithm. Use this option for maximal compatibility.
|
||||||
|
`Modern2023`: Secure algorithm. Use this option in case you have to always use secure algorithms
|
||||||
|
(e.g., because of company policy). Please note that the security of the algorithm is not that important
|
||||||
|
in reality, because the unencrypted certificate and private key are also stored in the Secret.
|
||||||
|
`Modern2026`: Encodes PKCS#12 files using algorithms that are considered modern as of 2026.
|
||||||
|
Private keys and certificates are encrypted using PBES2 with PBKDF2-HMAC-SHA-256 and AES-256-CBC.
|
||||||
|
The MAC algorithm is PBMAC1 with PBKDF2-HMAC-SHA-256 and HMAC-SHA256.
|
||||||
|
Files produced with this profile can be read by OpenSSL 3.4.0 and higher, Java 26 and higher,
|
||||||
|
or with Java using compatible versions of Bouncy Castle. Meets FIPS 140-3 requirements.
|
||||||
|
enum:
|
||||||
|
- LegacyRC2
|
||||||
|
- LegacyDES
|
||||||
|
- Modern2023
|
||||||
|
- Modern2026
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- create
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
literalSubject:
|
||||||
|
description: |-
|
||||||
|
Requested X.509 certificate subject, represented using the LDAP "String
|
||||||
|
Representation of a Distinguished Name" [1].
|
||||||
|
Important: the LDAP string format also specifies the order of the attributes
|
||||||
|
in the subject, this is important when issuing certs for LDAP authentication.
|
||||||
|
Example: `CN=foo,DC=corp,DC=example,DC=com`
|
||||||
|
More info [1]: https://datatracker.ietf.org/doc/html/rfc4514
|
||||||
|
More info: https://github.com/cert-manager/cert-manager/issues/3203
|
||||||
|
More info: https://github.com/cert-manager/cert-manager/issues/4424
|
||||||
|
|
||||||
|
Cannot be set if the `subject` or `commonName` field is set.
|
||||||
|
type: string
|
||||||
|
nameConstraints:
|
||||||
|
description: |-
|
||||||
|
x.509 certificate NameConstraint extension which MUST NOT be used in a non-CA certificate.
|
||||||
|
More Info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10
|
||||||
|
|
||||||
|
This is an Alpha Feature and is only enabled with the
|
||||||
|
`--feature-gates=NameConstraints=true` option set on both
|
||||||
|
the controller and webhook components.
|
||||||
|
properties:
|
||||||
|
critical:
|
||||||
|
description: if true then the name constraints are marked critical.
|
||||||
|
type: boolean
|
||||||
|
excluded:
|
||||||
|
description: |-
|
||||||
|
Excluded contains the constraints which must be disallowed. Any name matching a
|
||||||
|
restriction in the excluded field is invalid regardless
|
||||||
|
of information appearing in the permitted
|
||||||
|
properties:
|
||||||
|
dnsDomains:
|
||||||
|
description: DNSDomains is a list of DNS domains that are
|
||||||
|
permitted or excluded.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
emailAddresses:
|
||||||
|
description: EmailAddresses is a list of Email Addresses that
|
||||||
|
are permitted or excluded.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
ipRanges:
|
||||||
|
description: |-
|
||||||
|
IPRanges is a list of IP Ranges that are permitted or excluded.
|
||||||
|
This should be a valid CIDR notation.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
uriDomains:
|
||||||
|
description: URIDomains is a list of URI domains that are
|
||||||
|
permitted or excluded.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
type: object
|
||||||
|
permitted:
|
||||||
|
description: Permitted contains the constraints in which the names
|
||||||
|
must be located.
|
||||||
|
properties:
|
||||||
|
dnsDomains:
|
||||||
|
description: DNSDomains is a list of DNS domains that are
|
||||||
|
permitted or excluded.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
emailAddresses:
|
||||||
|
description: EmailAddresses is a list of Email Addresses that
|
||||||
|
are permitted or excluded.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
ipRanges:
|
||||||
|
description: |-
|
||||||
|
IPRanges is a list of IP Ranges that are permitted or excluded.
|
||||||
|
This should be a valid CIDR notation.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
uriDomains:
|
||||||
|
description: URIDomains is a list of URI domains that are
|
||||||
|
permitted or excluded.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
otherNames:
|
||||||
|
description: |-
|
||||||
|
`otherNames` is an escape hatch for SAN that allows any type. We currently restrict the support to string like otherNames, cf RFC 5280 p 37
|
||||||
|
Any UTF8 String valued otherName can be passed with by setting the keys oid: x.x.x.x and UTF8Value: somevalue for `otherName`.
|
||||||
|
Most commonly this would be UPN set with oid: 1.3.6.1.4.1.311.20.2.3
|
||||||
|
You should ensure that any OID passed is valid for the UTF8String type as we do not explicitly validate this.
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
oid:
|
||||||
|
description: |-
|
||||||
|
OID is the object identifier for the otherName SAN.
|
||||||
|
The object identifier must be expressed as a dotted string, for
|
||||||
|
example, "1.2.840.113556.1.4.221".
|
||||||
|
type: string
|
||||||
|
utf8Value:
|
||||||
|
description: |-
|
||||||
|
utf8Value is the string value of the otherName SAN.
|
||||||
|
The utf8Value accepts any valid UTF8 string to set as value for the otherName SAN.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
privateKey:
|
||||||
|
description: |-
|
||||||
|
Private key options. These include the key algorithm and size, the used
|
||||||
|
encoding and the rotation policy.
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
description: |-
|
||||||
|
Algorithm is the private key algorithm of the corresponding private key
|
||||||
|
for this certificate.
|
||||||
|
|
||||||
|
If provided, allowed values are either `RSA`, `ECDSA` or `Ed25519`.
|
||||||
|
If `algorithm` is specified and `size` is not provided,
|
||||||
|
key size of 2048 will be used for `RSA` key algorithm and
|
||||||
|
key size of 256 will be used for `ECDSA` key algorithm.
|
||||||
|
key size is ignored when using the `Ed25519` key algorithm.
|
||||||
|
enum:
|
||||||
|
- RSA
|
||||||
|
- ECDSA
|
||||||
|
- Ed25519
|
||||||
|
type: string
|
||||||
|
encoding:
|
||||||
|
description: |-
|
||||||
|
The private key cryptography standards (PKCS) encoding for this
|
||||||
|
certificate's private key to be encoded in.
|
||||||
|
|
||||||
|
If provided, allowed values are `PKCS1` and `PKCS8` standing for PKCS#1
|
||||||
|
and PKCS#8, respectively.
|
||||||
|
Defaults to `PKCS1` if not specified.
|
||||||
|
enum:
|
||||||
|
- PKCS1
|
||||||
|
- PKCS8
|
||||||
|
type: string
|
||||||
|
rotationPolicy:
|
||||||
|
description: |-
|
||||||
|
RotationPolicy controls how private keys should be regenerated when a
|
||||||
|
re-issuance is being processed.
|
||||||
|
|
||||||
|
If set to `Never`, a private key will only be generated if one does not
|
||||||
|
already exist in the target `spec.secretName`. If one does exist but it
|
||||||
|
does not have the correct algorithm or size, a warning will be raised
|
||||||
|
to await user intervention.
|
||||||
|
If set to `Always`, a private key matching the specified requirements
|
||||||
|
will be generated whenever a re-issuance occurs.
|
||||||
|
Default is `Always`.
|
||||||
|
The default was changed from `Never` to `Always` in cert-manager >=v1.18.0.
|
||||||
|
enum:
|
||||||
|
- Never
|
||||||
|
- Always
|
||||||
|
type: string
|
||||||
|
size:
|
||||||
|
description: |-
|
||||||
|
Size is the key bit size of the corresponding private key for this certificate.
|
||||||
|
|
||||||
|
If `algorithm` is set to `RSA`, valid values are `2048`, `4096` or `8192`,
|
||||||
|
and will default to `2048` if not specified.
|
||||||
|
If `algorithm` is set to `ECDSA`, valid values are `256`, `384` or `521`,
|
||||||
|
and will default to `256` if not specified.
|
||||||
|
If `algorithm` is set to `Ed25519`, Size is ignored.
|
||||||
|
No other values are allowed.
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
renewBefore:
|
||||||
|
description: |-
|
||||||
|
How long before the currently issued certificate's expiry cert-manager should
|
||||||
|
renew the certificate. For example, if a certificate is valid for 60 minutes,
|
||||||
|
and `renewBefore=10m`, cert-manager will begin to attempt to renew the certificate
|
||||||
|
50 minutes after it was issued (i.e. when there are 10 minutes remaining until
|
||||||
|
the certificate is no longer valid).
|
||||||
|
|
||||||
|
NOTE: The actual lifetime of the issued certificate is used to determine the
|
||||||
|
renewal time. If an issuer returns a certificate with a different lifetime than
|
||||||
|
the one requested, cert-manager will use the lifetime of the issued certificate.
|
||||||
|
|
||||||
|
If unset, this defaults to 1/3 of the issued certificate's lifetime.
|
||||||
|
Minimum accepted value is 5 minutes.
|
||||||
|
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
|
||||||
|
Cannot be set if the `renewBeforePercentage` field is set.
|
||||||
|
type: string
|
||||||
|
renewBeforePercentage:
|
||||||
|
description: |-
|
||||||
|
`renewBeforePercentage` is like `renewBefore`, except it is a relative percentage
|
||||||
|
rather than an absolute duration. For example, if a certificate is valid for 60
|
||||||
|
minutes, and `renewBeforePercentage=25`, cert-manager will begin to attempt to
|
||||||
|
renew the certificate 45 minutes after it was issued (i.e. when there are 15
|
||||||
|
minutes (25%) remaining until the certificate is no longer valid).
|
||||||
|
|
||||||
|
NOTE: The actual lifetime of the issued certificate is used to determine the
|
||||||
|
renewal time. If an issuer returns a certificate with a different lifetime than
|
||||||
|
the one requested, cert-manager will use the lifetime of the issued certificate.
|
||||||
|
|
||||||
|
Value must be an integer in the range (0,100). The minimum effective
|
||||||
|
`renewBefore` derived from the `renewBeforePercentage` and `duration` fields is 5
|
||||||
|
minutes.
|
||||||
|
Cannot be set if the `renewBefore` field is set.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
renewal:
|
||||||
|
description: |-
|
||||||
|
`renewal` allows configuration of how your certificate is renewed. If the policy mentioned is
|
||||||
|
`RenewBefore` then the controller respects `renewBefore` and `renewBeforePercentage`.
|
||||||
|
properties:
|
||||||
|
policy:
|
||||||
|
description: '`policy` must be one of `Disabled`, `RenewBefore`.'
|
||||||
|
enum:
|
||||||
|
- RenewBefore
|
||||||
|
- Disabled
|
||||||
|
type: string
|
||||||
|
windows:
|
||||||
|
description: '`windows` mentions the behavior of when the renewal
|
||||||
|
must happen.'
|
||||||
|
items:
|
||||||
|
description: CertificateRenewalWindows is the definition for
|
||||||
|
renewal windows
|
||||||
|
properties:
|
||||||
|
cron:
|
||||||
|
description: |-
|
||||||
|
`cron` is a cron compliant string to allow when the renewal should be allowed. Format is as shown below:
|
||||||
|
* * * * *
|
||||||
|
| | | | |
|
||||||
|
| | | | day of the week (0–6) (Sunday to Saturday;
|
||||||
|
| | | month (1–12) 7 is also Sunday on some systems)
|
||||||
|
| | day of the month (1–31)
|
||||||
|
| hour (0–23)
|
||||||
|
minute (0–59)
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
timezone:
|
||||||
|
description: |-
|
||||||
|
`timezone` is IANA compliant timezone. For example America/Denver.
|
||||||
|
If this field is not set, timezone is treated as UTC.
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
windowDuration:
|
||||||
|
description: |-
|
||||||
|
`windowDuration` is how long the cron definition is active for.
|
||||||
|
Value must be in units accepted by Go time.ParseDuration https://golang.org/pkg/time/#ParseDuration.
|
||||||
|
pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- cron
|
||||||
|
- windowDuration
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
type: object
|
||||||
|
revisionHistoryLimit:
|
||||||
|
description: |-
|
||||||
|
The maximum number of CertificateRequest revisions that are maintained in
|
||||||
|
the Certificate's history. Each revision represents a single `CertificateRequest`
|
||||||
|
created by this Certificate, either when it was created, renewed, or Spec
|
||||||
|
was changed. Revisions will be removed by oldest first if the number of
|
||||||
|
revisions exceeds this number.
|
||||||
|
|
||||||
|
If set, revisionHistoryLimit must be a value of `1` or greater.
|
||||||
|
Default value is `1`.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
secretName:
|
||||||
|
description: |-
|
||||||
|
Name of the Secret resource that will be automatically created and
|
||||||
|
managed by this Certificate resource. It will be populated with a
|
||||||
|
private key and certificate, signed by the denoted issuer. The Secret
|
||||||
|
resource lives in the same namespace as the Certificate resource.
|
||||||
|
type: string
|
||||||
|
secretTemplate:
|
||||||
|
description: |-
|
||||||
|
Defines annotations and labels to be copied to the Certificate's Secret.
|
||||||
|
Labels and annotations on the Secret will be changed as they appear on the
|
||||||
|
SecretTemplate when added or removed. SecretTemplate annotations are added
|
||||||
|
in conjunction with, and cannot overwrite, the base set of annotations
|
||||||
|
cert-manager sets on the Certificate's Secret.
|
||||||
|
properties:
|
||||||
|
annotations:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Annotations is a key value map to be copied to the
|
||||||
|
target Kubernetes Secret.
|
||||||
|
type: object
|
||||||
|
labels:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Labels is a key value map to be copied to the target
|
||||||
|
Kubernetes Secret.
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
signatureAlgorithm:
|
||||||
|
description: |-
|
||||||
|
Signature algorithm to use.
|
||||||
|
Allowed values for RSA keys: SHA256WithRSA, SHA384WithRSA, SHA512WithRSA.
|
||||||
|
Allowed values for ECDSA keys: ECDSAWithSHA256, ECDSAWithSHA384, ECDSAWithSHA512.
|
||||||
|
Allowed values for Ed25519 keys: PureEd25519.
|
||||||
|
enum:
|
||||||
|
- SHA256WithRSA
|
||||||
|
- SHA384WithRSA
|
||||||
|
- SHA512WithRSA
|
||||||
|
- ECDSAWithSHA256
|
||||||
|
- ECDSAWithSHA384
|
||||||
|
- ECDSAWithSHA512
|
||||||
|
- PureEd25519
|
||||||
|
type: string
|
||||||
|
subject:
|
||||||
|
description: |-
|
||||||
|
Requested set of X509 certificate subject attributes.
|
||||||
|
More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6
|
||||||
|
|
||||||
|
The common name attribute is specified separately in the `commonName` field.
|
||||||
|
Cannot be set if the `literalSubject` field is set.
|
||||||
|
properties:
|
||||||
|
countries:
|
||||||
|
description: Countries to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
localities:
|
||||||
|
description: Cities to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
organizationalUnits:
|
||||||
|
description: Organizational Units to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
organizations:
|
||||||
|
description: Organizations to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
postalCodes:
|
||||||
|
description: Postal codes to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
provinces:
|
||||||
|
description: State/Provinces to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
serialNumber:
|
||||||
|
description: Serial number to be used on the Certificate.
|
||||||
|
type: string
|
||||||
|
streetAddresses:
|
||||||
|
description: Street addresses to be used on the Certificate.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
type: object
|
||||||
|
uris:
|
||||||
|
description: Requested URI subject alternative names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
usages:
|
||||||
|
description: |-
|
||||||
|
Requested key usages and extended key usages.
|
||||||
|
These usages are used to set the `usages` field on the created CertificateRequest
|
||||||
|
resources. If `encodeUsagesInRequest` is unset or set to `true`, the usages
|
||||||
|
will additionally be encoded in the `request` field which contains the CSR blob.
|
||||||
|
|
||||||
|
If unset, defaults to `digital signature` and `key encipherment`.
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
KeyUsage specifies valid usage contexts for keys.
|
||||||
|
See:
|
||||||
|
https://tools.ietf.org/html/rfc5280#section-4.2.1.3
|
||||||
|
https://tools.ietf.org/html/rfc5280#section-4.2.1.12
|
||||||
|
|
||||||
|
Valid KeyUsage values are as follows:
|
||||||
|
"signing",
|
||||||
|
"digital signature",
|
||||||
|
"content commitment",
|
||||||
|
"key encipherment",
|
||||||
|
"key agreement",
|
||||||
|
"data encipherment",
|
||||||
|
"cert sign",
|
||||||
|
"crl sign",
|
||||||
|
"encipher only",
|
||||||
|
"decipher only",
|
||||||
|
"any",
|
||||||
|
"server auth",
|
||||||
|
"client auth",
|
||||||
|
"code signing",
|
||||||
|
"email protection",
|
||||||
|
"s/mime",
|
||||||
|
"ipsec end system",
|
||||||
|
"ipsec tunnel",
|
||||||
|
"ipsec user",
|
||||||
|
"timestamping",
|
||||||
|
"ocsp signing",
|
||||||
|
"microsoft sgc",
|
||||||
|
"netscape sgc"
|
||||||
|
enum:
|
||||||
|
- signing
|
||||||
|
- digital signature
|
||||||
|
- content commitment
|
||||||
|
- key encipherment
|
||||||
|
- key agreement
|
||||||
|
- data encipherment
|
||||||
|
- cert sign
|
||||||
|
- crl sign
|
||||||
|
- encipher only
|
||||||
|
- decipher only
|
||||||
|
- any
|
||||||
|
- server auth
|
||||||
|
- client auth
|
||||||
|
- code signing
|
||||||
|
- email protection
|
||||||
|
- s/mime
|
||||||
|
- ipsec end system
|
||||||
|
- ipsec tunnel
|
||||||
|
- ipsec user
|
||||||
|
- timestamping
|
||||||
|
- ocsp signing
|
||||||
|
- microsoft sgc
|
||||||
|
- netscape sgc
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-type: atomic
|
||||||
|
required:
|
||||||
|
- issuerRef
|
||||||
|
- secretName
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: |-
|
||||||
|
Status of the Certificate.
|
||||||
|
This is set and managed automatically.
|
||||||
|
Read-only.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
description: |-
|
||||||
|
List of status conditions to indicate the status of certificates.
|
||||||
|
Known condition types are `Ready` and `Issuing`.
|
||||||
|
items:
|
||||||
|
description: CertificateCondition contains condition information
|
||||||
|
for a Certificate.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
LastTransitionTime is the timestamp corresponding to the last status
|
||||||
|
change of this condition.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
Message is a human readable description of the details of the last
|
||||||
|
transition, complementing reason.
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
If set, this represents the .metadata.generation that the condition was
|
||||||
|
set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the
|
||||||
|
.status.condition[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the Certificate.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
Reason is a brief machine readable explanation for the condition's last
|
||||||
|
transition.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: Status of the condition, one of (`True`, `False`,
|
||||||
|
`Unknown`).
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: Type of the condition, known values are (`Ready`,
|
||||||
|
`Issuing`).
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
failedIssuanceAttempts:
|
||||||
|
description: |-
|
||||||
|
The number of continuous failed issuance attempts up till now. This
|
||||||
|
field gets removed (if set) on a successful issuance and gets set to
|
||||||
|
1 if unset and an issuance has failed. If an issuance has failed, the
|
||||||
|
delay till the next issuance will be calculated using formula
|
||||||
|
time.Hour * 2 ^ (failedIssuanceAttempts - 1).
|
||||||
|
type: integer
|
||||||
|
lastFailureTime:
|
||||||
|
description: |-
|
||||||
|
LastFailureTime is set only if the latest issuance for this
|
||||||
|
Certificate failed and contains the time of the failure. If an
|
||||||
|
issuance has failed, the delay till the next issuance will be
|
||||||
|
calculated using formula time.Hour * 2 ^ (failedIssuanceAttempts -
|
||||||
|
1). If the latest issuance has succeeded this field will be unset.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
nextPrivateKeySecretName:
|
||||||
|
description: |-
|
||||||
|
The name of the Secret resource containing the private key to be used
|
||||||
|
for the next certificate iteration.
|
||||||
|
The keymanager controller will automatically set this field if the
|
||||||
|
`Issuing` condition is set to `True`.
|
||||||
|
It will automatically unset this field when the Issuing condition is
|
||||||
|
not set or False.
|
||||||
|
type: string
|
||||||
|
notAfter:
|
||||||
|
description: |-
|
||||||
|
The expiration time of the certificate stored in the secret named
|
||||||
|
by this resource in `spec.secretName`.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
notBefore:
|
||||||
|
description: |-
|
||||||
|
The time after which the certificate stored in the secret named
|
||||||
|
by this resource in `spec.secretName` is valid.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
renewalTime:
|
||||||
|
description: |-
|
||||||
|
RenewalTime is the time at which the certificate will be next
|
||||||
|
renewed.
|
||||||
|
If not set, no upcoming renewal is scheduled.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
revision:
|
||||||
|
description: |-
|
||||||
|
The current 'revision' of the certificate as issued.
|
||||||
|
|
||||||
|
When a CertificateRequest resource is created, it will have the
|
||||||
|
`cert-manager.io/certificate-revision` set to one greater than the
|
||||||
|
current value of this field.
|
||||||
|
|
||||||
|
Upon issuance, this field will be set to the value of the annotation
|
||||||
|
on the CertificateRequest resource used to issue the certificate.
|
||||||
|
|
||||||
|
Persisting the value on the CertificateRequest resource allows the
|
||||||
|
certificates controller to know whether a request is part of an old
|
||||||
|
issuance or if it is part of the ongoing revision's issuance by
|
||||||
|
checking if the revision value in the annotation is greater than this
|
||||||
|
field.
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
selectableFields:
|
||||||
|
- jsonPath: .spec.issuerRef.group
|
||||||
|
- jsonPath: .spec.issuerRef.kind
|
||||||
|
- jsonPath: .spec.issuerRef.name
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
1759
Tests/Fixtures/real-yaml/cert-manager-values.yaml
Normal file
1759
Tests/Fixtures/real-yaml/cert-manager-values.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: backend
|
||||||
|
target: builder
|
||||||
|
secrets:
|
||||||
|
- db-password
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
user: postgres
|
||||||
|
secrets:
|
||||||
|
- db-password
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=example
|
||||||
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
|
||||||
|
expose:
|
||||||
|
- 5432
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "pg_isready" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
image: nginx
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./proxy/nginx.conf
|
||||||
|
target: /etc/nginx/conf.d/default.conf
|
||||||
|
read_only: true
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db-password:
|
||||||
|
file: db/password.txt
|
||||||
84
Tests/Fixtures/real-yaml/github-actions-swift-format.yml
Normal file
84
Tests/Fixtures/real-yaml/github-actions-swift-format.yml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
name: Pull request
|
||||||
|
|
||||||
|
# PRs created by GitHub Actions don't kick off further actions (https://github.com/peter-evans/create-pull-request/blob/d57e551ebc1a16dee0b8c9ea6d24dba7627a6e35/docs/concepts-guidelines.md#triggering-further-workflow-runs).
|
||||||
|
# As a workaround, we mark automerge PRs that are created by GitHub actions as draft and trigger the GitHub actions by marking the PR as ready for review. We'd prefer not re-triggering testing on a normal user's PR in this case, but skipping them causes the checks to reset.
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize, ready_for_review]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Test
|
||||||
|
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11
|
||||||
|
with:
|
||||||
|
enable_cross_pr_testing: true
|
||||||
|
linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]"
|
||||||
|
windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]"
|
||||||
|
soundness:
|
||||||
|
name: Soundness
|
||||||
|
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.11
|
||||||
|
with:
|
||||||
|
license_header_check_project_name: "Swift.org"
|
||||||
|
api_breakage_check_allowlist_path: "api-breakages.txt"
|
||||||
|
compatibility_check:
|
||||||
|
# Format a list of real-world projects that enforce formatting using the latest swift-format version.
|
||||||
|
name: Check compatibility with real world projects
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: swift:6.2
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Mark the workspace as safe
|
||||||
|
# https://github.com/actions/checkout/issues/766
|
||||||
|
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||||
|
- name: Check out related PRs
|
||||||
|
run: |
|
||||||
|
apt-get update && apt-get install -y curl
|
||||||
|
curl -s https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/cross-pr-checkout.swift > /tmp/cross-pr-checkout.swift
|
||||||
|
swift /tmp/cross-pr-checkout.swift "${{ github.repository }}" "${{ github.event.number }}"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
swift build --configuration release
|
||||||
|
- name: Add local swift-format to PATH
|
||||||
|
run: |
|
||||||
|
# Add the locally built version of swift-format to PATH so it gets picked up by check-swift-format.sh
|
||||||
|
echo "PATH=$(pwd)/.build/release:$PATH" >> "$GITHUB_ENV"
|
||||||
|
- name: Download check format script
|
||||||
|
run: |
|
||||||
|
curl https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/check-swift-format.sh > /tmp/check-swift-format.sh
|
||||||
|
chmod +x /tmp/check-swift-format.sh
|
||||||
|
- name: Format swift-format
|
||||||
|
run: |
|
||||||
|
cd "$GITHUB_WORKSPACE"
|
||||||
|
/tmp/check-swift-format.sh
|
||||||
|
- name: Checkout swift-syntax
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: swiftlang/swift-syntax
|
||||||
|
persist-credentials: false
|
||||||
|
path: swift-syntax
|
||||||
|
- name: Format swift-syntax
|
||||||
|
run: |
|
||||||
|
cd "$GITHUB_WORKSPACE/swift-syntax"
|
||||||
|
/tmp/check-swift-format.sh
|
||||||
|
- name: Checkout sourcekit-lsp
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: swiftlang/sourcekit-lsp
|
||||||
|
persist-credentials: false
|
||||||
|
path: sourcekit-lsp
|
||||||
|
- name: Format sourcekit-lsp
|
||||||
|
run: |
|
||||||
|
cd "$GITHUB_WORKSPACE/sourcekit-lsp"
|
||||||
|
/tmp/check-swift-format.sh
|
||||||
257280
Tests/Fixtures/real-yaml/github-rest-api.yaml
Normal file
257280
Tests/Fixtures/real-yaml/github-rest-api.yaml
Normal file
File diff suppressed because one or more lines are too long
12
Tests/Fixtures/real-yaml/kubernetes-simple-pod.yaml
Normal file
12
Tests/Fixtures/real-yaml/kubernetes-simple-pod.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: pods-simple-pod
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- sleep
|
||||||
|
- "3600"
|
||||||
|
image: busybox
|
||||||
|
name: pods-simple-container
|
||||||
18
Tests/Fixtures/real-yaml/kustomize-wordpress.yaml
Normal file
18
Tests/Fixtures/real-yaml/kustomize-wordpress.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
resources:
|
||||||
|
- wordpress
|
||||||
|
- mysql
|
||||||
|
patches:
|
||||||
|
- path: patch.yaml
|
||||||
|
namePrefix: demo-
|
||||||
|
|
||||||
|
vars:
|
||||||
|
- name: WORDPRESS_SERVICE
|
||||||
|
objref:
|
||||||
|
kind: Service
|
||||||
|
name: wordpress
|
||||||
|
apiVersion: v1
|
||||||
|
- name: MYSQL_SERVICE
|
||||||
|
objref:
|
||||||
|
kind: Service
|
||||||
|
name: mysql
|
||||||
|
apiVersion: v1
|
||||||
119
Tests/Fixtures/real-yaml/openapi-petstore.yaml
Normal file
119
Tests/Fixtures/real-yaml/openapi-petstore.yaml
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
openapi: "3.0.0"
|
||||||
|
info:
|
||||||
|
version: 1.0.0
|
||||||
|
title: Swagger Petstore
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
servers:
|
||||||
|
- url: http://petstore.swagger.io/v1
|
||||||
|
paths:
|
||||||
|
/pets:
|
||||||
|
get:
|
||||||
|
summary: List all pets
|
||||||
|
operationId: listPets
|
||||||
|
tags:
|
||||||
|
- pets
|
||||||
|
parameters:
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
description: How many items to return at one time (max 100)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
maximum: 100
|
||||||
|
format: int32
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A paged array of pets
|
||||||
|
headers:
|
||||||
|
x-next:
|
||||||
|
description: A link to the next page of responses
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Pets"
|
||||||
|
default:
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
post:
|
||||||
|
summary: Create a pet
|
||||||
|
operationId: createPets
|
||||||
|
tags:
|
||||||
|
- pets
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Pet'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Null response
|
||||||
|
default:
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/pets/{petId}:
|
||||||
|
get:
|
||||||
|
summary: Info for a specific pet
|
||||||
|
operationId: showPetById
|
||||||
|
tags:
|
||||||
|
- pets
|
||||||
|
parameters:
|
||||||
|
- name: petId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The id of the pet to retrieve
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Expected response to a valid request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Pet"
|
||||||
|
default:
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Pet:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
Pets:
|
||||||
|
type: array
|
||||||
|
maxItems: 100
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Pet"
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
481
Tests/Fixtures/real-yaml/prometheus-conf-good.yml
Normal file
481
Tests/Fixtures/real-yaml/prometheus-conf-good.yml
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
# my global config
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 30s
|
||||||
|
body_size_limit: 15MB
|
||||||
|
sample_limit: 1500
|
||||||
|
target_limit: 30
|
||||||
|
label_limit: 30
|
||||||
|
label_name_length_limit: 200
|
||||||
|
label_value_length_limit: 200
|
||||||
|
query_log_file: query.log
|
||||||
|
scrape_failure_log_file: fail.log
|
||||||
|
# scrape_timeout is set to the global default (10s).
|
||||||
|
|
||||||
|
external_labels:
|
||||||
|
monitor: codelab
|
||||||
|
foo: bar
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
gogc: 42
|
||||||
|
|
||||||
|
rule_files:
|
||||||
|
- "first.rules"
|
||||||
|
- "my/*.rules"
|
||||||
|
|
||||||
|
remote_write:
|
||||||
|
- url: http://remote1/push
|
||||||
|
name: drop_expensive
|
||||||
|
write_relabel_configs:
|
||||||
|
- source_labels: [__name__]
|
||||||
|
regex: expensive.*
|
||||||
|
action: drop
|
||||||
|
oauth2:
|
||||||
|
client_id: "123"
|
||||||
|
client_secret: "456"
|
||||||
|
token_url: "http://remote1/auth"
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
- url: http://remote2/push
|
||||||
|
protobuf_message: io.prometheus.write.v2.Request
|
||||||
|
name: rw_tls
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
headers:
|
||||||
|
name: value
|
||||||
|
|
||||||
|
otlp:
|
||||||
|
promote_resource_attributes: ["k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"]
|
||||||
|
|
||||||
|
remote_read:
|
||||||
|
- url: http://remote1/read
|
||||||
|
read_recent: true
|
||||||
|
name: default
|
||||||
|
enable_http2: false
|
||||||
|
- url: http://remote3/read
|
||||||
|
read_recent: false
|
||||||
|
name: read_special
|
||||||
|
required_matchers:
|
||||||
|
job: special
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: prometheus
|
||||||
|
|
||||||
|
honor_labels: true
|
||||||
|
# scrape_interval is defined by the configured global (15s).
|
||||||
|
# scrape_timeout is defined by the global default (10s).
|
||||||
|
|
||||||
|
# metrics_path defaults to '/metrics'
|
||||||
|
# scheme defaults to 'http'.
|
||||||
|
|
||||||
|
fallback_scrape_protocol: PrometheusText0.0.4
|
||||||
|
|
||||||
|
scrape_failure_log_file: fail_prom.log
|
||||||
|
file_sd_configs:
|
||||||
|
- files:
|
||||||
|
- foo/*.slow.json
|
||||||
|
- foo/*.slow.yml
|
||||||
|
- single/file.yml
|
||||||
|
refresh_interval: 10m
|
||||||
|
- files:
|
||||||
|
- bar/*.yaml
|
||||||
|
|
||||||
|
static_configs:
|
||||||
|
- targets: ["localhost:9090", "localhost:9191"]
|
||||||
|
labels:
|
||||||
|
my: label
|
||||||
|
your: label
|
||||||
|
|
||||||
|
http_headers:
|
||||||
|
foo:
|
||||||
|
values: ["foobar"]
|
||||||
|
secrets: ["bar", "foo"]
|
||||||
|
files: ["valid_password_file"]
|
||||||
|
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [job, __meta_dns_name]
|
||||||
|
regex: (.*)some-[regex]
|
||||||
|
target_label: job
|
||||||
|
replacement: foo-${1}
|
||||||
|
# action defaults to 'replace'
|
||||||
|
- source_labels: [abc]
|
||||||
|
target_label: cde
|
||||||
|
- replacement: static
|
||||||
|
target_label: abc
|
||||||
|
- regex:
|
||||||
|
replacement: static
|
||||||
|
target_label: abc
|
||||||
|
- source_labels: [foo]
|
||||||
|
target_label: abc
|
||||||
|
action: keepequal
|
||||||
|
- source_labels: [foo]
|
||||||
|
target_label: abc
|
||||||
|
action: dropequal
|
||||||
|
|
||||||
|
authorization:
|
||||||
|
credentials_file: valid_token_file
|
||||||
|
|
||||||
|
tls_config:
|
||||||
|
min_version: TLS10
|
||||||
|
|
||||||
|
- job_name: service-x
|
||||||
|
|
||||||
|
basic_auth:
|
||||||
|
username: admin_name
|
||||||
|
password: "multiline\nmysecret\ntest"
|
||||||
|
|
||||||
|
scrape_interval: 50s
|
||||||
|
scrape_timeout: 5s
|
||||||
|
scrape_protocols: ["PrometheusText0.0.4"]
|
||||||
|
|
||||||
|
body_size_limit: 10MB
|
||||||
|
sample_limit: 1000
|
||||||
|
target_limit: 35
|
||||||
|
label_limit: 35
|
||||||
|
label_name_length_limit: 210
|
||||||
|
label_value_length_limit: 210
|
||||||
|
|
||||||
|
metrics_path: /my_path
|
||||||
|
scheme: https
|
||||||
|
|
||||||
|
dns_sd_configs:
|
||||||
|
- refresh_interval: 15s
|
||||||
|
names:
|
||||||
|
- first.dns.address.domain.com
|
||||||
|
- second.dns.address.domain.com
|
||||||
|
- names:
|
||||||
|
- first.dns.address.domain.com
|
||||||
|
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [job]
|
||||||
|
regex: (.*)some-[regex]
|
||||||
|
action: drop
|
||||||
|
- source_labels: [__address__]
|
||||||
|
modulus: 8
|
||||||
|
target_label: __tmp_hash
|
||||||
|
action: hashmod
|
||||||
|
- source_labels: [__tmp_hash]
|
||||||
|
regex: 1
|
||||||
|
action: keep
|
||||||
|
- action: labelmap
|
||||||
|
regex: 1
|
||||||
|
- action: labeldrop
|
||||||
|
regex: d
|
||||||
|
- action: labelkeep
|
||||||
|
regex: k
|
||||||
|
|
||||||
|
metric_relabel_configs:
|
||||||
|
- source_labels: [__name__]
|
||||||
|
regex: expensive_metric.*
|
||||||
|
action: drop
|
||||||
|
|
||||||
|
- job_name: service-y
|
||||||
|
|
||||||
|
consul_sd_configs:
|
||||||
|
- server: "localhost:1234"
|
||||||
|
token: mysecret
|
||||||
|
path_prefix: /consul
|
||||||
|
services: ["nginx", "cache", "mysql"]
|
||||||
|
health_filter: 'Service.Tags contains "canary"'
|
||||||
|
tags: ["canary", "v1"]
|
||||||
|
node_meta:
|
||||||
|
rack: "123"
|
||||||
|
allow_stale: true
|
||||||
|
scheme: https
|
||||||
|
tls_config:
|
||||||
|
ca_file: valid_ca_file
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
insecure_skip_verify: false
|
||||||
|
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_sd_consul_tags]
|
||||||
|
separator: ","
|
||||||
|
regex: label:([^=]+)=([^,]+)
|
||||||
|
target_label: ${1}
|
||||||
|
replacement: ${2}
|
||||||
|
|
||||||
|
- job_name: service-z
|
||||||
|
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
authorization:
|
||||||
|
credentials: mysecret
|
||||||
|
|
||||||
|
- job_name: service-kubernetes
|
||||||
|
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: endpoints
|
||||||
|
api_server: "https://localhost:1234"
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
basic_auth:
|
||||||
|
username: "myusername"
|
||||||
|
password: "mysecret"
|
||||||
|
|
||||||
|
- job_name: service-kubernetes-namespaces
|
||||||
|
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: endpoints
|
||||||
|
api_server: "https://localhost:1234"
|
||||||
|
namespaces:
|
||||||
|
names:
|
||||||
|
- default
|
||||||
|
|
||||||
|
basic_auth:
|
||||||
|
username: "myusername"
|
||||||
|
password_file: valid_password_file
|
||||||
|
|
||||||
|
- job_name: service-kuma
|
||||||
|
|
||||||
|
kuma_sd_configs:
|
||||||
|
- server: http://kuma-control-plane.kuma-system.svc:5676
|
||||||
|
client_id: main-prometheus
|
||||||
|
|
||||||
|
- job_name: service-marathon
|
||||||
|
marathon_sd_configs:
|
||||||
|
- servers:
|
||||||
|
- "https://marathon.example.com:443"
|
||||||
|
|
||||||
|
auth_token: "mysecret"
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
- job_name: service-nomad
|
||||||
|
nomad_sd_configs:
|
||||||
|
- server: 'http://localhost:4646'
|
||||||
|
|
||||||
|
- job_name: service-ec2
|
||||||
|
ec2_sd_configs:
|
||||||
|
- region: us-east-1
|
||||||
|
access_key: access
|
||||||
|
secret_key: mysecret
|
||||||
|
profile: profile
|
||||||
|
filters:
|
||||||
|
- name: tag:environment
|
||||||
|
values:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
- name: tag:service
|
||||||
|
values:
|
||||||
|
- web
|
||||||
|
- db
|
||||||
|
|
||||||
|
- job_name: service-lightsail
|
||||||
|
lightsail_sd_configs:
|
||||||
|
- region: us-east-1
|
||||||
|
access_key: access
|
||||||
|
secret_key: mysecret
|
||||||
|
profile: profile
|
||||||
|
|
||||||
|
- job_name: service-azure
|
||||||
|
azure_sd_configs:
|
||||||
|
- environment: AzurePublicCloud
|
||||||
|
authentication_method: OAuth
|
||||||
|
subscription_id: 11AAAA11-A11A-111A-A111-1111A1111A11
|
||||||
|
resource_group: my-resource-group
|
||||||
|
tenant_id: BBBB222B-B2B2-2B22-B222-2BB2222BB2B2
|
||||||
|
client_id: 333333CC-3C33-3333-CCC3-33C3CCCCC33C
|
||||||
|
client_secret: mysecret
|
||||||
|
port: 9100
|
||||||
|
|
||||||
|
- job_name: service-nerve
|
||||||
|
nerve_sd_configs:
|
||||||
|
- servers:
|
||||||
|
- localhost
|
||||||
|
paths:
|
||||||
|
- /monitoring
|
||||||
|
|
||||||
|
- job_name: 0123service-xxx
|
||||||
|
metrics_path: /metrics
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost:9090
|
||||||
|
|
||||||
|
- job_name: badfederation
|
||||||
|
honor_timestamps: false
|
||||||
|
metrics_path: /federate
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost:9090
|
||||||
|
|
||||||
|
- job_name: 測試
|
||||||
|
metrics_path: /metrics
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost:9090
|
||||||
|
|
||||||
|
- job_name: httpsd
|
||||||
|
http_sd_configs:
|
||||||
|
- url: "http://example.com/prometheus"
|
||||||
|
|
||||||
|
- job_name: service-triton
|
||||||
|
triton_sd_configs:
|
||||||
|
- account: "testAccount"
|
||||||
|
dns_suffix: "triton.example.com"
|
||||||
|
endpoint: "triton.example.com"
|
||||||
|
port: 9163
|
||||||
|
refresh_interval: 1m
|
||||||
|
version: 1
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
- job_name: digitalocean-droplets
|
||||||
|
digitalocean_sd_configs:
|
||||||
|
- authorization:
|
||||||
|
credentials: abcdef
|
||||||
|
|
||||||
|
- job_name: docker
|
||||||
|
docker_sd_configs:
|
||||||
|
- host: unix:///var/run/docker.sock
|
||||||
|
|
||||||
|
- job_name: dockerswarm
|
||||||
|
dockerswarm_sd_configs:
|
||||||
|
- host: http://127.0.0.1:2375
|
||||||
|
role: nodes
|
||||||
|
|
||||||
|
- job_name: service-openstack
|
||||||
|
openstack_sd_configs:
|
||||||
|
- role: instance
|
||||||
|
region: RegionOne
|
||||||
|
port: 80
|
||||||
|
refresh_interval: 1m
|
||||||
|
tls_config:
|
||||||
|
ca_file: valid_ca_file
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
- job_name: service-puppetdb
|
||||||
|
puppetdb_sd_configs:
|
||||||
|
- url: https://puppetserver/
|
||||||
|
query: 'resources { type = "Package" and title = "httpd" }'
|
||||||
|
include_parameters: true
|
||||||
|
port: 80
|
||||||
|
refresh_interval: 1m
|
||||||
|
tls_config:
|
||||||
|
ca_file: valid_ca_file
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
|
||||||
|
- job_name: hetzner
|
||||||
|
scrape_native_histograms: true
|
||||||
|
relabel_configs:
|
||||||
|
- action: uppercase
|
||||||
|
source_labels: [instance]
|
||||||
|
target_label: instance
|
||||||
|
hetzner_sd_configs:
|
||||||
|
- role: hcloud
|
||||||
|
authorization:
|
||||||
|
credentials: abcdef
|
||||||
|
- role: robot
|
||||||
|
basic_auth:
|
||||||
|
username: abcdef
|
||||||
|
password: abcdef
|
||||||
|
|
||||||
|
- job_name: service-eureka
|
||||||
|
eureka_sd_configs:
|
||||||
|
- server: "http://eureka.example.com:8761/eureka"
|
||||||
|
|
||||||
|
- job_name: ovhcloud
|
||||||
|
ovhcloud_sd_configs:
|
||||||
|
- service: vps
|
||||||
|
endpoint: ovh-eu
|
||||||
|
application_key: testAppKey
|
||||||
|
application_secret: testAppSecret
|
||||||
|
consumer_key: testConsumerKey
|
||||||
|
refresh_interval: 1m
|
||||||
|
- service: dedicated_server
|
||||||
|
endpoint: ovh-eu
|
||||||
|
application_key: testAppKey
|
||||||
|
application_secret: testAppSecret
|
||||||
|
consumer_key: testConsumerKey
|
||||||
|
refresh_interval: 1m
|
||||||
|
|
||||||
|
- job_name: scaleway
|
||||||
|
scaleway_sd_configs:
|
||||||
|
- role: instance
|
||||||
|
project_id: 11111111-1111-1111-1111-111111111112
|
||||||
|
access_key: SCWXXXXXXXXXXXXXXXXX
|
||||||
|
secret_key: 11111111-1111-1111-1111-111111111111
|
||||||
|
- role: baremetal
|
||||||
|
project_id: 11111111-1111-1111-1111-111111111112
|
||||||
|
access_key: SCWXXXXXXXXXXXXXXXXX
|
||||||
|
secret_key: 11111111-1111-1111-1111-111111111111
|
||||||
|
|
||||||
|
- job_name: linode-instances
|
||||||
|
linode_sd_configs:
|
||||||
|
- authorization:
|
||||||
|
credentials: abcdef
|
||||||
|
|
||||||
|
- job_name: stackit-servers
|
||||||
|
stackit_sd_configs:
|
||||||
|
- project: 11111111-1111-1111-1111-111111111111
|
||||||
|
service_account_key: mysecret_sa_key
|
||||||
|
private_key: mysecret_private_key
|
||||||
|
authorization:
|
||||||
|
credentials: abcdef
|
||||||
|
|
||||||
|
- job_name: uyuni
|
||||||
|
uyuni_sd_configs:
|
||||||
|
- server: https://localhost:1234
|
||||||
|
username: gopher
|
||||||
|
password: hole
|
||||||
|
|
||||||
|
- job_name: ionos
|
||||||
|
ionos_sd_configs:
|
||||||
|
- datacenter_id: 8feda53f-15f0-447f-badf-ebe32dad2fc0
|
||||||
|
authorization:
|
||||||
|
credentials: abcdef
|
||||||
|
|
||||||
|
- job_name: vultr
|
||||||
|
vultr_sd_configs:
|
||||||
|
- authorization:
|
||||||
|
credentials: abcdef
|
||||||
|
|
||||||
|
- job_name: outscale
|
||||||
|
outscale_sd_configs:
|
||||||
|
- region: eu-west-2
|
||||||
|
access_key: A1B2C3D4E5F6G7H8I9J0
|
||||||
|
secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
|
||||||
|
alerting:
|
||||||
|
alertmanagers:
|
||||||
|
- scheme: https
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- "1.2.3.4:9093"
|
||||||
|
- "1.2.3.5:9093"
|
||||||
|
- "1.2.3.6:9093"
|
||||||
|
|
||||||
|
storage:
|
||||||
|
tsdb:
|
||||||
|
out_of_order_time_window: 30m
|
||||||
|
stale_series_compaction_threshold: 0.5
|
||||||
|
retention:
|
||||||
|
time: 1d
|
||||||
|
size: 1GB
|
||||||
|
percentage: 28
|
||||||
|
|
||||||
|
tracing:
|
||||||
|
endpoint: "localhost:4317"
|
||||||
|
client_type: "grpc"
|
||||||
|
headers:
|
||||||
|
foo: "bar"
|
||||||
|
timeout: 5s
|
||||||
|
compression: "gzip"
|
||||||
|
tls_config:
|
||||||
|
cert_file: valid_cert_file
|
||||||
|
key_file: valid_key_file
|
||||||
|
insecure_skip_verify: true
|
||||||
427
Tests/Fixtures/real-yaml/prow-cluster-api-presubmits.yaml
Normal file
427
Tests/Fixtures/real-yaml/prow-cluster-api-presubmits.yaml
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
# Code generated by cluster-api's prowjob-gen. DO NOT EDIT.
|
||||||
|
presubmits:
|
||||||
|
kubernetes-sigs/cluster-api:
|
||||||
|
- name: pull-cluster-api-build-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
always_run: true
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
command:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-build.sh
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 6000m
|
||||||
|
memory: 4Gi
|
||||||
|
limits:
|
||||||
|
cpu: 6000m
|
||||||
|
memory: 4Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-build-release-1-13
|
||||||
|
- name: pull-cluster-api-apidiff-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
optional: true
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-apidiff.sh
|
||||||
|
image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 6000m
|
||||||
|
memory: 2Gi
|
||||||
|
limits:
|
||||||
|
cpu: 6000m
|
||||||
|
memory: 2Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-apidiff-release-1-13
|
||||||
|
- name: pull-cluster-api-verify-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
always_run: true
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
command:
|
||||||
|
- "runner.sh"
|
||||||
|
- ./scripts/ci-verify.sh
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 5000m
|
||||||
|
memory: 3Gi
|
||||||
|
limits:
|
||||||
|
cpu: 5000m
|
||||||
|
memory: 3Gi
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-verify-release-1-13
|
||||||
|
- name: pull-cluster-api-test-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-test.sh
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 7
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 7
|
||||||
|
memory: 8Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-test-release-1-13
|
||||||
|
- name: pull-cluster-api-test-mink8s-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
always_run: false
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-test.sh
|
||||||
|
env:
|
||||||
|
# This value determines the minimum Kubernetes
|
||||||
|
# supported version for Cluster API management cluster
|
||||||
|
# and can be found by referring to [Supported Kubernetes Version](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions)
|
||||||
|
# docs (look for minimum supported k8s version for management cluster, i.e N-3).
|
||||||
|
#
|
||||||
|
# To check the latest available envtest in Kubebuilder for the minor version we determined above, please
|
||||||
|
# refer to https://github.com/kubernetes-sigs/kubebuilder/tree/tools-releases.
|
||||||
|
- name: KUBEBUILDER_ENVTEST_KUBERNETES_VERSION
|
||||||
|
value: "1.32.0"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 7
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 7
|
||||||
|
memory: 8Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-test-mink8s-release-1-13
|
||||||
|
- name: pull-cluster-api-e2e-mink8s-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
preset-kind-volume-mounts: "true"
|
||||||
|
extra_refs:
|
||||||
|
- org: kubernetes
|
||||||
|
repo: kubernetes
|
||||||
|
base_ref: master
|
||||||
|
path_alias: k8s.io/kubernetes
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
always_run: false
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-e2e.sh
|
||||||
|
env:
|
||||||
|
# enable IPV6 in bootstrap image
|
||||||
|
- name: "DOCKER_IN_DOCKER_IPV6_ENABLED"
|
||||||
|
value: "true"
|
||||||
|
- name: GINKGO_LABEL_FILTER
|
||||||
|
value: "!Conformance"
|
||||||
|
# Ensure required kind images get built.
|
||||||
|
- name: KIND_BUILD_IMAGES
|
||||||
|
value: "KUBERNETES_VERSION,KUBERNETES_VERSION_LATEST_CI,KUBERNETES_VERSION_UPGRADE_TO,KUBERNETES_VERSION_UPGRADE_FROM"
|
||||||
|
# This value determines the minimum Kubernetes
|
||||||
|
# supported version for Cluster API management cluster
|
||||||
|
# and can be found by referring to [Supported Kubernetes Version](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions)
|
||||||
|
# docs (look for minimum supported k8s version for management cluster, i.e N-3).
|
||||||
|
# Please also make sure to refer a version where a kindest/node image exists
|
||||||
|
# for (see https://github.com/kubernetes-sigs/kind/releases/)
|
||||||
|
- name: KUBERNETES_VERSION_MANAGEMENT
|
||||||
|
value: "v1.32.8"
|
||||||
|
# we need privileged mode in order to do docker in docker
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 3000m
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 3000m
|
||||||
|
memory: 8Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-e2e-mink8s-release-1-13
|
||||||
|
- name: pull-cluster-api-e2e-blocking-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
preset-kind-volume-mounts: "true"
|
||||||
|
extra_refs:
|
||||||
|
- org: kubernetes
|
||||||
|
repo: kubernetes
|
||||||
|
base_ref: master
|
||||||
|
path_alias: k8s.io/kubernetes
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util|webhooks|version)/|main\.go|go\.mod|go\.sum|Dockerfile|Makefile)'
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- "./scripts/ci-e2e.sh"
|
||||||
|
env:
|
||||||
|
- name: GINKGO_LABEL_FILTER
|
||||||
|
value: "PR-Blocking"
|
||||||
|
# Ensure required kind images get built.
|
||||||
|
- name: KIND_BUILD_IMAGES
|
||||||
|
value: "KUBERNETES_VERSION"
|
||||||
|
# we need privileged mode in order to do docker in docker
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 3000m
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 3000m
|
||||||
|
memory: 8Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-e2e-blocking-release-1-13
|
||||||
|
- name: pull-cluster-api-e2e-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
preset-kind-volume-mounts: "true"
|
||||||
|
extra_refs:
|
||||||
|
- org: kubernetes
|
||||||
|
repo: kubernetes
|
||||||
|
base_ref: master
|
||||||
|
path_alias: k8s.io/kubernetes
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
always_run: false
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- "./scripts/ci-e2e.sh"
|
||||||
|
env:
|
||||||
|
# enable IPV6 in bootstrap image
|
||||||
|
- name: "DOCKER_IN_DOCKER_IPV6_ENABLED"
|
||||||
|
value: "true"
|
||||||
|
- name: GINKGO_LABEL_FILTER
|
||||||
|
value: "!Conformance"
|
||||||
|
# Ensure required kind images get built.
|
||||||
|
- name: KIND_BUILD_IMAGES
|
||||||
|
value: "KUBERNETES_VERSION,KUBERNETES_VERSION_LATEST_CI,KUBERNETES_VERSION_UPGRADE_TO,KUBERNETES_VERSION_UPGRADE_FROM"
|
||||||
|
# we need privileged mode in order to do docker in docker
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 3000m
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
cpu: 3000m
|
||||||
|
memory: 8Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-e2e-release-1-13
|
||||||
|
- name: pull-cluster-api-e2e-upgrade-1-36-1-37-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
preset-kind-volume-mounts: "true"
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
always_run: false
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
extra_refs:
|
||||||
|
- org: kubernetes
|
||||||
|
repo: kubernetes
|
||||||
|
base_ref: master
|
||||||
|
path_alias: k8s.io/kubernetes
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- "./scripts/ci-e2e.sh"
|
||||||
|
env:
|
||||||
|
- name: ALWAYS_BUILD_KIND_IMAGES
|
||||||
|
value: "true"
|
||||||
|
- name: KUBERNETES_VERSION_UPGRADE_FROM
|
||||||
|
value: "stable-1.36"
|
||||||
|
- name: KUBERNETES_VERSION_UPGRADE_TO
|
||||||
|
value: "ci/latest-1.37"
|
||||||
|
- name: ETCD_VERSION_UPGRADE_TO
|
||||||
|
value: "3.6.8-0"
|
||||||
|
- name: COREDNS_VERSION_UPGRADE_TO
|
||||||
|
value: "v1.14.2"
|
||||||
|
- name: GINKGO_LABEL_FILTER
|
||||||
|
value: "(Conformance && K8s-Upgrade)"
|
||||||
|
# we need privileged mode in order to do docker in docker
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 6000m
|
||||||
|
memory: 6Gi
|
||||||
|
limits:
|
||||||
|
cpu: 6000m
|
||||||
|
memory: 6Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-e2e-release-1-13-1-36-1-37
|
||||||
|
- name: pull-cluster-api-e2e-conformance-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
preset-kind-volume-mounts: "true"
|
||||||
|
extra_refs:
|
||||||
|
- org: kubernetes
|
||||||
|
repo: kubernetes
|
||||||
|
base_ref: master
|
||||||
|
path_alias: k8s.io/kubernetes
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
always_run: false
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- "./scripts/ci-e2e.sh"
|
||||||
|
env:
|
||||||
|
- name: GINKGO_LABEL_FILTER
|
||||||
|
value: "(Conformance && K8s-Install)"
|
||||||
|
# Ensure required kind images get built.
|
||||||
|
- name: KIND_BUILD_IMAGES
|
||||||
|
value: "KUBERNETES_VERSION"
|
||||||
|
# we need privileged mode in order to do docker in docker
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 4000m
|
||||||
|
memory: 4Gi
|
||||||
|
limits:
|
||||||
|
cpu: 4000m
|
||||||
|
memory: 4Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-e2e-conformance-release-1-13
|
||||||
|
- name: pull-cluster-api-e2e-conformance-ci-latest-release-1-13
|
||||||
|
cluster: eks-prow-build-cluster
|
||||||
|
labels:
|
||||||
|
preset-dind-enabled: "true"
|
||||||
|
preset-kind-volume-mounts: "true"
|
||||||
|
extra_refs:
|
||||||
|
- org: kubernetes
|
||||||
|
repo: kubernetes
|
||||||
|
base_ref: master
|
||||||
|
path_alias: k8s.io/kubernetes
|
||||||
|
decorate: true
|
||||||
|
decoration_config:
|
||||||
|
timeout: 180m
|
||||||
|
always_run: false
|
||||||
|
branches:
|
||||||
|
# The script this job runs is not in all branches.
|
||||||
|
- ^release-1.13$
|
||||||
|
path_alias: sigs.k8s.io/cluster-api
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35
|
||||||
|
args:
|
||||||
|
- runner.sh
|
||||||
|
- "./scripts/ci-e2e.sh"
|
||||||
|
env:
|
||||||
|
- name: GINKGO_LABEL_FILTER
|
||||||
|
value: "(Conformance && K8s-Install-ci-latest)"
|
||||||
|
# Ensure required kind images get built.
|
||||||
|
- name: KIND_BUILD_IMAGES
|
||||||
|
value: "KUBERNETES_VERSION_LATEST_CI"
|
||||||
|
# we need privileged mode in order to do docker in docker
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 4000m
|
||||||
|
memory: 4Gi
|
||||||
|
limits:
|
||||||
|
cpu: 4000m
|
||||||
|
memory: 4Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
testgrid-tab-name: capi-pr-e2e-conformance-ci-latest-release-1-13
|
||||||
7
Tests/Fixtures/validation-failures/duplicate-keys.yaml
Normal file
7
Tests/Fixtures/validation-failures/duplicate-keys.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
title: First
|
||||||
|
title: Second
|
||||||
|
paths:
|
||||||
|
/users:
|
||||||
|
get: {}
|
||||||
|
/users:
|
||||||
|
post: {}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
slug: missing-title
|
||||||
|
metadata:
|
||||||
|
draft: true
|
||||||
4
Tests/Fixtures/validation-failures/warnings.yaml
Normal file
4
Tests/Fixtures/validation-failures/warnings.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
title: Legacy
|
||||||
|
deprecated: true
|
||||||
|
nested:
|
||||||
|
deprecated: true
|
||||||
22
Tests/ParsingAliasTests.swift
Normal file
22
Tests/ParsingAliasTests.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@testable import PureYAML
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("Parsing Aliases")
|
||||||
|
struct ParsingAliasTests {
|
||||||
|
@Test("Resolves anchors and aliases through the event composer")
|
||||||
|
func test_anchorsAndAliases() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
first: &item {name: Example}
|
||||||
|
second: *item
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(root?["first"] == root?["second"])
|
||||||
|
guard case let .mapping(second)? = root?["second"] else {
|
||||||
|
recordIssue("expected aliased mapping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#expect(second["name"] == .string("Example"))
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Tests/ParsingErrorTests.swift
Normal file
67
Tests/ParsingErrorTests.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
@testable import PureYAML
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("Parsing Errors")
|
||||||
|
struct ParsingErrorTests {
|
||||||
|
@Test("Reports exact parser errors")
|
||||||
|
func test_errorReporting() {
|
||||||
|
expectParseError("", .emptyDocument)
|
||||||
|
expectParseError("# comment only", .emptyDocument)
|
||||||
|
expectParseError("\tkey: value", .tabIndentation(line: 1))
|
||||||
|
expectParseError("root:\n child: yes\n wrong: no", .unexpectedIndentation(line: 3))
|
||||||
|
expectParseError("- one\nkey: value", .mixedCollectionStyles(line: 2))
|
||||||
|
expectParseError("root:\n child: value\n dangling", .unexpectedToken(
|
||||||
|
expected: "mapping key",
|
||||||
|
actual: "scalar value=\"dangling\" style=plain",
|
||||||
|
line: 3,
|
||||||
|
column: 3,
|
||||||
|
))
|
||||||
|
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
|
||||||
|
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
|
||||||
|
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
|
||||||
|
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
|
||||||
|
expectParseError("value: !!int nope", .invalidTaggedScalar(
|
||||||
|
tag: "tag:yaml.org,2002:int",
|
||||||
|
value: "nope",
|
||||||
|
line: 1,
|
||||||
|
column: 8,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser errors describe returned failures")
|
||||||
|
func test_errorDescriptions() {
|
||||||
|
#expect(PureYAML.Parsing.ParseError.emptyDocument.description == "document is empty")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.tabIndentation(line: 2).description == "tabs are not allowed for indentation at line 2")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3).description == "unexpected indentation at line 3")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.mixedCollectionStyles(line: 4).description == "mapping and sequence entries are mixed at line 4")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.expectedMappingKey(line: 5).description == "expected a mapping key at line 5")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.expectedAnchorName(line: 6).description == "expected an anchor name at line 6")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.expectedAliasName(line: 7).description == "expected an alias name at line 7")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unterminatedTag(line: 8).description == "unterminated tag at line 8")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unterminatedQuotedString(line: 9).description == "unterminated quoted string at line 9")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: 10).description == "incompatible YAML directive at line 10")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unsupportedDirective(name: "%FOO", line: 10).description == "unsupported directive '%FOO' at line 10")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: 10).description == "multi-document streams are not supported at line 10")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.expectedNode(line: 10, column: 2).description == "expected a YAML node at line 10, column 2")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.expectedScalarKey(line: 11, column: 3).description == "expected a scalar mapping key at line 11, column 3")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.undefinedAlias(anchor: "item", line: 12, column: 4).description == "undefined alias 'item' at line 12, column 4")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.invalidTaggedScalar(
|
||||||
|
tag: "tag:yaml.org,2002:int",
|
||||||
|
value: "nope",
|
||||||
|
line: 13,
|
||||||
|
column: 5,
|
||||||
|
).description == "invalid scalar 'nope' for tag 'tag:yaml.org,2002:int' at line 13, column 5")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unexpectedEvent(
|
||||||
|
expected: "mapping key",
|
||||||
|
actual: "sequenceStart anchor=- tag=- style=flow @1:1@0",
|
||||||
|
line: 13,
|
||||||
|
column: 5,
|
||||||
|
).description == "expected mapping key at line 13, column 5, found sequenceStart anchor=- tag=- style=flow @1:1@0")
|
||||||
|
#expect(PureYAML.Parsing.ParseError.unexpectedToken(
|
||||||
|
expected: "stream end",
|
||||||
|
actual: "mappingKey",
|
||||||
|
line: 14,
|
||||||
|
column: 1,
|
||||||
|
).description == "expected stream end at line 14, column 1, found mappingKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
218
Tests/ParsingIndentationRegressionTests.swift
Normal file
218
Tests/ParsingIndentationRegressionTests.swift
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
@testable import PureYAML
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("Parsing Indentation Regressions")
|
||||||
|
struct ParsingIndentationRegressionTests {
|
||||||
|
@Test("Parses compact sequence item mappings without swallowing lower-column siblings")
|
||||||
|
func test_compactSequenceItemMappingsDoNotSwallowLowerColumnSiblings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
patches:
|
||||||
|
- path: patch.yaml
|
||||||
|
namePrefix: demo-
|
||||||
|
|
||||||
|
vars:
|
||||||
|
- name: WORDPRESS_SERVICE
|
||||||
|
objref:
|
||||||
|
kind: Service
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let patch = root?.sequence("patches")?.first?.mapping
|
||||||
|
let variable = root?.sequence("vars")?.first?.mapping
|
||||||
|
|
||||||
|
#expect(patch?["path"] == .string("patch.yaml"))
|
||||||
|
#expect(patch?["namePrefix"] == nil)
|
||||||
|
#expect(root?["namePrefix"] == .string("demo-"))
|
||||||
|
#expect(variable?["name"] == .string("WORDPRESS_SERVICE"))
|
||||||
|
#expect(variable?.mapping("objref")?["kind"] == .string("Service"))
|
||||||
|
#expect(root?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses compact sequence item mappings after indentless sequence values")
|
||||||
|
func test_compactSequenceItemMappingsAfterIndentlessSequenceValues() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-apidiff.sh
|
||||||
|
image: gcr.io/k8s-staging-test-infra/kubekins-e2e
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 6000m
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let container = root?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||||
|
#expect(container?.sequence("command") == [
|
||||||
|
.string("runner.sh"),
|
||||||
|
.string("./scripts/ci-apidiff.sh"),
|
||||||
|
])
|
||||||
|
#expect(container?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e"))
|
||||||
|
#expect(container?.mapping("resources")?.mapping("requests")?["cpu"] == .string("6000m"))
|
||||||
|
#expect(container?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses lower-column mapping siblings after nested compact sequence item mappings")
|
||||||
|
func test_lowerColumnMappingSiblingsAfterNestedCompactSequenceItemMappings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
presubmits:
|
||||||
|
org/repo:
|
||||||
|
- name: first
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- runner.sh
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 6000m
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: core
|
||||||
|
- name: second
|
||||||
|
cluster: build
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let jobs = root?.mapping("presubmits")?.sequence("org/repo")
|
||||||
|
let first = jobs?.first?.mapping
|
||||||
|
let second = jobs?.dropFirst().first?.mapping
|
||||||
|
let container = first?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||||
|
|
||||||
|
#expect(container?.sequence("command") == [.string("runner.sh")])
|
||||||
|
#expect(container?.mapping("resources")?.mapping("requests")?["cpu"] == .string("6000m"))
|
||||||
|
#expect(container?.mapping("resources")?.mapping("limits")?["memory"] == .string("2Gi"))
|
||||||
|
#expect(first?.mapping("annotations")?["testgrid-dashboards"] == .string("core"))
|
||||||
|
#expect(second?["name"] == .string("second"))
|
||||||
|
#expect(second?["cluster"] == .string("build"))
|
||||||
|
#expect(first?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses indentless sequence values followed by flow siblings")
|
||||||
|
func test_indentlessSequenceValuesFollowedByFlowSiblings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
approveSignerNames:
|
||||||
|
- issuers.cert-manager.io/*
|
||||||
|
- clusterissuers.cert-manager.io/*
|
||||||
|
extraArgs: []
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(root?.sequence("approveSignerNames") == [
|
||||||
|
.string("issuers.cert-manager.io/*"),
|
||||||
|
.string("clusterissuers.cert-manager.io/*"),
|
||||||
|
])
|
||||||
|
#expect(root?.sequence("extraArgs") == [])
|
||||||
|
#expect(root?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses nested block sequence values followed by mapping siblings")
|
||||||
|
func test_nestedBlockSequenceValuesFollowedByMappingSiblings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
sort_by:
|
||||||
|
- - 123
|
||||||
|
- asc
|
||||||
|
- - 456
|
||||||
|
- desc
|
||||||
|
group_by:
|
||||||
|
- 123
|
||||||
|
vertical_group_by:
|
||||||
|
- 456
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(root?.sequence("sort_by") == [
|
||||||
|
.sequence([.int(123), .string("asc")]),
|
||||||
|
.sequence([.int(456), .string("desc")]),
|
||||||
|
])
|
||||||
|
#expect(root?.sequence("group_by") == [.int(123)])
|
||||||
|
#expect(root?.sequence("vertical_group_by") == [.int(456)])
|
||||||
|
#expect(root?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses explicit block scalar indentation indicators")
|
||||||
|
func test_explicitBlockScalarIndentationIndicators() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
example:
|
||||||
|
value: |2
|
||||||
|
MMM. .MMM
|
||||||
|
MMMMMMMMMMMMMMMMMMM
|
||||||
|
MMMMMMMMMMMMMMMMMMMMM | Avoid administrative distraction. |
|
||||||
|
next: done
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(
|
||||||
|
root?.mapping("example")?["value"] ==
|
||||||
|
.string(" MMM. .MMM\n MMMMMMMMMMMMMMMMMMM\nMMMMMMMMMMMMMMMMMMMMM | Avoid administrative distraction. |\n"),
|
||||||
|
)
|
||||||
|
#expect(root?.mapping("example")?["next"] == .string("done"))
|
||||||
|
#expect(root?.mapping("example")?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses sequence item mappings after nested values")
|
||||||
|
func test_sequenceItemMappingsAfterNestedValues() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- sleep
|
||||||
|
- "3600"
|
||||||
|
image: busybox
|
||||||
|
name: worker
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let containers = requireSequence(root?["containers"])
|
||||||
|
guard let firstContainer = containers?.first else {
|
||||||
|
recordIssue("expected first container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let container = requireMapping(firstContainer)
|
||||||
|
|
||||||
|
#expect(container?["command"] == .sequence([
|
||||||
|
.string("sleep"),
|
||||||
|
.string("3600"),
|
||||||
|
]))
|
||||||
|
#expect(container?["image"] == .string("busybox"))
|
||||||
|
#expect(container?["name"] == .string("worker"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses sequence item mapping siblings after deep nested mapping values")
|
||||||
|
func test_sequenceItemMappingSiblingsAfterDeepNestedMappingValues() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
runs:
|
||||||
|
- tool:
|
||||||
|
driver:
|
||||||
|
name: CodeQL
|
||||||
|
rules:
|
||||||
|
- id: js/unused-local-variable
|
||||||
|
name: js/unused-local-variable
|
||||||
|
results:
|
||||||
|
- guid: 326aa09f
|
||||||
|
message:
|
||||||
|
text: Unused variable foo.
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let run = root?.sequence("runs")?.first?.mapping
|
||||||
|
let driver = run?.mapping("tool")?.mapping("driver")
|
||||||
|
let rule = driver?.sequence("rules")?.first?.mapping
|
||||||
|
let result = run?.sequence("results")?.first?.mapping
|
||||||
|
|
||||||
|
#expect(driver?["name"] == .string("CodeQL"))
|
||||||
|
#expect(rule?["id"] == .string("js/unused-local-variable"))
|
||||||
|
#expect(rule?["name"] == .string("js/unused-local-variable"))
|
||||||
|
#expect(result?["guid"] == .string("326aa09f"))
|
||||||
|
#expect(result?.mapping("message")?["text"] == .string("Unused variable foo."))
|
||||||
|
#expect(run?["missing"] == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
199
Tests/ParsingMultilineScalarTests.swift
Normal file
199
Tests/ParsingMultilineScalarTests.swift
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
@testable import PureYAML
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("Parsing Multiline Scalars")
|
||||||
|
struct ParsingMultilineScalarTests {
|
||||||
|
@Test("Parses multiline plain scalars in mappings")
|
||||||
|
func test_multilinePlainScalarsInMappings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
columns:
|
||||||
|
- description: CreationTimestamp is a timestamp representing the server time when
|
||||||
|
this object was created. It is not guaranteed to be set in happens-before
|
||||||
|
order across separate operations.
|
||||||
|
jsonPath: .metadata.creationTimestamp
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let expectedDescription = [
|
||||||
|
"CreationTimestamp is a timestamp representing the server time when this object was created.",
|
||||||
|
"It is not guaranteed to be set in happens-before order across separate operations.",
|
||||||
|
].joined(separator: " ")
|
||||||
|
let column = root?.sequence("columns")?.first?.mapping
|
||||||
|
#expect(column?["description"] == .string(expectedDescription))
|
||||||
|
#expect(column?["jsonPath"] == .string(".metadata.creationTimestamp"))
|
||||||
|
#expect(column?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses multiline plain scalars before the next sequence entry")
|
||||||
|
func test_multilinePlainScalarsBeforeNextSequenceEntry() throws {
|
||||||
|
let value = try PureYAML.parse(
|
||||||
|
"""
|
||||||
|
- name: code-quality
|
||||||
|
description: Insights into reliability, maintainability, and efficiency of your
|
||||||
|
codebase.
|
||||||
|
- name: codespaces
|
||||||
|
description: Endpoints to manage Codespaces using the REST API.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
let tags = requireSequence(value)
|
||||||
|
let first = tags?.first?.mapping
|
||||||
|
let second = tags?.dropFirst().first?.mapping
|
||||||
|
|
||||||
|
#expect(first?["name"] == .string("code-quality"))
|
||||||
|
#expect(first?["description"] == .string("Insights into reliability, maintainability, and efficiency of your codebase."))
|
||||||
|
#expect(second?["name"] == .string("codespaces"))
|
||||||
|
#expect(second?["description"] == .string("Endpoints to manage Codespaces using the REST API."))
|
||||||
|
#expect(first?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses multiline plain scalar sequence items")
|
||||||
|
func test_multilinePlainScalarSequenceItems() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
examples:
|
||||||
|
- Updated instructions to help the team with planning and coordination
|
||||||
|
tasks
|
||||||
|
next: done
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(root?.sequence("examples") == [
|
||||||
|
.string("Updated instructions to help the team with planning and coordination tasks"),
|
||||||
|
])
|
||||||
|
#expect(root?["next"] == .string("done"))
|
||||||
|
#expect(root?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses multiline plain scalar continuations that start with quotes")
|
||||||
|
func test_multilinePlainScalarContinuationsThatStartWithQuotes() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
get:
|
||||||
|
description: Gets a team using the team's slug. To create the slug, GitHub replaces
|
||||||
|
special characters in the name string, changes all words to lowercase, and
|
||||||
|
replaces spaces with a `-` separator and adds the "ent:" prefix. For example,
|
||||||
|
"My TEam Näme" would become `ent:my-team-name`.
|
||||||
|
tags:
|
||||||
|
- enterprise-teams
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let expectedDescription = [
|
||||||
|
"Gets a team using the team's slug.",
|
||||||
|
"To create the slug, GitHub replaces special characters in the name string,",
|
||||||
|
"changes all words to lowercase, and replaces spaces with a `-` separator",
|
||||||
|
"and adds the \"ent:\" prefix. For example,",
|
||||||
|
"\"My TEam Näme\" would become `ent:my-team-name`.",
|
||||||
|
].joined(separator: " ")
|
||||||
|
let get = root?.mapping("get")
|
||||||
|
#expect(get?["description"] == .string(expectedDescription))
|
||||||
|
#expect(get?.sequence("tags") == [.string("enterprise-teams")])
|
||||||
|
#expect(get?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses multiline plain scalar continuations that start with flow markers")
|
||||||
|
func test_multilinePlainScalarContinuationsThatStartWithFlowMarkers() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
selected_repository_ids:
|
||||||
|
description: An array of repository ids that can access the organization
|
||||||
|
secret. You can manage the list of
|
||||||
|
selected repositories using the [List selected repositories for
|
||||||
|
an organization secret](https://docs.example.com/list),
|
||||||
|
[Set selected repositories for an organization secret](https://docs.example.com/set),
|
||||||
|
and [Remove selected repository from an organization secret](https://docs.example.com/remove)
|
||||||
|
endpoints.
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let expectedDescription = [
|
||||||
|
"An array of repository ids that can access the organization secret.",
|
||||||
|
"You can manage the list of selected repositories using the",
|
||||||
|
"[List selected repositories for an organization secret](https://docs.example.com/list),",
|
||||||
|
"[Set selected repositories for an organization secret](https://docs.example.com/set),",
|
||||||
|
"and [Remove selected repository from an organization secret](https://docs.example.com/remove) endpoints.",
|
||||||
|
].joined(separator: " ")
|
||||||
|
let selected = root?.mapping("selected_repository_ids")
|
||||||
|
#expect(selected?["description"] == .string(expectedDescription))
|
||||||
|
#expect(selected?.mapping("items")?["type"] == .string("integer"))
|
||||||
|
#expect(selected?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses flow-marker continuations after previous sequence indentation")
|
||||||
|
func test_flowMarkerContinuationsAfterPreviousSequenceIndentation() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
schema:
|
||||||
|
enum:
|
||||||
|
- all
|
||||||
|
- private
|
||||||
|
selected_repository_ids:
|
||||||
|
description: An array of repository ids that can access the organization
|
||||||
|
[Set selected repositories](https://docs.example.com/set),
|
||||||
|
and more.
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let selected = root?.mapping("schema")?.mapping("selected_repository_ids")
|
||||||
|
#expect(
|
||||||
|
selected?["description"] ==
|
||||||
|
.string("An array of repository ids that can access the organization [Set selected repositories](https://docs.example.com/set), and more."),
|
||||||
|
)
|
||||||
|
#expect(selected?.mapping("items")?["type"] == .string("integer"))
|
||||||
|
#expect(selected?["missing"] == nil)
|
||||||
|
#expect(root?.mapping("schema")?.sequence("enum") == [.string("all"), .string("private")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses colon-style markup inside multiline plain scalars")
|
||||||
|
func test_colonStyleMarkupInsideMultilinePlainScalars() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
label:
|
||||||
|
description: The name of the label. Emoji can be added to label
|
||||||
|
names, using either native emoji or colon-style markup. For example,
|
||||||
|
typing `:strawberry:` will render the emoji . For a full list of available emoji and codes.
|
||||||
|
color: f29513
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let expectedDescription = [
|
||||||
|
"The name of the label. Emoji can be added to label names,",
|
||||||
|
"using either native emoji or colon-style markup. For example,",
|
||||||
|
"typing `:strawberry:` will render the emoji",
|
||||||
|
".",
|
||||||
|
"For a full list of available emoji and codes.",
|
||||||
|
].joined(separator: " ")
|
||||||
|
#expect(root?.mapping("label")?["description"] == .string(expectedDescription))
|
||||||
|
#expect(root?.mapping("label")?["color"] == .string("f29513"))
|
||||||
|
#expect(root?.mapping("label")?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses block scalar text that starts with YAML indicators")
|
||||||
|
func test_blockScalarTextThatStartsWithYAMLIndicators() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
description: |
|
||||||
|
**Fine-grained access tokens**
|
||||||
|
|
||||||
|
* [GitHub App user access tokens](https://docs.github.com)
|
||||||
|
- dash list item stays text
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(root?["description"] == .string(
|
||||||
|
"""
|
||||||
|
**Fine-grained access tokens**
|
||||||
|
* [GitHub App user access tokens](https://docs.github.com)
|
||||||
|
- dash list item stays text
|
||||||
|
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -130,6 +130,89 @@ struct ParsingTests {
|
|||||||
#expect(second["description"] == .string("Production"))
|
#expect(second["description"] == .string("Production"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Parses indentless sequences as mapping values")
|
||||||
|
func test_indentlessSequencesAsMappingValues() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
resources:
|
||||||
|
- wordpress
|
||||||
|
- mysql
|
||||||
|
patches:
|
||||||
|
- path: patch.yaml
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(root?["resources"] == .sequence([
|
||||||
|
.string("wordpress"),
|
||||||
|
.string("mysql"),
|
||||||
|
]))
|
||||||
|
#expect(root?["patches"] == .sequence([
|
||||||
|
.mapping(.init([
|
||||||
|
.init(key: "path", value: .string("patch.yaml")),
|
||||||
|
])),
|
||||||
|
]))
|
||||||
|
#expect(root?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses nested indentless sequence values followed by mapping siblings")
|
||||||
|
func test_nestedIndentlessSequenceValuesInsideMappingsFollowedBySiblings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
spec:
|
||||||
|
names:
|
||||||
|
categories:
|
||||||
|
- cert-manager
|
||||||
|
kind: Certificate
|
||||||
|
shortNames:
|
||||||
|
- cert
|
||||||
|
- certs
|
||||||
|
singular: certificate
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let names = root?.mapping("spec")?.mapping("names")
|
||||||
|
#expect(names?.sequence("categories") == [.string("cert-manager")])
|
||||||
|
#expect(names?["kind"] == .string("Certificate"))
|
||||||
|
#expect(names?.sequence("shortNames") == [.string("cert"), .string("certs")])
|
||||||
|
#expect(names?["singular"] == .string("certificate"))
|
||||||
|
#expect(names?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses indentless sequence values inside sequence item mappings")
|
||||||
|
func test_indentlessSequenceValuesInsideSequenceItemMappings() throws {
|
||||||
|
let root = try requireMapping(PureYAML.parse(
|
||||||
|
"""
|
||||||
|
presubmits:
|
||||||
|
kubernetes-sigs/cluster-api:
|
||||||
|
- name: pull-cluster-api-build-release-1-13
|
||||||
|
branches:
|
||||||
|
- ^release-1.13$
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: gcr.io/k8s-staging-test-infra/kubekins-e2e
|
||||||
|
command:
|
||||||
|
- runner.sh
|
||||||
|
- ./scripts/ci-build.sh
|
||||||
|
annotations:
|
||||||
|
testgrid-dashboards: cluster-api-core-1.13
|
||||||
|
""",
|
||||||
|
))
|
||||||
|
|
||||||
|
let jobs = root?.mapping("presubmits")?.sequence("kubernetes-sigs/cluster-api")
|
||||||
|
let job = jobs?.first?.mapping
|
||||||
|
let container = job?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||||
|
|
||||||
|
#expect(job?["name"] == .string("pull-cluster-api-build-release-1-13"))
|
||||||
|
#expect(job?.sequence("branches") == [.string("^release-1.13$")])
|
||||||
|
#expect(container?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e"))
|
||||||
|
#expect(container?.sequence("command") == [
|
||||||
|
.string("runner.sh"),
|
||||||
|
.string("./scripts/ci-build.sh"),
|
||||||
|
])
|
||||||
|
#expect(job?.mapping("annotations")?["testgrid-dashboards"] == .string("cluster-api-core-1.13"))
|
||||||
|
#expect(job?["missing"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
@Test("Parses top-level sequences")
|
@Test("Parses top-level sequences")
|
||||||
func test_topLevelSequences() throws {
|
func test_topLevelSequences() throws {
|
||||||
let value = try PureYAML.parse(
|
let value = try PureYAML.parse(
|
||||||
@ -189,83 +272,4 @@ struct ParsingTests {
|
|||||||
#expect(root?["stripped"] == .string("one\ntwo"))
|
#expect(root?["stripped"] == .string("one\ntwo"))
|
||||||
#expect(root?["folded"] == .string("one two\n"))
|
#expect(root?["folded"] == .string("one two\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Resolves anchors and aliases through the event composer")
|
|
||||||
func test_anchorsAndAliases() throws {
|
|
||||||
let root = try requireMapping(PureYAML.parse(
|
|
||||||
"""
|
|
||||||
first: &item {name: Example}
|
|
||||||
second: *item
|
|
||||||
""",
|
|
||||||
))
|
|
||||||
|
|
||||||
#expect(root?["first"] == root?["second"])
|
|
||||||
guard case let .mapping(second)? = root?["second"] else {
|
|
||||||
recordIssue("expected aliased mapping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
#expect(second["name"] == .string("Example"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Reports exact parser errors")
|
|
||||||
func test_errorReporting() {
|
|
||||||
expectParseError("", .emptyDocument)
|
|
||||||
expectParseError("# comment only", .emptyDocument)
|
|
||||||
expectParseError("\tkey: value", .tabIndentation(line: 1))
|
|
||||||
expectParseError("root:\n child: yes\n wrong: no", .unexpectedIndentation(line: 3))
|
|
||||||
expectParseError("- one\nkey: value", .mixedCollectionStyles(line: 2))
|
|
||||||
expectParseError("root:\n child: value\n dangling", .unexpectedToken(
|
|
||||||
expected: "mapping key",
|
|
||||||
actual: "scalar value=\"dangling\" style=plain",
|
|
||||||
line: 3,
|
|
||||||
column: 3,
|
|
||||||
))
|
|
||||||
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
|
|
||||||
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
|
|
||||||
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
|
|
||||||
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
|
|
||||||
expectParseError("value: !!int nope", .invalidTaggedScalar(
|
|
||||||
tag: "tag:yaml.org,2002:int",
|
|
||||||
value: "nope",
|
|
||||||
line: 1,
|
|
||||||
column: 8,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("Parser errors describe returned failures")
|
|
||||||
func test_errorDescriptions() {
|
|
||||||
#expect(PureYAML.Parsing.ParseError.emptyDocument.description == "document is empty")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.tabIndentation(line: 2).description == "tabs are not allowed for indentation at line 2")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3).description == "unexpected indentation at line 3")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.mixedCollectionStyles(line: 4).description == "mapping and sequence entries are mixed at line 4")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.expectedMappingKey(line: 5).description == "expected a mapping key at line 5")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.expectedAnchorName(line: 6).description == "expected an anchor name at line 6")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.expectedAliasName(line: 7).description == "expected an alias name at line 7")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unterminatedTag(line: 8).description == "unterminated tag at line 8")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unterminatedQuotedString(line: 9).description == "unterminated quoted string at line 9")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: 10).description == "incompatible YAML directive at line 10")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unsupportedDirective(name: "%FOO", line: 10).description == "unsupported directive '%FOO' at line 10")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: 10).description == "multi-document streams are not supported at line 10")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.expectedNode(line: 10, column: 2).description == "expected a YAML node at line 10, column 2")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.expectedScalarKey(line: 11, column: 3).description == "expected a scalar mapping key at line 11, column 3")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.undefinedAlias(anchor: "item", line: 12, column: 4).description == "undefined alias 'item' at line 12, column 4")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.invalidTaggedScalar(
|
|
||||||
tag: "tag:yaml.org,2002:int",
|
|
||||||
value: "nope",
|
|
||||||
line: 13,
|
|
||||||
column: 5,
|
|
||||||
).description == "invalid scalar 'nope' for tag 'tag:yaml.org,2002:int' at line 13, column 5")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unexpectedEvent(
|
|
||||||
expected: "mapping key",
|
|
||||||
actual: "sequenceStart anchor=- tag=- style=flow @1:1@0",
|
|
||||||
line: 13,
|
|
||||||
column: 5,
|
|
||||||
).description == "expected mapping key at line 13, column 5, found sequenceStart anchor=- tag=- style=flow @1:1@0")
|
|
||||||
#expect(PureYAML.Parsing.ParseError.unexpectedToken(
|
|
||||||
expected: "stream end",
|
|
||||||
actual: "mappingKey",
|
|
||||||
line: 14,
|
|
||||||
column: 1,
|
|
||||||
).description == "expected stream end at line 14, column 1, found mappingKey")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
355
Tests/RealYAMLFixtureTests.swift
Normal file
355
Tests/RealYAMLFixtureTests.swift
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
import Foundation
|
||||||
|
@testable import PureYAML
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("Real YAML Fixtures")
|
||||||
|
struct RealYAMLFixtureTests {
|
||||||
|
@Test("Real YAML fixture files match their pinned manifest", arguments: RealYAMLFixture.allCases)
|
||||||
|
func test_realYAMLFixtureFilesMatchPinnedManifest(fixture: RealYAMLFixture) throws {
|
||||||
|
let source = try fixture.load()
|
||||||
|
|
||||||
|
#expect(source.utf8.count == fixture.byteCount)
|
||||||
|
#expect(source.count(where: { $0 == "\n" }) == fixture.lineCount)
|
||||||
|
|
||||||
|
for requiredSubstring in fixture.requiredSubstrings {
|
||||||
|
#expect(source.contains(requiredSubstring))
|
||||||
|
}
|
||||||
|
for forbiddenSubstring in fixture.forbiddenSubstrings {
|
||||||
|
#expect(!source.contains(forbiddenSubstring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parses real YAML fixtures with exact representative values", arguments: RealYAMLFixture.allCases)
|
||||||
|
func test_parsesRealYAMLFixturesWithExactRepresentativeValues(fixture: RealYAMLFixture) throws {
|
||||||
|
let value = try PureYAML.parse(fixture.load())
|
||||||
|
|
||||||
|
fixture.expectRepresentativeValues(in: value)
|
||||||
|
#expect(PureYAML.Validation.Validator().collect(value) == PureYAML.Validation.Result())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RealYAMLFixture: String, CaseIterable, CustomStringConvertible {
|
||||||
|
case certManagerCertificateCRD = "cert-manager-certificate-crd.yaml"
|
||||||
|
case certManagerValues = "cert-manager-values.yaml"
|
||||||
|
case dockerComposeNginxGolangPostgres = "docker-compose-nginx-golang-postgres.yaml"
|
||||||
|
case githubActionsSwiftFormat = "github-actions-swift-format.yml"
|
||||||
|
case githubRestAPI = "github-rest-api.yaml"
|
||||||
|
case kubernetesSimplePod = "kubernetes-simple-pod.yaml"
|
||||||
|
case kustomizeWordPress = "kustomize-wordpress.yaml"
|
||||||
|
case openAPIPetstore = "openapi-petstore.yaml"
|
||||||
|
case prometheusGoodConfig = "prometheus-conf-good.yml"
|
||||||
|
case prowClusterAPIPresubmits = "prow-cluster-api-presubmits.yaml"
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineCount: Int {
|
||||||
|
switch self {
|
||||||
|
case .certManagerCertificateCRD:
|
||||||
|
883
|
||||||
|
case .certManagerValues:
|
||||||
|
1759
|
||||||
|
case .dockerComposeNginxGolangPostgres:
|
||||||
|
48
|
||||||
|
case .githubActionsSwiftFormat:
|
||||||
|
84
|
||||||
|
case .githubRestAPI:
|
||||||
|
257_280
|
||||||
|
case .kubernetesSimplePod:
|
||||||
|
12
|
||||||
|
case .kustomizeWordPress:
|
||||||
|
18
|
||||||
|
case .openAPIPetstore:
|
||||||
|
119
|
||||||
|
case .prometheusGoodConfig:
|
||||||
|
481
|
||||||
|
case .prowClusterAPIPresubmits:
|
||||||
|
427
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var byteCount: Int {
|
||||||
|
switch self {
|
||||||
|
case .certManagerCertificateCRD:
|
||||||
|
44059
|
||||||
|
case .certManagerValues:
|
||||||
|
65536
|
||||||
|
case .dockerComposeNginxGolangPostgres:
|
||||||
|
852
|
||||||
|
case .githubActionsSwiftFormat:
|
||||||
|
3591
|
||||||
|
case .githubRestAPI:
|
||||||
|
9_549_118
|
||||||
|
case .kubernetesSimplePod:
|
||||||
|
186
|
||||||
|
case .kustomizeWordPress:
|
||||||
|
262
|
||||||
|
case .openAPIPetstore:
|
||||||
|
2768
|
||||||
|
case .prometheusGoodConfig:
|
||||||
|
11562
|
||||||
|
case .prowClusterAPIPresubmits:
|
||||||
|
14213
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredSubstrings: [String] {
|
||||||
|
switch self {
|
||||||
|
case .certManagerCertificateCRD:
|
||||||
|
[
|
||||||
|
"apiVersion: apiextensions.k8s.io/v1",
|
||||||
|
"jsonPath: .status.conditions[?(@.type == \"Ready\")].status",
|
||||||
|
"Known condition types are `Ready` and `Issuing`.",
|
||||||
|
]
|
||||||
|
case .certManagerValues:
|
||||||
|
[
|
||||||
|
"installCRDs: false",
|
||||||
|
"hostUsers: false",
|
||||||
|
"webhook:",
|
||||||
|
]
|
||||||
|
case .dockerComposeNginxGolangPostgres:
|
||||||
|
[
|
||||||
|
"services:",
|
||||||
|
"test: [ \"CMD\", \"pg_isready\" ]",
|
||||||
|
"file: db/password.txt",
|
||||||
|
]
|
||||||
|
case .githubActionsSwiftFormat:
|
||||||
|
[
|
||||||
|
"name: Pull request",
|
||||||
|
"types: [opened, reopened, synchronize, ready_for_review]",
|
||||||
|
"run: |",
|
||||||
|
]
|
||||||
|
case .githubRestAPI:
|
||||||
|
[
|
||||||
|
"openapi: 3.1.0",
|
||||||
|
"title: GitHub v3 REST API",
|
||||||
|
"components:",
|
||||||
|
]
|
||||||
|
case .kubernetesSimplePod:
|
||||||
|
[
|
||||||
|
"kind: Pod",
|
||||||
|
"name: pods-simple-container",
|
||||||
|
]
|
||||||
|
case .kustomizeWordPress:
|
||||||
|
[
|
||||||
|
"namePrefix: demo-",
|
||||||
|
"name: WORDPRESS_SERVICE",
|
||||||
|
]
|
||||||
|
case .openAPIPetstore:
|
||||||
|
[
|
||||||
|
"title: Swagger Petstore",
|
||||||
|
"operationId: listPets",
|
||||||
|
"'201':",
|
||||||
|
]
|
||||||
|
case .prometheusGoodConfig:
|
||||||
|
[
|
||||||
|
"scrape_interval: 15s",
|
||||||
|
"fallback_scrape_protocol: PrometheusText0.0.4",
|
||||||
|
"password: \"multiline\\nmysecret\\ntest\"",
|
||||||
|
]
|
||||||
|
case .prowClusterAPIPresubmits:
|
||||||
|
[
|
||||||
|
"presubmits:",
|
||||||
|
"run_if_changed: '^((api|bootstrap|cmd|config|controllers|controlplane|errors|exp|feature|hack|internal|scripts|test|util"
|
||||||
|
+ "|webhooks|version)/|main\\.go|go\\.mod|go\\.sum|Dockerfile|Makefile)'",
|
||||||
|
"securityContext:",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var forbiddenSubstrings: [String] {
|
||||||
|
[
|
||||||
|
"<<<<<<<",
|
||||||
|
"=======",
|
||||||
|
">>>>>>>",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func load() throws -> String {
|
||||||
|
let url = try #require(Bundle.module.url(
|
||||||
|
forResource: resourceName,
|
||||||
|
withExtension: resourceExtension,
|
||||||
|
subdirectory: "Fixtures/real-yaml",
|
||||||
|
))
|
||||||
|
return try String(contentsOf: url, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectRepresentativeValues(in value: PureYAML.Model.Value) {
|
||||||
|
switch self {
|
||||||
|
case .certManagerCertificateCRD:
|
||||||
|
expectCertManagerCertificateCRD(value)
|
||||||
|
case .certManagerValues:
|
||||||
|
expectCertManagerValues(value)
|
||||||
|
case .dockerComposeNginxGolangPostgres:
|
||||||
|
expectDockerComposeNginxGolangPostgres(value)
|
||||||
|
case .githubActionsSwiftFormat:
|
||||||
|
expectGitHubActionsSwiftFormat(value)
|
||||||
|
case .githubRestAPI:
|
||||||
|
expectGitHubRestAPI(value)
|
||||||
|
case .kubernetesSimplePod:
|
||||||
|
expectKubernetesSimplePod(value)
|
||||||
|
case .kustomizeWordPress:
|
||||||
|
expectKustomizeWordPress(value)
|
||||||
|
case .openAPIPetstore:
|
||||||
|
expectOpenAPIPetstore(value)
|
||||||
|
case .prometheusGoodConfig:
|
||||||
|
expectPrometheusGoodConfig(value)
|
||||||
|
case .prowClusterAPIPresubmits:
|
||||||
|
expectProwClusterAPIPresubmits(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resourceName: String {
|
||||||
|
guard let dotIndex = rawValue.lastIndex(of: ".") else {
|
||||||
|
return rawValue
|
||||||
|
}
|
||||||
|
return String(rawValue[..<dotIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resourceExtension: String {
|
||||||
|
guard let dotIndex = rawValue.lastIndex(of: ".") else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return String(rawValue[rawValue.index(after: dotIndex)...])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectCertManagerCertificateCRD(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?["apiVersion"] == .string("apiextensions.k8s.io/v1"))
|
||||||
|
#expect(root?["kind"] == .string("CustomResourceDefinition"))
|
||||||
|
#expect(root?.mapping("metadata")?["name"] == .string("certificates.cert-manager.io"))
|
||||||
|
|
||||||
|
let spec = root?.mapping("spec")
|
||||||
|
#expect(spec?.mapping("names")?["kind"] == .string("Certificate"))
|
||||||
|
|
||||||
|
let versions = requireSequence(spec?["versions"])
|
||||||
|
let firstVersion = versions?.first?.mapping
|
||||||
|
let firstColumn = firstVersion?.sequence("additionalPrinterColumns")?.first?.mapping
|
||||||
|
|
||||||
|
#expect(firstColumn?["name"] == .string("Ready"))
|
||||||
|
#expect(firstColumn?["jsonPath"] == .string(".status.conditions[?(@.type == \"Ready\")].status"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectCertManagerValues(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?.mapping("global")?.mapping("rbac")?["create"] == .bool(true))
|
||||||
|
#expect(root?["installCRDs"] == .bool(false))
|
||||||
|
#expect(root?.mapping("crds")?["enabled"] == .bool(false))
|
||||||
|
#expect(root?.mapping("crds")?["keep"] == .bool(true))
|
||||||
|
#expect(root?["replicaCount"] == .int(1))
|
||||||
|
#expect(root?.mapping("webhook")?["timeoutSeconds"] == .int(30))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectDockerComposeNginxGolangPostgres(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
let services = root?.mapping("services")
|
||||||
|
|
||||||
|
#expect(services?.mapping("backend")?.mapping("build")?["context"] == .string("backend"))
|
||||||
|
#expect(services?.mapping("backend")?.mapping("depends_on")?.mapping("db")?["condition"] == .string("service_healthy"))
|
||||||
|
#expect(services?.mapping("db")?["image"] == .string("postgres"))
|
||||||
|
#expect(services?.mapping("db")?.sequence("healthcheck", "test") == [.string("CMD"), .string("pg_isready")])
|
||||||
|
#expect(services?.mapping("proxy")?.sequence("depends_on") == [.string("backend")])
|
||||||
|
#expect(root?.mapping("secrets")?.mapping("db-password")?["file"] == .string("db/password.txt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectGitHubActionsSwiftFormat(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?["name"] == .string("Pull request"))
|
||||||
|
#expect(root?.mapping("permissions")?["contents"] == .string("read"))
|
||||||
|
#expect(root?.mapping("on")?.mapping("pull_request")?.sequence("types") == [
|
||||||
|
.string("opened"),
|
||||||
|
.string("reopened"),
|
||||||
|
.string("synchronize"),
|
||||||
|
.string("ready_for_review"),
|
||||||
|
])
|
||||||
|
#expect(root?.mapping("concurrency")?["cancel-in-progress"] == .bool(true))
|
||||||
|
#expect(
|
||||||
|
root?.mapping("jobs")?.mapping("tests")?["uses"] ==
|
||||||
|
.string("swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11"),
|
||||||
|
)
|
||||||
|
#expect(root?.mapping("jobs")?.mapping("compatibility_check")?.mapping("container")?["image"] == .string("swift:6.2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectGitHubRestAPI(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?["openapi"] == .string("3.1.0"))
|
||||||
|
#expect(root?.mapping("info")?["title"] == .string("GitHub v3 REST API"))
|
||||||
|
#expect(root?.mapping("info")?["version"] == .string("1.1.4"))
|
||||||
|
|
||||||
|
let firstTag = root?.sequence("tags")?.first?.mapping
|
||||||
|
#expect(firstTag?["name"] == .string("actions"))
|
||||||
|
#expect(root?.mapping("components")?.mapping("schemas")?.mapping("repository")?.mapping("properties")?.mapping("full_name")?["type"] == .string("string"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectKubernetesSimplePod(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?["apiVersion"] == .string("v1"))
|
||||||
|
#expect(root?["kind"] == .string("Pod"))
|
||||||
|
#expect(root?.mapping("metadata")?["name"] == .string("pods-simple-pod"))
|
||||||
|
|
||||||
|
let container = root?.mapping("spec")?.sequence("containers")?.first?.mapping
|
||||||
|
#expect(container?["image"] == .string("busybox"))
|
||||||
|
#expect(container?["name"] == .string("pods-simple-container"))
|
||||||
|
#expect(container?.sequence("command") == [.string("sleep"), .string("3600")])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectKustomizeWordPress(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?.sequence("resources") == [.string("wordpress"), .string("mysql")])
|
||||||
|
#expect(root?.sequence("patches")?.first?.mapping?["path"] == .string("patch.yaml"))
|
||||||
|
#expect(root?["namePrefix"] == .string("demo-"))
|
||||||
|
|
||||||
|
let firstVariable = root?.sequence("vars")?.first?.mapping
|
||||||
|
#expect(firstVariable?["name"] == .string("WORDPRESS_SERVICE"))
|
||||||
|
#expect(firstVariable?.mapping("objref")?["kind"] == .string("Service"))
|
||||||
|
#expect(firstVariable?.mapping("objref")?["name"] == .string("wordpress"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectOpenAPIPetstore(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?["openapi"] == .string("3.0.0"))
|
||||||
|
#expect(root?.mapping("info")?["title"] == .string("Swagger Petstore"))
|
||||||
|
#expect(root?.mapping("paths")?.mapping("/pets")?.mapping("get")?["operationId"] == .string("listPets"))
|
||||||
|
#expect(root?.mapping("paths")?.mapping("/pets")?.mapping("post")?.mapping("responses")?["201"] != nil)
|
||||||
|
#expect(root?.mapping("components")?.mapping("schemas")?.mapping("Pet")?.sequence("required") == [
|
||||||
|
.string("id"),
|
||||||
|
.string("name"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectPrometheusGoodConfig(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
|
||||||
|
#expect(root?.mapping("global")?["scrape_interval"] == .string("15s"))
|
||||||
|
#expect(root?.mapping("global")?.mapping("external_labels")?["monitor"] == .string("codelab"))
|
||||||
|
#expect(root?.sequence("remote_write")?.first?.mapping?.mapping("oauth2")?["client_id"] == .string("123"))
|
||||||
|
|
||||||
|
let staticTargets = root?
|
||||||
|
.sequence("scrape_configs")?.first?
|
||||||
|
.mapping?
|
||||||
|
.sequence("static_configs")?.first?
|
||||||
|
.mapping?
|
||||||
|
.sequence("targets")
|
||||||
|
#expect(staticTargets == [.string("localhost:9090"), .string("localhost:9191")])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectProwClusterAPIPresubmits(_ value: PureYAML.Model.Value) {
|
||||||
|
let root = requireMapping(value)
|
||||||
|
let jobs = root?.mapping("presubmits")?.sequence("kubernetes-sigs/cluster-api")
|
||||||
|
let firstJob = jobs?.first?.mapping
|
||||||
|
|
||||||
|
#expect(firstJob?["name"] == .string("pull-cluster-api-build-release-1-13"))
|
||||||
|
#expect(firstJob?["cluster"] == .string("eks-prow-build-cluster"))
|
||||||
|
#expect(firstJob?["always_run"] == .bool(true))
|
||||||
|
#expect(firstJob?.sequence("branches") == [.string("^release-1.13$")])
|
||||||
|
#expect(firstJob?.mapping("spec")?.sequence("containers")?.first?.mapping?["image"] == .string("gcr.io/k8s-staging-test-infra/kubekins-e2e:v20260601-2cbf4bdb47-1.35"))
|
||||||
|
}
|
||||||
@ -31,6 +31,35 @@ func requireSequence(
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PureYAML.Model.Value {
|
||||||
|
var mapping: PureYAML.Model.Mapping? {
|
||||||
|
guard case let .mapping(mapping) = self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PureYAML.Model.Mapping {
|
||||||
|
func mapping(_ key: String) -> PureYAML.Model.Mapping? {
|
||||||
|
self[key]?.mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
func sequence(_ keys: String...) -> [PureYAML.Model.Value]? {
|
||||||
|
var value: PureYAML.Model.Value?
|
||||||
|
for key in keys {
|
||||||
|
let mapping: PureYAML.Model.Mapping? = if let currentValue = value {
|
||||||
|
currentValue.mapping
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
value = mapping?[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return requireSequence(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func expectParseError(
|
func expectParseError(
|
||||||
_ yaml: String,
|
_ yaml: String,
|
||||||
_ expected: PureYAML.Parsing.ParseError,
|
_ expected: PureYAML.Parsing.ParseError,
|
||||||
|
|||||||
134
Tests/ValidationFailureFixtureTests.swift
Normal file
134
Tests/ValidationFailureFixtureTests.swift
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import Foundation
|
||||||
|
@testable import PureYAML
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("Validation Failure Fixtures")
|
||||||
|
struct ValidationFailureFixtureTests {
|
||||||
|
@Test("Deliberately failing YAML fixtures report exact validation descriptions", arguments: validationFailureFixtures)
|
||||||
|
func test_deliberatelyFailingFixturesReportExactValidationDescriptions(testCase: ValidationFailureFixture) throws {
|
||||||
|
let value = try testCase.loadValue()
|
||||||
|
let result = testCase.validator.collect(value)
|
||||||
|
|
||||||
|
#expect(result.issues == testCase.expectedIssues)
|
||||||
|
#expect(result.issues.map(\.description) == testCase.expectedDescriptions)
|
||||||
|
#expect(result.descriptionLines == testCase.expectedDescriptions)
|
||||||
|
#expect(!result.isValid == testCase.expectedIssues.contains { $0.severity == .error })
|
||||||
|
|
||||||
|
expectValidationError(value, using: testCase.validator) { collection in
|
||||||
|
#expect(collection.issues == testCase.expectedIssues)
|
||||||
|
#expect(collection.description == testCase.expectedDescriptions.joined(separator: "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Warning-only failure fixture returns warnings in non-strict mode")
|
||||||
|
func test_warningOnlyFailureFixtureReturnsWarningsInNonStrictMode() throws {
|
||||||
|
let testCase = try #require(validationFailureFixtures.first { $0.name == "warning-only validation output" })
|
||||||
|
let value = try testCase.loadValue()
|
||||||
|
let warnings = try testCase.validator.validate(value, strict: false)
|
||||||
|
|
||||||
|
#expect(warnings == testCase.expectedIssues)
|
||||||
|
#expect(warnings.map(\.description) == testCase.expectedDescriptions)
|
||||||
|
#expect(warnings.allSatisfy { $0.severity == .warning })
|
||||||
|
#expect(!warnings.contains(.init(
|
||||||
|
severity: .warning,
|
||||||
|
reason: "Forbidden key 'deprecated' is present",
|
||||||
|
path: .init([.key("title")]),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let validationFailureFixtures: [ValidationFailureFixture] = [
|
||||||
|
.init(
|
||||||
|
name: "duplicate key validation output",
|
||||||
|
fileName: "duplicate-keys",
|
||||||
|
validator: .init(),
|
||||||
|
expectedIssues: [
|
||||||
|
.init(
|
||||||
|
severity: .error,
|
||||||
|
reason: "Duplicate mapping key 'title'",
|
||||||
|
path: .init([.key("title")]),
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
severity: .error,
|
||||||
|
reason: "Duplicate mapping key '/users'",
|
||||||
|
path: .init([.key("paths"), .key("/users")]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
expectedDescriptions: [
|
||||||
|
"error: Duplicate mapping key 'title' at $.title",
|
||||||
|
"error: Duplicate mapping key '/users' at $.paths[\"/users\"]",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
name: "required and forbidden validation output",
|
||||||
|
fileName: "missing-required-and-forbidden",
|
||||||
|
validator: .blank
|
||||||
|
.validating(requiredRootStringKeyRule("title"))
|
||||||
|
.validating(forbiddenMappingKeyRule("draft", severity: .error)),
|
||||||
|
expectedIssues: [
|
||||||
|
.init(
|
||||||
|
severity: .error,
|
||||||
|
reason: "Required string key 'title' is missing",
|
||||||
|
path: .init([.key("title")]),
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
severity: .error,
|
||||||
|
reason: "Forbidden key 'draft' is present",
|
||||||
|
path: .init([.key("metadata"), .key("draft")]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
expectedDescriptions: [
|
||||||
|
"error: Required string key 'title' is missing at $.title",
|
||||||
|
"error: Forbidden key 'draft' is present at $.metadata.draft",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
name: "warning-only validation output",
|
||||||
|
fileName: "warnings",
|
||||||
|
validator: .blank.validating(forbiddenMappingKeyRule("deprecated", severity: .warning)),
|
||||||
|
expectedIssues: [
|
||||||
|
.init(
|
||||||
|
severity: .warning,
|
||||||
|
reason: "Forbidden key 'deprecated' is present",
|
||||||
|
path: .init([.key("deprecated")]),
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
severity: .warning,
|
||||||
|
reason: "Forbidden key 'deprecated' is present",
|
||||||
|
path: .init([.key("nested"), .key("deprecated")]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
expectedDescriptions: [
|
||||||
|
"warning: Forbidden key 'deprecated' is present at $.deprecated",
|
||||||
|
"warning: Forbidden key 'deprecated' is present at $.nested.deprecated",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
struct ValidationFailureFixture: CustomStringConvertible {
|
||||||
|
var name: String
|
||||||
|
var fileName: String
|
||||||
|
var validator: PureYAML.Validation.Validator
|
||||||
|
var expectedIssues: [PureYAML.Validation.Issue]
|
||||||
|
var expectedDescriptions: [String]
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadValue() throws -> PureYAML.Model.Value {
|
||||||
|
let url = try #require(Bundle.module.url(
|
||||||
|
forResource: fileName,
|
||||||
|
withExtension: "yaml",
|
||||||
|
subdirectory: "Fixtures/validation-failures",
|
||||||
|
))
|
||||||
|
let yaml = try String(contentsOf: url, encoding: .utf8)
|
||||||
|
return try PureYAML.parse(yaml)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension PureYAML.Validation.Result {
|
||||||
|
var descriptionLines: [String] {
|
||||||
|
issues.map(\.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,24 @@ validation rule must make these parts explicit:
|
|||||||
- **Default status**: state whether the rule belongs in
|
- **Default status**: state whether the rule belongs in
|
||||||
`Validation.Validator.defaultRules` or must be opt-in.
|
`Validation.Validator.defaultRules` or must be opt-in.
|
||||||
|
|
||||||
|
## Validation Document Rule
|
||||||
|
|
||||||
|
User-facing tools must offer a validation document/report instead of exposing
|
||||||
|
only a raw crash, trap, stack trace, or unstructured thrown error. Library APIs
|
||||||
|
may throw typed errors, but CLIs, batch tools, and corpus runners must be able
|
||||||
|
to write or print a readable validation document that includes:
|
||||||
|
|
||||||
|
- input URL or file path;
|
||||||
|
- failure category;
|
||||||
|
- line and column when available;
|
||||||
|
- failing reference or path when applicable;
|
||||||
|
- original error text;
|
||||||
|
- whether processing continued or stopped.
|
||||||
|
|
||||||
|
Input-level failures must be data. A tool may return a non-zero exit code after
|
||||||
|
reporting failures, but it must not silently drop them or terminate before the
|
||||||
|
validation document can be produced.
|
||||||
|
|
||||||
## Test Contract
|
## Test Contract
|
||||||
|
|
||||||
Every validator change must include Swift Testing coverage for:
|
Every validator change must include Swift Testing coverage for:
|
||||||
|
|||||||
@ -66,12 +66,18 @@ source_singletons=$(
|
|||||||
)
|
)
|
||||||
report_matches "Sources must not introduce singleton or service-locator access patterns" "$source_singletons"
|
report_matches "Sources must not introduce singleton or service-locator access patterns" "$source_singletons"
|
||||||
|
|
||||||
|
source_traps=$(
|
||||||
|
grep -RInE '\bfatalError[[:space:]]*\(|\bpreconditionFailure[[:space:]]*\(|\bassertionFailure[[:space:]]*\(|\btry!|\bas!|[A-Za-z0-9_)\]}][[:space:]]*![[:space:]]*([).,\];:]|$)' Sources --include '*.swift' 2>/dev/null || true
|
||||||
|
)
|
||||||
|
report_matches "Sources must not use traps, forced casts, try!, or force unwraps" "$source_traps"
|
||||||
|
|
||||||
test_imports=$(
|
test_imports=$(
|
||||||
grep -RInE '^[[:space:]]*(@testable[[:space:]]+)?import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' Tests --include '*.swift' 2>/dev/null \
|
grep -RInE '^[[:space:]]*(@testable[[:space:]]+)?import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' Tests --include '*.swift' 2>/dev/null \
|
||||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*@testable[[:space:]]+import[[:space:]]+PureYAML[[:space:]]*$' \
|
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*@testable[[:space:]]+import[[:space:]]+PureYAML[[:space:]]*$' \
|
||||||
|
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Foundation[[:space:]]*$' \
|
||||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Testing[[:space:]]*$' || true
|
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Testing[[:space:]]*$' || true
|
||||||
)
|
)
|
||||||
report_matches "Tests may import only Testing and @testable PureYAML" "$test_imports"
|
report_matches "Tests may import only Foundation, Testing, and @testable PureYAML" "$test_imports"
|
||||||
|
|
||||||
test_xctest=$(
|
test_xctest=$(
|
||||||
grep -RInE '\bXCTest(Case|Expectation)?\b|\bXCTAssert[A-Za-z]*\b|^[[:space:]]*import[[:space:]]+XCTest[[:space:]]*$' Tests --include '*.swift' 2>/dev/null || true
|
grep -RInE '\bXCTest(Case|Expectation)?\b|\bXCTAssert[A-Za-z]*\b|^[[:space:]]*import[[:space:]]+XCTest[[:space:]]*$' Tests --include '*.swift' 2>/dev/null || true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user