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
109 lines
4.9 KiB
Swift
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])
|
|
}
|
|
}
|