import Foundation
import Testing
@testable import OrgSwift
/// Locate the org-conformance corpus: env var first, then the sibling checkout that sits
/// next to this package under a shared parent (…/org-swift and …/org-conformance).
private func corpusCasesDir() -> URL? {
if let env = ProcessInfo.processInfo.environment["ORG_CONFORMANCE_DIR"] {
let cases = URL(fileURLWithPath: env).appendingPathComponent("cases")
if FileManager.default.fileExists(atPath: cases.path) { return cases }
}
// #filePath = …/org-swift/Tests/OrgSwiftTests/ConformanceTests.swift
let pkgRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
let sibling = pkgRoot.deletingLastPathComponent()
.appendingPathComponent("org-conformance").appendingPathComponent("cases")
if FileManager.default.fileExists(atPath: sibling.path) { return sibling }
return nil
}
private func caseNames(_ dir: URL) -> [String] {
let items = (try? FileManager.default.contentsOfDirectory(atPath: dir.path)) ?? []
return items.filter { $0.hasSuffix(".org") }.map { String($0.dropLast(4)) }.sorted()
}
private func read(_ dir: URL, _ name: String, _ ext: String) -> String {
(try? String(contentsOf: dir.appendingPathComponent("\(name).\(ext)"), encoding: .utf8)) ?? ""
}
private func goldenSkeleton(_ dir: URL, _ name: String) -> [String] {
let raw = read(dir, name, "skeleton")
let trimmed = raw.hasSuffix("\n") ? String(raw.dropLast()) : raw
return trimmed.isEmpty ? [] : trimmed.components(separatedBy: "\n")
}
/// orgo's `render()` produces body content only: `#+TITLE`/`#+AUTHOR`/`#+DATE` are document
/// metadata carried by the page template, and a top-level `*` heading is `
` because the
/// title owns ``. The corpus goldens are that body content, so the renderer is measured
/// in the matching configuration.
private let orgoCompatibleOptions = OrgRenderOptions(metadataHeader: false, headingLevelOffset: 1)
/// What each corpus case does against orgo today. This is a *reviewed* record, not a wish:
/// a case that starts matching, or a matching case that regresses, both fail the test and
/// demand this map be updated — which is the point. Each `.diverges` reason names the
/// missing capability, and together they are OrgSwift's conformance backlog (see GAPS.md).
private enum Expectation {
case matches
case diverges(String)
}
private let expectations: [String: Expectation] = [
"table": .matches,
"blocks": .matches,
"core": .matches,
"elements": .matches,
"footnote": .matches,
"headings": .matches,
"images": .matches,
"lists": .matches,
"minimal": .matches,
"outofscope": .diverges("out-of-scope constructs; #+INCLUDE and drawers leak — orgo may differ here too"),
"tblfm": .matches,
"timestamps": .matches,
]
struct ConformanceTests {
/// The skeleton port must be byte-identical to orgo's, or comparing renderers means
/// nothing. Prove it against the reference HTML first: feed each corpus `.html` through
/// the Swift port and require it to reproduce the checked-in `.skeleton`. A failure here
/// is a port bug, isolated from any renderer question.
@Test
func skeletonPortMatchesGoldens() throws {
guard let dir = corpusCasesDir() else {
print("org-conformance corpus not found — skipping (set ORG_CONFORMANCE_DIR)")
return
}
for name in caseNames(dir) {
let got = OrgSkeleton.skeleton(read(dir, name, "html"))
#expect(got == goldenSkeleton(dir, name), "skeleton port diverges from orgo on '\(name)'")
}
}
/// Render every case with OrgSwift and check the result against the reviewed
/// expectation map. Green when reality matches the record; a case that newly conforms
/// (a gap closed) or newly diverges (a regression) fails and points at the map.
@Test
func renderConformanceMatchesExpectation() throws {
guard let dir = corpusCasesDir() else {
print("org-conformance corpus not found — skipping (set ORG_CONFORMANCE_DIR)")
return
}
let dump = ProcessInfo.processInfo.environment["ORG_DUMP"] != nil
for name in caseNames(dir) {
let expected = goldenSkeleton(dir, name)
let got = OrgSkeleton.skeleton(OrgRenderer.renderToHTML(read(dir, name, "org"), options: orgoCompatibleOptions))
let conforms = got == expected
switch expectations[name] {
case .matches:
#expect(conforms, "'\(name)' was expected to match orgo but diverged")
case .diverges(let reason):
#expect(!conforms, "'\(name)' now MATCHES orgo — gap closed (\(reason)). Move it to .matches in expectations.")
case nil:
Issue.record("'\(name)' has no entry in the expectations map")
}
if dump && (conforms != (expectations[name].map { if case .matches = $0 { true } else { false } } ?? false)) {
let maxN = max(got.count, expected.count)
print("---- DIFF \(name) ----")
for k in 0..