From dcb9a40b18317eb1b1061700f30675ccebcb5608 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Fri, 26 Jun 2026 08:27:00 +0200 Subject: [PATCH] test(parsing): cover ByteEncoder output-encoding paths (#320) ByteEncoder (the public output encoder) was reached only through the serializer's UTF-8 default, leaving its transformation-format, single- byte, BOM, and naming paths untested. Adds direct assertions of the spec-defined bytes: UTF-8/16/32 (incl. an astral surrogate pair), ISO-8859-1 with a decimal character-reference fallback for an unrepresentable scalar, byte-order marks, supports, and canonical names. --- Tests/ByteEncoderTests.swift | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Tests/ByteEncoderTests.swift diff --git a/Tests/ByteEncoderTests.swift b/Tests/ByteEncoderTests.swift new file mode 100644 index 0000000..7868b53 --- /dev/null +++ b/Tests/ByteEncoderTests.swift @@ -0,0 +1,66 @@ +import Testing +@testable import PureXML + +/// `PureXML.Parsing.ByteEncoder` is the public output encoder (the inverse of +/// `ByteDecoder`). It is reached through the serializer, whose tests only exercise +/// the UTF-8 default, so the transformation-format, single-byte, BOM, and naming +/// paths were untested. These assert the spec-defined byte output directly. +@Suite("ByteEncoder output encoding") +struct ByteEncoderTests { + private typealias Encoder = PureXML.Parsing.ByteEncoder + private typealias Encoding = PureXML.Parsing.InputEncoding + + @Test("UTF-8 encodes a BMP and a multi-byte scalar") + func test_utf8() { + // "A" U+0041, "€" U+20AC -> E2 82 AC. + #expect(Encoder.encode("A€", as: .utf8) == [0x41, 0xE2, 0x82, 0xAC]) + } + + @Test("UTF-16 big- and little-endian, including an astral surrogate pair") + func test_utf16() { + #expect(Encoder.encode("A€", as: .utf16BigEndian) == [0x00, 0x41, 0x20, 0xAC]) + #expect(Encoder.encode("A€", as: .utf16LittleEndian) == [0x41, 0x00, 0xAC, 0x20]) + // "𐀀" U+10000 -> surrogate pair D800 DC00. + #expect(Encoder.encode("𐀀", as: .utf16BigEndian) == [0xD8, 0x00, 0xDC, 0x00]) + } + + @Test("UTF-32 big- and little-endian") + func test_utf32() { + #expect(Encoder.encode("A", as: .utf32BigEndian) == [0x00, 0x00, 0x00, 0x41]) + #expect(Encoder.encode("A", as: .utf32LittleEndian) == [0x41, 0x00, 0x00, 0x00]) + #expect(Encoder.encode("𐀀", as: .utf32BigEndian) == [0x00, 0x01, 0x00, 0x00]) + } + + @Test("a single-byte encoding maps representable scalars and escapes the rest") + func test_singleByteWithCharacterReference() { + // "é" U+00E9 is ISO-8859-1 byte 0xE9; "€" U+20AC is not representable and + // becomes the ASCII bytes of a decimal character reference. + #expect(Encoder.encode("é", as: .latin1) == [0xE9]) + #expect(Encoder.encode("€", as: .latin1) == Array("€".utf8)) + } + + @Test("byte-order marks are emitted only for the UTF-16/32 forms") + func test_byteOrderMark() { + #expect(Encoder.byteOrderMark(.utf16BigEndian) == [0xFE, 0xFF]) + #expect(Encoder.byteOrderMark(.utf16LittleEndian) == [0xFF, 0xFE]) + #expect(Encoder.byteOrderMark(.utf32BigEndian) == [0x00, 0x00, 0xFE, 0xFF]) + #expect(Encoder.byteOrderMark(.utf32LittleEndian) == [0xFF, 0xFE, 0x00, 0x00]) + #expect(Encoder.byteOrderMark(.utf8).isEmpty) + #expect(Encoder.byteOrderMark(.latin1).isEmpty) + } + + @Test("supports covers the transformation formats and a single-byte family") + func test_supports() { + #expect(Encoder.supports(.utf8)) + #expect(Encoder.supports(.utf16BigEndian)) + #expect(Encoder.supports(.latin1)) + } + + @Test("canonical declaration names match the registered encoding labels") + func test_canonicalName() { + #expect(Encoder.canonicalName(.utf8) == "UTF-8") + #expect(Encoder.canonicalName(.utf16BigEndian) == "UTF-16") + #expect(Encoder.canonicalName(.latin1) == "ISO-8859-1") + #expect(Encoder.canonicalName(.shiftJIS) == "Shift_JIS") + } +}