krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/AgendaSearchTests.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Tests/OrgCoreTests/AgendaSearchTests.swift history · blame · raw

120 lines · 5503 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    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}