Tests/OrgCoreTests/TextStatsTests.swift
79 lines · 4364 bytes
6 symbols in this file
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct TextStatsTests {
6 static let samples = [
7 "don't stop", "foo_bar baz", "x-y", "café au lait", "日本語 text", "*bold* /it/ =code=", "2026-10-05 Mon",
8 "https://example.com/a-b?c=1", "[[file:x.org][Desc here]]", "* TODO Heading :tag:work:", "a.b,c;d",
9 "it's 3.14 or 1e5", "naïve—dash", "emoji 😀 ok", "über_cool", "«quoted» “smart” ‘single’", "a\u{00A0}b",
10 "\tindented\ttext", "- [X] item\n - sub 2\n", "x × y ÷ z → w", "€5 £3 ©2026 °C ™ ¶ § •", "✓ done ★ star", "¶ § · µ ¬ ¨ ■ ♥ ⌘ ✂ ➜ 🎉", "line one\r\nline two\r\n", "Sony α7 III", "漢字かなカナ ひらがなカタカナ", "e\u{0301}te résumé", "Привет мир", "a\u{0080}\u{0098}b", "",
11 ]
12
13 /// `count-words`, `count-lines` and `current-column` at the end of each sample in an org buffer.
14 static func emacs(_ samples: [String]) throws -> [String] {
15 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-stats-\(UUID().uuidString)")
16 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
17 defer { try? FileManager.default.removeItem(at: folder) }
18 let input = folder.appendingPathComponent("in.json")
19 let output = folder.appendingPathComponent("out.json")
20 let script = folder.appendingPathComponent("stats.el")
21 try JSONEncoder().encode(samples).write(to: input)
22 try """
23 ;;; -*- lexical-binding: t -*-
24 (require 'org)
25 (require 'json)
26 (let ((samples (append (json-read-file (getenv "ORACLE_INPUT")) nil)) result)
27 (dolist (s samples)
28 (with-temp-buffer
29 (org-mode)
30 (insert s)
31 (push (format "%d words %d lines %d chars, %d:%d"
32 (count-words (point-min) (point-max))
33 (count-lines (point-min) (point-max))
34 (- (point-max) (point-min))
35 (line-number-at-pos) (current-column))
36 result)))
37 (let ((coding-system-for-write 'utf-8-unix))
38 (with-temp-file (getenv "ORACLE_OUTPUT")
39 (insert (json-encode (vconcat (nreverse result)))))))
40 """.write(to: script, atomically: true, encoding: .utf8)
41 let log = try EmacsOracle.run(["-Q", "--batch", "-l", script.path], environment: ["ORACLE_INPUT": input.path, "ORACLE_OUTPUT": output.path])
42 guard let data = try? Data(contentsOf: output) else { throw AgendaOracle.OracleError(log: log) }
43 return try JSONDecoder().decode([String].self, from: data)
44 }
45
46 static func ours(_ s: String) -> String {
47 let region = TextStats.region(s)
48 let position = TextStats.position(in: s, at: s.utf16.count)
49 return "\(region.words) words \(region.lines) lines \(region.characters) chars, \(position.line):\(position.column)"
50 }
51
52 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
53 func matchesEmacs() throws {
54 let emacs = try Self.emacs(Self.samples)
55 for (sample, expected) in zip(Self.samples, emacs) {
56 #expect(Self.ours(sample) == expected, "\(sample)")
57 }
58 }
59
60 /// `ORGSTAR_ORACLE_CORPUS=<folder>`: every org file under the folder.
61 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_ORACLE_CORPUS"] != nil))
62 func corpusMatchesEmacs() throws {
63 let root = URL(fileURLWithPath: ProcessInfo.processInfo.environment["ORGSTAR_ORACLE_CORPUS"]!)
64 let files = FileManager.default.enumerator(at: root, includingPropertiesForKeys: nil)!
65 .compactMap { $0 as? URL }.filter { $0.pathExtension == "org" }
66 let texts = files.compactMap { try? String(contentsOf: $0, encoding: .utf8) }
67 let emacs = try Self.emacs(texts)
68 for (text, expected) in zip(texts, emacs) {
69 #expect(Self.ours(text) == expected, "\(text.prefix(60))")
70 }
71 }
72
73 @Test func positionMidLine() {
74 let text = "* Heading\n\tab日本\n"
75 let offset = (text as NSString).range(of: "本").location
76 #expect(TextStats.position(in: text, at: offset) == (2, 12))
77 #expect(TextStats.position(in: text, at: 0) == (1, 0))
78 }
79}