Tests/OrgCoreTests/AgendaSearchTests.swift
119 lines · 5372 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// `org-search-view` against Emacs.
6struct AgendaSearchTests {
7 static let first = """
8 #+TODO: TODO NEXT | DONE
9 Rent before the first heading.
10 * TODO [#A] Pay rent :home:
11 SCHEDULED: <2026-10-01 Thu>
12 Ask the landlord about the boiler.
13 * Meeting 10:00 with Bob
14 Notes about the end of
15 the line, and Bob's plan.
16 * DONE Paid the rent
17 * NEXT Report :work:
18 ** Section with alpha and beta
19 Alpha only here, and a boilerplate.
20 ** Section with alpha
21 Gamma.
22 * todo list of chores
23 Water the plants, alpha.
24 * Archived :ARCHIVE:
25 The rent is here too.
26 ** TODO Under archived rent
27 * COMMENT Commented rent
28 * Two words
29 The phrase two words
30 spans here; foo_bar and foo-bar.
31 Costs $5 or 50% (don't) for the café.
32 * The last
33 word ends this entry with last
34 * word starts the next heading
35 * Ünïcode tïtle :tag:
36 Straße and STRASSE.
37 """
38
39 static let second = """
40 #+CATEGORY: other
41 * WAIT Second file rent
42 * Plans
43 alpha, beta, gamma in the body.
44 ** HOLD Nested beta
45 * Headline alpha
46 """
47
48 static let strings = [
49 // Phrases.
50 "rent", "RENT", "pay rent", "end of the line", "end of\nthe", "two words", "last word",
51 // A match ending in the next heading's line selects that entry.
52 "last * word","Bob's", "boiler",
53 "straße", "foo_bar", "10:00", "nothing at all", "alpha, beta",
54 // Boolean.
55 "+alpha", "+alpha -beta", "+alpha +gamma", "-alpha", "+beta -nested", "{alph?a}", "+{^Gam}", "-{b[eo]}",
56 "+{\\<rent\\>}", "+\"two words\"", "+\"two words\"", "+\"the line\"", "{boiler\\b}", "+alpha +\"in the body\"",
57 "+{foo\\s-bar}", "+boiler -plate", "+{a b}", "+{alpha} -{x y}",
58 // Emacs word syntax in Org buffers: `_`, `-` and `'` are not word characters; `$` and `%` are.
59 "+{\\<bar\\>}", "+{foo\\b}", "+{o\\B_}", "+{\\<5\\>}", "+{\\b50\\b}", "+{don\\>}", "+{\\w+_\\w+}", "+{\\W5}",
60 "+{caf\\w\\>}", "+{\\<\\$5}",
61 // Headlines only.
62 "*rent", "*alpha", "*+alpha -beta", "*+section", "*-alpha", "*pay rent", "*with alpha and",
63 // TODO only and whole words.
64 "!rent", "!+alpha", "*!section", ":+boiler", ":+alpha", "+:boiler", ":boiler", "*!:+rent",
65 ]
66
67 static func lisp(_ s: String) -> String {
68 "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "\n", with: "\\n") + "\""
69 }
70
71 static let files = [("a.org", first), ("b.org", second)]
72 static let today = Days.absolute(year: 2026, month: 10, day: 5)
73
74 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
75 func searchMatchesEmacs() throws {
76 for string in Self.strings {
77 for todoOnly in [false, true] {
78 try AgendaOracle.compareList(files: Self.files, form: "(org-search-view \(todoOnly ? "t" : "nil") \(Self.lisp(string)))", today: Self.today) {
79 Agenda.textSearch($0, string: string, todoOnly: todoOnly)
80 }
81 }
82 }
83 }
84
85 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
86 func searchOptionsMatchEmacs() throws {
87 for string in ["boiler", "+boiler", "alpha beta", "+alpha -beta", "{^Gam}", "two words", "*rent", "rent -paid", "+\"two words\""] {
88 for (boolean, full) in [(true, false), (false, true), (true, true)] {
89 var options = AgendaOptions()
90 options.searchAlwaysBoolean = boolean
91 options.searchForceFullWords = full
92 let form = "(let ((org-agenda-search-view-always-boolean \(boolean ? "t" : "nil")) (org-agenda-search-view-force-full-words \(full ? "t" : "nil"))) (org-search-view nil \(Self.lisp(string))))"
93 try AgendaOracle.compareList(files: Self.files, form: form, today: Self.today) {
94 Agenda.textSearch($0, string: string, options: options)
95 }
96 }
97 }
98 }
99
100 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
101 func searchPrefixMatchesEmacs() throws {
102 let formats = ["search": "%c %?-5:l %b", "agenda": " %i %-12:c%?-12t% s", "todo": " %i %-12:c", "tags": " %i %-12:c"]
103 var options = AgendaOptions()
104 options.prefixFormat = formats
105 let form = "(let ((org-agenda-prefix-format \(AgendaOracle.prefixLisp(formats)))) (org-search-view nil \"alpha\"))"
106 try AgendaOracle.compareList(files: Self.files, form: form, today: Self.today) {
107 Agenda.textSearch($0, string: "alpha", options: options)
108 }
109 }
110
111 @Test func parsesSnippets() {
112 let query = Agenda.SearchQuery("*!:+alpha -\"two words\" {a b} back\\ slash", todoOnly: false, options: AgendaOptions())
113 #expect(query.headlinesOnly && query.todoOnly && query.boolean)
114 #expect(query.positive.map(\.emacs) == ["\\<back slash\\>","\\<alpha\\>", "a b"])
115 #expect(query.negative.map(\.emacs) == ["\\<two words\\>"])
116 let phrase = Agenda.SearchQuery("pay rent.", todoOnly: false, options: AgendaOptions())
117 #expect(!phrase.boolean && phrase.positive.map(\.emacs) == ["pay\\s-+rent\\."])
118 }
119}