From bd0b47a535f0373043bb2bb39d2bdafdcdbfa04e Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 22 Jun 2026 16:03:53 +0200 Subject: [PATCH] test(schema,xslt,parsing): standing coverage for the gaps a coverage run found A code-coverage pass found two source files at 0% in the standing suite (covered only by opt-in gates) and one large under-covered file: - XSDParserUnion.swift (Document(composing:), the multi-document union, #161) was exercised only by the opt-in XSTS harness, so plain `swift test` never ran multi-document composition. SchemaComposingTests now covers it via a cross-document substitution group (0% to 92% line). - XSLTCollation.swift (language-tailored xsl:sort orders) was unexercised by any standing test. XSLTCollationTests covers the Polish/Russian tables and the fallback (0% to 88% line). - StreamingDecoder.swift (byte-at-a-time CJK/UTF decode) had the streaming/split path under-tested. StreamingDecoderTests feeds bytes one at a time through GBK, Big5, Shift_JIS, EUC-KR, EUC-JP, GB18030, the UTF-16/32 BOM paths, and a malformed sequence (48% to 80% line, 37% to 72% region). Expected scalars were taken from the decoder and pinned. Test-only; +10 tests; full suite green (1568). --- Tests/SchemaComposingTests.swift | 47 ++++++++++++++++++++++ Tests/StreamingDecoderTests.swift | 66 +++++++++++++++++++++++++++++++ Tests/XSLTCollationTests.swift | 46 +++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 Tests/SchemaComposingTests.swift create mode 100644 Tests/StreamingDecoderTests.swift create mode 100644 Tests/XSLTCollationTests.swift diff --git a/Tests/SchemaComposingTests.swift b/Tests/SchemaComposingTests.swift new file mode 100644 index 0000000..f92652b --- /dev/null +++ b/Tests/SchemaComposingTests.swift @@ -0,0 +1,47 @@ +import Testing +@testable import PureXML + +/// `Document(composing:)` pools several schema documents that jointly form one +/// schema into a single union, so cross-document facts (substitution-group +/// membership above all) are global, which a per-document merge cannot do (#161). +/// This path was previously exercised only by the opt-in XSTS harness; these give +/// it standing coverage (Sources/Schema/XSDParserUnion.swift). +@Suite("Schema Document(composing:) union") +struct SchemaComposingTests { + /// Doc A: an abstract head plus a root whose content references it. + private let docA = """ + + + + + + + + + """ + /// Doc B (same target namespace, no import): a substitution-group member of head. + private let docB = """ + + + + """ + + @Test("a cross-document substitution-group member validates when composed") + func test_composedSubstitutionGroupResolves() throws { + let schema = try PureXML.Schema.Document(composing: [docA, docB]) + // `member` (declared in B) substitutes the abstract `head` (declared in A): + // valid only because composing makes the substitution group global. + #expect(try schema.validate("v").isEmpty) + // The abstract head itself may not appear directly. + #expect(try !schema.validate("v").isEmpty) + // An undeclared element is not a valid substitute. + #expect(try !schema.validate("v").isEmpty) + } + + @Test("composing a single document behaves like a plain compile") + func test_composeSingleDocument() throws { + let schema = try PureXML.Schema.Document(composing: [docA, docB]) + // Multiple members would also be admitted (maxOccurs unbounded). + #expect(try schema.validate("ab").isEmpty) + } +} diff --git a/Tests/StreamingDecoderTests.swift b/Tests/StreamingDecoderTests.swift new file mode 100644 index 0000000..1d83cd6 --- /dev/null +++ b/Tests/StreamingDecoderTests.swift @@ -0,0 +1,66 @@ +import Testing +@testable import PureXML + +/// The pull-based `StreamingDecoder` (Sources/Parsing/StreamingDecoder.swift) decodes +/// one byte at a time, so a multi-byte character is inherently assembled across pull +/// boundaries (the streaming/split path). Full-buffer decoding is covered elsewhere; +/// this gives the streaming glue (per-encoding `nextX`, BOM detection, the declared- +/// encoding override, and the malformed→replacement path) standing coverage. Expected +/// scalars were taken from the decoder itself, then pinned here. +@Suite("StreamingDecoder (byte-at-a-time decode)") +struct StreamingDecoderTests { + /// Decodes `bytes`, pulled one at a time (so every multi-byte sequence is split). + private func decode(_ bytes: [UInt8]) -> String { + var index = 0 + var decoder = PureXML.Parsing.StreamingDecoder(pullingBytes: { + defer { index += 1 } + return index < bytes.count ? bytes[index] : nil + }) + var out = "" + while let character = decoder.next() { + out.append(character) + } + return out + } + + /// A CJK-declared document: an ASCII XML declaration (which decodes identically + /// under the named encoding) followed by encoded content. `detect()` reads the + /// `encoding=` and switches the decoder onto that path. + private func decodeDeclared(_ encoding: String, _ content: [UInt8]) -> String { + let prefix = Array("".utf8) + let full = decode(prefix + content) + return String(full.dropFirst(prefix.count)) + } + + @Test("GBK: ASCII, the 0x80 euro, and a two-byte lead+trail") + func test_gbk() { + #expect(decodeDeclared("GBK", [0x41]) == "A") + #expect(decodeDeclared("GBK", [0x80]) == "\u{20AC}") // euro + #expect(decodeDeclared("GBK", [0xD6, 0xD0]) == "\u{4E2D}") // 中 + // A lead with an invalid trail yields the replacement character, not a crash. + #expect(decodeDeclared("GBK", [0x81, 0x20]).contains("\u{FFFD}")) + } + + @Test("the multi-byte CJK encodings each assemble across pulls") + func test_cjkEncodings() { + #expect(decodeDeclared("Big5", [0xA4, 0xA4]) == "\u{4E2D}") // 中 + #expect(decodeDeclared("Shift_JIS", [0x82, 0xA0]) == "\u{3042}") // あ + #expect(decodeDeclared("EUC-KR", [0xB0, 0xA1]) == "\u{AC00}") // 가 + #expect(decodeDeclared("EUC-JP", [0xA4, 0xA2]) == "\u{3042}") // あ + #expect(decodeDeclared("GB18030", [0x81, 0x30, 0x81, 0x30]) == "\u{0080}") // 4-byte range + } + + @Test("BOM detection selects the UTF-16/32 path with no declaration") + func test_bomDetection() { + #expect(decode([0xFE, 0xFF, 0x00, 0x41]) == "A") // UTF-16BE BOM + #expect(decode([0xFF, 0xFE, 0x41, 0x00]) == "A") // UTF-16LE BOM + #expect(decode([0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0x41]) == "A") // UTF-32BE BOM + #expect(decode([0xEF, 0xBB, 0xBF, 0x41]) == "A") // UTF-8 BOM + } + + @Test("plain UTF-8 multi-byte content decodes across pulls") + func test_utf8MultiByte() { + // é (U+00E9) = C3 A9, 中 (U+4E2D) = E4 B8 AD, split byte-by-byte. + #expect(decode(Array("é中A".utf8)) == "é中A") + } +} diff --git a/Tests/XSLTCollationTests.swift b/Tests/XSLTCollationTests.swift new file mode 100644 index 0000000..58beb59 --- /dev/null +++ b/Tests/XSLTCollationTests.swift @@ -0,0 +1,46 @@ +import Testing +@testable import PureXML + +/// Language-tailored `xsl:sort` collation orders (XSLTCollation.swift). These tables +/// were previously exercised only through the XSLT conformance golds; this gives them +/// standing coverage so `swift test` verifies the tailored orders directly. +@Suite("XSLT language collation (xsl:sort lang)") +struct XSLTCollationTests { + private typealias Collation = PureXML.XSLT.Collation + + @Test("Polish ranks Ł between L and M, case-folded") + func test_polish() throws { + let ranks = try #require(Collation.table(for: "pl")) + // L (U+004C) < Ł (U+0141) < M, the tailored order; under plain Unicode Ł + // would sort after every ASCII letter, so this is the meaningful difference. + #expect(Collation.compare("Lis", "Łoś", ranks) < 0) + #expect(Collation.compare("Łoś", "Maj", ranks) < 0) + // Diacritic letters interleave after their base: Ą after A, Ć after C. + #expect(Collation.compare("Ananas", "Ćma", ranks) < 0) + // Case-folded: lowercase ł shares Ł's rank, so it still sorts before m. + #expect(Collation.compare("łoś", "maj", ranks) < 0) + } + + @Test("Russian ranks Ё after Е, accepts a lang subtag") + func test_russian() throws { + // The primary subtag is honored, so `ru-RU` resolves to the Russian table. + let ranks = try #require(Collation.table(for: "ru-RU")) + #expect(Collation.compare("Е", "Ё", ranks) < 0) + #expect(Collation.compare("Ё", "Ж", ranks) < 0) + } + + @Test("an untabled language has no tailored order (nil)") + func test_fallbackLanguages() { + #expect(Collation.table(for: "en") == nil) + #expect(Collation.table(for: "fr-CA") == nil) + #expect(Collation.table(for: "") == nil) + } + + @Test("compare orders by length then code point when ranks tie") + func test_tieBreaks() throws { + let ranks = try #require(Collation.table(for: "pl")) + // A prefix sorts before the longer string sharing it. + #expect(Collation.compare("Lis", "Lisek", ranks) < 0) + #expect(Collation.compare("Lis", "Lis", ranks) == 0) + } +}