Tests/OrgCoreTests/AgendaSearchTests.swift
120 lines · 5503 bytes
6 symbols in this file
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 Marked ©2026, «quoted», a·b, x→y and 😀smile.
33 * The last
34 word ends this entry with last
35 * word starts the next heading
36 * Ünïcode tïtle :tag:
37 Straße and STRASSE.
38 """
39
40 static let second = """
41 #+CATEGORY: other
42 * WAIT Second file rent
43 * Plans
44 alpha, beta, gamma in the body.
45 ** HOLD Nested beta
46 * Headline alpha
47 """
48
49 static let strings = [
50 // Phrases.
51 "rent", "RENT", "pay rent", "end of the line", "end of\nthe", "two words", "last word",
52 // A match ending in the next heading's line selects that entry.
53 "last * word","Bob's", "boiler",
54 "straße", "foo_bar", "10:00", "nothing at all", "alpha, beta",
55 // Boolean.
56 "+alpha", "+alpha -beta", "+alpha +gamma", "-alpha", "+beta -nested", "{alph?a}", "+{^Gam}", "-{b[eo]}",
57 "+{\\<rent\\>}", "+\"two words\"", "+\"two words\"", "+\"the line\"", "{boiler\\b}", "+alpha +\"in the body\"",
58 "+{foo\\s-bar}", "+boiler -plate", "+{a b}", "+{alpha} -{x y}",
59 // Emacs word syntax in the search view: `_`, `-` and `'` are not word characters; `$` and `%` are.
60 "+{\\<bar\\>}", "+{foo\\b}", "+{o\\B_}", "+{\\<5\\>}", "+{\\b50\\b}", "+{don\\>}", "+{\\w+_\\w+}", "+{\\W5}",
61 "+{caf\\w\\>}", "+{\\<\\$5}", "+{\\<2026}", "+{\\<quoted\\>}", "+{a\\b·}", "+{\\<b,}", "+{x\\>}",
62 // Headlines only.
63 "*rent", "*alpha", "*+alpha -beta", "*+section", "*-alpha", "*pay rent", "*with alpha and",
64 // TODO only and whole words.
65 "!rent", "!+alpha", "*!section", ":+boiler", ":+alpha", "+:boiler", ":boiler", "*!:+rent",
66 ]
67
68 static func lisp(_ s: String) -> String {
69 "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "\n", with: "\\n") + "\""
70 }
71
72 static let files = [("a.org", first), ("b.org", second)]
73 static let today = Days.absolute(year: 2026, month: 10, day: 5)
74
75 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
76 func searchMatchesEmacs() throws {
77 for string in Self.strings {
78 for todoOnly in [false, true] {
79 try AgendaOracle.compareList(files: Self.files, form: "(org-search-view \(todoOnly ? "t" : "nil") \(Self.lisp(string)))", today: Self.today) {
80 Agenda.textSearch($0, string: string, todoOnly: todoOnly)
81 }
82 }
83 }
84 }
85
86 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
87 func searchOptionsMatchEmacs() throws {
88 for string in ["boiler", "+boiler", "alpha beta", "+alpha -beta", "{^Gam}", "two words", "*rent", "rent -paid", "+\"two words\""] {
89 for (boolean, full) in [(true, false), (false, true), (true, true)] {
90 var options = AgendaOptions()
91 options.searchAlwaysBoolean = boolean
92 options.searchForceFullWords = full
93 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))))"
94 try AgendaOracle.compareList(files: Self.files, form: form, today: Self.today) {
95 Agenda.textSearch($0, string: string, options: options)
96 }
97 }
98 }
99 }
100
101 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
102 func searchPrefixMatchesEmacs() throws {
103 let formats = ["search": "%c %?-5:l %b", "agenda": " %i %-12:c%?-12t% s", "todo": " %i %-12:c", "tags": " %i %-12:c"]
104 var options = AgendaOptions()
105 options.prefixFormat = formats
106 let form = "(let ((org-agenda-prefix-format \(AgendaOracle.prefixLisp(formats)))) (org-search-view nil \"alpha\"))"
107 try AgendaOracle.compareList(files: Self.files, form: form, today: Self.today) {
108 Agenda.textSearch($0, string: "alpha", options: options)
109 }
110 }
111
112 @Test func parsesSnippets() {
113 let query = Agenda.SearchQuery("*!:+alpha -\"two words\" {a b} back\\ slash", todoOnly: false, options: AgendaOptions())
114 #expect(query.headlinesOnly && query.todoOnly && query.boolean)
115 #expect(query.positive.map(\.emacs) == ["\\<back slash\\>","\\<alpha\\>", "a b"])
116 #expect(query.negative.map(\.emacs) == ["\\<two words\\>"])
117 let phrase = Agenda.SearchQuery("pay rent.", todoOnly: false, options: AgendaOptions())
118 #expect(!phrase.boolean && phrase.positive.map(\.emacs) == ["pay\\s-+rent\\."])
119 }
120}