Tests/OrgSwiftTests/ConformanceTests.swift
117 lines · 5563 bytes
1import Foundation
2import Testing
3@testable import OrgSwift
4
5/// Locate the org-conformance corpus: env var first, then the sibling checkout that sits
6/// next to this package under a shared parent (…/org-swift and …/org-conformance).
7private func corpusCasesDir() -> URL? {
8 if let env = ProcessInfo.processInfo.environment["ORG_CONFORMANCE_DIR"] {
9 let cases = URL(fileURLWithPath: env).appendingPathComponent("cases")
10 if FileManager.default.fileExists(atPath: cases.path) { return cases }
11 }
12 // #filePath = …/org-swift/Tests/OrgSwiftTests/ConformanceTests.swift
13 let pkgRoot = URL(fileURLWithPath: #filePath)
14 .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
15 let sibling = pkgRoot.deletingLastPathComponent()
16 .appendingPathComponent("org-conformance").appendingPathComponent("cases")
17 if FileManager.default.fileExists(atPath: sibling.path) { return sibling }
18 return nil
19}
20
21private func caseNames(_ dir: URL) -> [String] {
22 let items = (try? FileManager.default.contentsOfDirectory(atPath: dir.path)) ?? []
23 return items.filter { $0.hasSuffix(".org") }.map { String($0.dropLast(4)) }.sorted()
24}
25
26private func read(_ dir: URL, _ name: String, _ ext: String) -> String {
27 (try? String(contentsOf: dir.appendingPathComponent("\(name).\(ext)"), encoding: .utf8)) ?? ""
28}
29
30private func goldenSkeleton(_ dir: URL, _ name: String) -> [String] {
31 let raw = read(dir, name, "skeleton")
32 let trimmed = raw.hasSuffix("\n") ? String(raw.dropLast()) : raw
33 return trimmed.isEmpty ? [] : trimmed.components(separatedBy: "\n")
34}
35
36/// orgo's `render()` produces body content only: `#+TITLE`/`#+AUTHOR`/`#+DATE` are document
37/// metadata carried by the page template, and a top-level `*` heading is `<h2>` because the
38/// title owns `<h1>`. The corpus goldens are that body content, so the renderer is measured
39/// in the matching configuration.
40private let orgoCompatibleOptions = OrgRenderOptions(metadataHeader: false, headingLevelOffset: 1)
41
42/// What each corpus case does against orgo today. This is a *reviewed* record, not a wish:
43/// a case that starts matching, or a matching case that regresses, both fail the test and
44/// demand this map be updated — which is the point. Each `.diverges` reason names the
45/// missing capability, and together they are OrgSwift's conformance backlog (see GAPS.md).
46private enum Expectation {
47 case matches
48 case diverges(String)
49}
50
51private let expectations: [String: Expectation] = [
52 "table": .matches,
53 "blocks": .matches,
54 "core": .matches,
55 "elements": .matches,
56 "footnote": .matches,
57 "headings": .matches,
58 "images": .matches,
59 "lists": .matches,
60 "minimal": .matches,
61 "outofscope": .diverges("out-of-scope constructs; #+INCLUDE and drawers leak — orgo may differ here too"),
62 "tblfm": .matches,
63 "timestamps": .matches,
64]
65
66struct ConformanceTests {
67 /// The skeleton port must be byte-identical to orgo's, or comparing renderers means
68 /// nothing. Prove it against the reference HTML first: feed each corpus `.html` through
69 /// the Swift port and require it to reproduce the checked-in `.skeleton`. A failure here
70 /// is a port bug, isolated from any renderer question.
71 @Test
72 func skeletonPortMatchesGoldens() throws {
73 guard let dir = corpusCasesDir() else {
74 print("org-conformance corpus not found — skipping (set ORG_CONFORMANCE_DIR)")
75 return
76 }
77 for name in caseNames(dir) {
78 let got = OrgSkeleton.skeleton(read(dir, name, "html"))
79 #expect(got == goldenSkeleton(dir, name), "skeleton port diverges from orgo on '\(name)'")
80 }
81 }
82
83 /// Render every case with OrgSwift and check the result against the reviewed
84 /// expectation map. Green when reality matches the record; a case that newly conforms
85 /// (a gap closed) or newly diverges (a regression) fails and points at the map.
86 @Test
87 func renderConformanceMatchesExpectation() throws {
88 guard let dir = corpusCasesDir() else {
89 print("org-conformance corpus not found — skipping (set ORG_CONFORMANCE_DIR)")
90 return
91 }
92 let dump = ProcessInfo.processInfo.environment["ORG_DUMP"] != nil
93 for name in caseNames(dir) {
94 let expected = goldenSkeleton(dir, name)
95 let got = OrgSkeleton.skeleton(OrgRenderer.renderToHTML(read(dir, name, "org"), options: orgoCompatibleOptions))
96 let conforms = got == expected
97
98 switch expectations[name] {
99 case .matches:
100 #expect(conforms, "'\(name)' was expected to match orgo but diverged")
101 case .diverges(let reason):
102 #expect(!conforms, "'\(name)' now MATCHES orgo — gap closed (\(reason)). Move it to .matches in expectations.")
103 case nil:
104 Issue.record("'\(name)' has no entry in the expectations map")
105 }
106
107 if dump && (conforms != (expectations[name].map { if case .matches = $0 { true } else { false } } ?? false)) {
108 let maxN = max(got.count, expected.count)
109 print("---- DIFF \(name) ----")
110 for k in 0..<maxN where (k < expected.count ? expected[k] : "∅") != (k < got.count ? got[k] : "∅") {
111 print(" orgo[\(k)]=\(k < expected.count ? expected[k] : "∅")")
112 print(" ours[\(k)]=\(k < got.count ? got[k] : "∅")")
113 }
114 }
115 }
116 }
117}