PureYAML/Tests/TaggedBuiltinScalarTests.swift
Mihaela Mihaljevic e1c08260c4 feat(tagged): add typed timestamp and binary scalar accessors
Completes issue #11. Tagged.Scalar.timestampValue parses a !!timestamp
scalar under the YAML 1.1 grammar into the pure-Swift Tagged.Timestamp
(calendar components, timezone offset, secondsSinceEpoch via the
days-from-civil closed form; epoch test expectations measured against
Python's independent implementation). Tagged.Scalar.binaryValue base64
decodes a !!binary scalar into bytes, accepting unpadded and block
scalar wrapped content and rejecting corrupt input whole.

Yams hands back Foundation Date/Data; this package is mechanically
Foundation-free, so the bridges are caller-owned one-liners:
Date(timeIntervalSince1970:) and Data(_:). !!set and !!omap stay
skipped since Model.Mapping already preserves order and duplicates.

Closes #11
2026-07-12 14:20:23 +02:00

109 lines
4.9 KiB
Swift

@testable import PureYAML
import Testing
/// The standard-tag typed accessors (issue #11): `!!timestamp` scalars parse
/// into `Tagged.Timestamp` and `!!binary` scalars base64-decode into bytes,
/// with no caller-registered constructor. Expected epoch values are
/// measured against Python's `datetime.timestamp()`, an independent
/// implementation, not derived from the code under test.
@Suite("Tagged built-in timestamp and binary accessors")
struct TaggedBuiltinScalarTests {
func scalar(_ source: String) throws -> PureYAML.Tagged.Scalar {
let node = try PureYAML.parseTagged(source)
guard case let .mapping(mapping) = node, case let .scalar(scalar)? = mapping["v"] else {
throw PureYAML.Parsing.ParseError.unexpectedEvent(
expected: "scalar mapping value",
actual: "other",
line: 1,
column: 1,
)
}
return scalar
}
@Test("The canonical timestamp form parses with UTC and fraction")
func test_canonicalTimestamp() throws {
let stamp = try #require(scalar("v: !!timestamp 2001-12-15T02:59:43.1Z\n").timestampValue)
#expect(stamp.year == 2001)
#expect(stamp.month == 12)
#expect(stamp.day == 15)
#expect(stamp.hour == 2)
#expect(stamp.offsetSeconds == 0)
#expect(stamp.secondsSinceEpoch == 1_008_385_183.1)
}
@Test("A space-separated timestamp with a bare negative offset parses")
func test_spaceSeparatedTimestampWithOffset() throws {
let stamp = try #require(scalar("v: !!timestamp 2001-12-14 21:59:43.10 -5\n").timestampValue)
#expect(stamp.offsetSeconds == -5 * 3600)
#expect(stamp.secondsSinceEpoch == 1_008_385_183.1)
}
@Test("A date-only timestamp parses as midnight UTC")
func test_dateOnlyTimestamp() throws {
let stamp = try #require(scalar("v: !!timestamp 2002-12-14\n").timestampValue)
#expect(stamp.secondsSinceEpoch == 1_039_824_000.0)
}
@Test("A pre-epoch timestamp yields a negative epoch offset")
func test_preEpochTimestamp() throws {
let stamp = try #require(scalar("v: !!timestamp 1969-07-20T20:17:00Z\n").timestampValue)
#expect(stamp.secondsSinceEpoch == -14_182_980.0)
}
@Test("Calendar-invalid and malformed timestamps return nil")
func test_invalidTimestamps() throws {
#expect(try scalar("v: !!timestamp 2001-02-30\n").timestampValue == nil)
#expect(try scalar("v: !!timestamp 2001-13-01\n").timestampValue == nil)
#expect(try scalar("v: !!timestamp 2001-12-15T25:00:00Z\n").timestampValue == nil)
#expect(try scalar("v: !!timestamp not-a-date\n").timestampValue == nil)
#expect(try scalar("v: !!timestamp 2001-12-15T02:59:43.1Z extra\n").timestampValue == nil)
}
@Test("An untagged date-like scalar does not auto-decode")
func test_untaggedScalarStaysUntyped() throws {
#expect(try scalar("v: 2002-12-14\n").timestampValue == nil)
#expect(try scalar("v: 2002-12-14\n").binaryValue == nil)
}
@Test("The specification's binary example decodes to the GIF header bytes")
func test_binaryDecodes() throws {
let bytes = try #require(scalar("v: !!binary R0lGODlhDAAMAIQAAP8AAA==\n").binaryValue)
#expect(Array(bytes.prefix(8)) == [71, 73, 70, 56, 57, 97, 12, 0])
}
@Test("Binary content spread over a block scalar decodes")
func test_binaryBlockScalar() throws {
let source = "v: !!binary |\n R0lGODlh\n DAAMAIQAAP8AAA==\n"
let bytes = try #require(scalar(source).binaryValue)
#expect(Array(bytes.prefix(8)) == [71, 73, 70, 56, 57, 97, 12, 0])
}
@Test("Unpadded base64 decodes and corrupt base64 returns nil")
func test_binaryEdgeForms() throws {
#expect(try scalar("v: !!binary R0lGODlhDAAMAIQAAP8AAA\n").binaryValue != nil)
#expect(try scalar("v: !!binary AAAA\n").binaryValue == [0, 0, 0])
#expect(try scalar("v: !!binary not@base64!\n").binaryValue == nil)
#expect(try scalar("v: !!binary A\n").binaryValue == nil)
#expect(try scalar("v: !!binary AB==CD\n").binaryValue == nil)
}
@Test("Timestamp and binary scalars round-trip through dump with tags intact")
func test_taggedScalarsRoundTrip() throws {
let source = "when: !!timestamp 2001-12-15T02:59:43.1Z\nblob: !!binary R0lGODlhDAAMAIQAAP8AAA==\n"
let node = try PureYAML.parseTagged(source)
let reparsed = try PureYAML.parseTagged(PureYAML.dump(node))
#expect(reparsed == node)
guard case let .mapping(mapping) = reparsed else {
Issue.record("expected mapping")
return
}
guard case let .scalar(when)? = mapping["when"], case let .scalar(blob)? = mapping["blob"] else {
Issue.record("expected scalars")
return
}
#expect(when.timestampValue?.secondsSinceEpoch == 1_008_385_183.1)
#expect(blob.binaryValue?.prefix(4) == [71, 73, 70, 56])
}
}