diff --git a/Tests/NumericCodingLadderTests.swift b/Tests/NumericCodingLadderTests.swift new file mode 100644 index 0000000..267375b --- /dev/null +++ b/Tests/NumericCodingLadderTests.swift @@ -0,0 +1,82 @@ +@testable import PureYAML +import Testing + +@Suite("Numeric Coding Ladder") +struct NumericCodingLadderTests { + @Test("Decodes every integer width from an int value") + func test_decodesFullIntegerWidthLadder() throws { + #expect(try PureYAML.decode(Int8.self, from: .int(100)) == 100) + #expect(try PureYAML.decode(Int16.self, from: .int(30000)) == 30000) + #expect(try PureYAML.decode(Int32.self, from: .int(2_000_000_000)) == 2_000_000_000) + #expect(try PureYAML.decode(Int64.self, from: .int(9_000_000_000)) == 9_000_000_000) + #expect(try PureYAML.decode(UInt.self, from: .int(42)) == 42) + #expect(try PureYAML.decode(UInt8.self, from: .int(200)) == 200) + #expect(try PureYAML.decode(UInt16.self, from: .int(60000)) == 60000) + #expect(try PureYAML.decode(UInt32.self, from: .int(4_000_000_000)) == 4_000_000_000) + #expect(try PureYAML.decode(UInt64.self, from: .int(42)) == 42) + } + + @Test("Reports the exact type when an int value is out of range per width") + func test_outOfRangeDecodeReportsExactTypePerWidth() { + expectDecodeOutOfRange(Int8.self, "Int8", .int(1000)) + expectDecodeOutOfRange(Int16.self, "Int16", .int(40000)) + expectDecodeOutOfRange(Int32.self, "Int32", .int(5_000_000_000)) + expectDecodeOutOfRange(UInt8.self, "UInt8", .int(300)) + expectDecodeOutOfRange(UInt16.self, "UInt16", .int(70000)) + expectDecodeOutOfRange(UInt32.self, "UInt32", .int(5_000_000_000)) + // Negative into an unsigned width is also out of range. + expectDecodeOutOfRange(UInt8.self, "UInt8", .int(-1)) + expectDecodeOutOfRange(UInt.self, "UInt", .int(-1)) + expectDecodeOutOfRange(UInt64.self, "UInt64", .int(-1)) + } + + @Test("Round-trips Float through the double representation") + func test_floatEncodeDecodeRoundTrip() throws { + #expect(try PureYAML.encode(Float(1.5)) == .double(1.5)) + #expect(try PureYAML.decode(Float.self, from: .double(1.5)) == 1.5) + #expect(try PureYAML.decode(Float.self, from: .int(2)) == 2.0) + } + + @Test("Double decoding rejects non-numeric kinds") + func test_doubleDecodeRejectsNonNumericKinds() { + expectDecodingError(.typeMismatch(expected: "Double", actual: "bool", path: .root)) { + try PureYAML.decode(Double.self, from: .bool(true)) + } + expectDecodingError(.typeMismatch(expected: "Double", actual: "string", path: .root)) { + try PureYAML.decode(Double.self, from: .string("x")) + } + expectDecodingError(.typeMismatch(expected: "Double", actual: "null", path: .root)) { + try PureYAML.decode(Double.self, from: .null) + } + } +} + +private func expectDecodeOutOfRange( + _ type: (some Decodable).Type, + _ typeName: String, + _ value: PureYAML.Model.Value, + sourceLocation: SourceLocation = #_sourceLocation, +) { + expectDecodingError( + .integerOutOfRange(type: typeName, path: .root), + sourceLocation: sourceLocation, + ) { + try PureYAML.decode(type, from: value) + } +} + +private func expectDecodingError( + _ expected: PureYAML.Decoding.Error, + sourceLocation: SourceLocation = #_sourceLocation, + operation: () throws -> some Any, +) { + do { + _ = try operation() + Issue.record("expected decoding error \(expected)", sourceLocation: sourceLocation) + } catch let error as PureYAML.Decoding.Error { + #expect(error == expected, sourceLocation: sourceLocation) + #expect(error.description == expected.description, sourceLocation: sourceLocation) + } catch { + Issue.record("unexpected error \(error)", sourceLocation: sourceLocation) + } +} diff --git a/Tests/ParsingErrorPathTests.swift b/Tests/ParsingErrorPathTests.swift new file mode 100644 index 0000000..b500033 --- /dev/null +++ b/Tests/ParsingErrorPathTests.swift @@ -0,0 +1,37 @@ +@testable import PureYAML +import Testing + +@Suite("Parsing Error Paths") +struct ParsingErrorPathTests { + @Test("A node property with no following node throws expectedNode") + func test_danglingNodePropertyThrowsExpectedNode() { + expectParseError("&anchor\n", .expectedNode(line: 1, column: 1)) + expectParseError("root:\n key: &a\nother: 1\n", .expectedNode(line: 2, column: 8)) + } + + @Test("An unterminated flow mapping throws at stream end") + func test_unterminatedFlowMappingThrows() { + expectParseError("x: {a: b", .unexpectedToken( + expected: "flow entry or flow mapping end", + actual: "streamEnd", + line: 1, + column: 9, + )) + } + + @Test("Block scalar content less indented than the explicit indicator throws") + func test_explicitBlockScalarIndentationUnderIndentThrows() { + expectParseError("k: |2\n a\n", .unexpectedIndentation(line: 2)) + } + + @Test("Diagnostic validation reports an invalid merge value via the buffered composer") + func test_diagnosticValidationRejectsInvalidMergeValue() { + let report = PureYAML.diagnosticValidationReport("a: &x 1\nb:\n <<: *x\n", file: "merge.yaml") + #expect(!report.isValid) + #expect(report.diagnostics.count == 1) + let diagnostic = report.diagnostics.first + #expect(diagnostic?.kind == .parse) + #expect(diagnostic?.code == "invalidMergeValue") + #expect(diagnostic?.line == 3) + } +} diff --git a/Tests/UnkeyedContainerEdgeTests.swift b/Tests/UnkeyedContainerEdgeTests.swift new file mode 100644 index 0000000..512d2d2 --- /dev/null +++ b/Tests/UnkeyedContainerEdgeTests.swift @@ -0,0 +1,23 @@ +@testable import PureYAML +import Testing + +@Suite("Unkeyed Container Edges") +struct UnkeyedContainerEdgeTests { + @Test("decodeNil returns false for present values and still decodes them") + func test_unkeyedDecodeNilFalseBranchDecodesPresentValues() throws { + let decoded = try PureYAML.decode([Int?].self, from: .sequence([.int(1), .null, .int(2)])) + #expect(decoded == [1, nil, 2]) + } + + @Test("A nested unkeyed container type mismatch reports the index path") + func test_unkeyedNestedContainerMismatchReportsIndexPath() { + do { + _ = try PureYAML.decode([[Int]].self, from: .sequence([.int(1)])) + Issue.record("expected a decoding error") + } catch let error as PureYAML.Decoding.Error { + #expect(error.description == "Expected sequence at $[0], found int") + } catch { + Issue.record("unexpected error \(error)") + } + } +} diff --git a/Tests/ValueHashableTests.swift b/Tests/ValueHashableTests.swift new file mode 100644 index 0000000..e5861be --- /dev/null +++ b/Tests/ValueHashableTests.swift @@ -0,0 +1,44 @@ +@testable import PureYAML +import Testing + +@Suite("Value Hashing") +struct ValueHashableTests { + typealias Value = PureYAML.Model.Value + + @Test("NaN double values compare equal to themselves") + func test_doubleNaNValuesAreEqual() { + #expect(Value.double(.nan) == Value.double(.nan)) + #expect(Value.double(.nan) != Value.double(1.0)) + #expect(Value.double(1.0) == Value.double(1.0)) + } + + @Test("Value hashing is consistent with equality, including NaN") + func test_valueHashingIsConsistentWithEqualityIncludingNaN() { + // Two equal NaN values must collapse to one Set element, which only + // holds if hash(into:) agrees with the custom == for NaN. + let set: Set = [.double(.nan), .double(.nan)] + #expect(set.count == 1) + + // Distinct values stay distinct as dictionary keys. + let table: [Value: Int] = [ + .double(.nan): 1, + .int(1): 2, + .string("1"): 3, + .bool(true): 4, + .null: 5, + ] + #expect(table.count == 5) + #expect(table[.double(.nan)] == 1) + #expect(table[.int(1)] == 2) + } + + @Test("Equal values across cases hash and compare distinctly") + func test_distinctCasesAreNotEqual() { + #expect(Value.int(1) != Value.string("1")) + #expect(Value.int(1) != Value.double(1.0)) + #expect(Value.bool(true) != Value.int(1)) + #expect(Value.null != Value.string("null")) + let set: Set = [.int(1), .string("1"), .double(1.0), .bool(true), .null] + #expect(set.count == 5) + } +}