aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-filter.el
blob: 6143311197d2df3a8958851fc07c7f8b3144eab5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
;;; test-pearl-filter.el --- Tests for the issue filter DSL -*- lexical-binding: t; -*-

;; Copyright (C) 2026 Craig Jennings

;; Author: Craig Jennings <c@cjennings.net>

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Tests for the Layer 1 issue-filter DSL: `pearl--build-issue-filter'
;; (and its predicate helpers) and `pearl--validate-issue-filter'.  All
;; pure -- no network.  Each authoring key is checked in isolation, then in
;; combination (sibling clauses AND-ed), with `:state'/`:open' precedence and a
;; json-encode round-trip; validation covers the error cases.

;;; Code:

(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'json)

;;; predicate helpers

(ert-deftest test-pearl-filter-eq-helper ()
  "`--eq' wraps a value in an eq comparator."
  (should (equal (pearl--eq "x") '(("eq" . "x"))))
  (should (equal (pearl--eq t) '(("eq" . t)))))

(ert-deftest test-pearl-filter-in-nin-helpers-make-vectors ()
  "`--in' / `--nin' encode their values as JSON arrays (vectors)."
  (should (equal (pearl--in '("a" "b")) '(("in" . ["a" "b"]))))
  (should (equal (pearl--nin '("a")) '(("nin" . ["a"])))))

;;; compile-priority

(ert-deftest test-pearl-filter-compile-priority-symbol-and-int ()
  "Priority symbols map to numbers; integers pass through."
  (should (= 1 (pearl--compile-priority 'urgent)))
  (should (= 0 (pearl--compile-priority 'none)))
  (should (= 4 (pearl--compile-priority 'low)))
  (should (= 3 (pearl--compile-priority 3))))

;;; build-issue-filter -- single dimensions (Normal)

(ert-deftest test-pearl-filter-assignee-me ()
  ":assignee :me compiles to assignee.isMe.eq true."
  (should (equal (pearl--build-issue-filter '(:assignee :me))
                 '(("assignee" ("isMe" ("eq" . t)))))))

(ert-deftest test-pearl-filter-assignee-email ()
  ":assignee with an email compiles to assignee.email.eq."
  (should (equal (pearl--build-issue-filter '(:assignee "x@y.com"))
                 '(("assignee" ("email" ("eq" . "x@y.com")))))))

(ert-deftest test-pearl-filter-open ()
  ":open t compiles to state.type nin the closed types."
  (should (equal (pearl--build-issue-filter '(:open t))
                 '(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"])))))))

(ert-deftest test-pearl-filter-state-name ()
  ":state compiles to state.name.eq."
  (should (equal (pearl--build-issue-filter '(:state "In Progress"))
                 '(("state" ("name" ("eq" . "In Progress")))))))

(ert-deftest test-pearl-filter-state-type-list ()
  ":state-type with a list compiles to state.type.in."
  (should (equal (pearl--build-issue-filter '(:state-type ("started" "unstarted")))
                 '(("state" ("type" ("in" . ["started" "unstarted"])))))))

(ert-deftest test-pearl-filter-state-type-single ()
  ":state-type with a bare string is wrapped into a one-element array."
  (should (equal (pearl--build-issue-filter '(:state-type "started"))
                 '(("state" ("type" ("in" . ["started"])))))))

(ert-deftest test-pearl-filter-project-team-cycle ()
  ":project / :cycle compile to id.eq; :team to key.eq."
  (should (equal (pearl--build-issue-filter '(:project "p-1"))
                 '(("project" ("id" ("eq" . "p-1"))))))
  (should (equal (pearl--build-issue-filter '(:team "ENG"))
                 '(("team" ("key" ("eq" . "ENG"))))))
  (should (equal (pearl--build-issue-filter '(:cycle "c-1"))
                 '(("cycle" ("id" ("eq" . "c-1")))))))

(ert-deftest test-pearl-filter-labels-any-of ()
  ":labels compiles to labels.some.name.in (carries any of the listed labels)."
  (should (equal (pearl--build-issue-filter '(:labels ("bug" "p1")))
                 '(("labels" ("some" ("name" ("in" . ["bug" "p1"]))))))))

(ert-deftest test-pearl-filter-priority-symbol ()
  ":priority symbol compiles to priority.eq with the numeric value."
  (should (equal (pearl--build-issue-filter '(:priority high))
                 '(("priority" ("eq" . 2))))))

;;; precedence (:state / :state-type win over :open)

(ert-deftest test-pearl-filter-explicit-state-beats-open ()
  "An explicit :state overrides :open."
  (should (equal (pearl--build-issue-filter '(:open t :state "Done"))
                 '(("state" ("name" ("eq" . "Done")))))))

(ert-deftest test-pearl-filter-state-type-beats-open ()
  ":state-type overrides :open."
  (should (equal (pearl--build-issue-filter '(:open t :state-type ("started")))
                 '(("state" ("type" ("in" . ["started"])))))))

;;; composition (sibling clauses AND-ed) + ordering keys ignored

(ert-deftest test-pearl-filter-composition-keeps-all-clauses ()
  "Multiple keys produce sibling clauses; :sort/:order don't affect the filter."
  (let ((f (pearl--build-issue-filter
            '(:assignee :me :open t :project "p-1" :labels ("bug")
              :priority urgent :sort updated :order desc))))
    (should (assoc "assignee" f))
    (should (assoc "state" f))
    (should (assoc "project" f))
    (should (assoc "labels" f))
    (should (assoc "priority" f))
    ;; ordering keys are not part of the IssueFilter
    (should-not (assoc "sort" f))
    (should-not (assoc "order" f))))

;;; boundary

(ert-deftest test-pearl-filter-empty-plist-empty-filter ()
  "An empty plist compiles to an empty filter."
  (should (null (pearl--build-issue-filter '()))))

(ert-deftest test-pearl-filter-priority-zero-kept ()
  ":priority 0 (none) is kept, not treated as absent."
  (should (equal (pearl--build-issue-filter '(:priority 0))
                 '(("priority" ("eq" . 0))))))

;;; json-encode round-trip (proves the alist shape renders the right JSON)

(ert-deftest test-pearl-filter-json-encodes-as-expected ()
  "The compiled filter json-encodes to the expected IssueFilter JSON."
  (should (string= (json-encode (pearl--build-issue-filter '(:assignee :me :open t)))
                   (concat "{\"assignee\":{\"isMe\":{\"eq\":true}},"
                           "\"state\":{\"type\":{\"nin\":"
                           "[\"completed\",\"canceled\",\"duplicate\"]}}}"))))

;;; validation (Error cases)

(ert-deftest test-pearl-filter-validate-accepts-good-filter ()
  "A well-formed filter validates to t."
  (should (eq t (pearl--validate-issue-filter
                 '(:assignee :me :open t :priority high :labels ("bug") :order desc)))))

(ert-deftest test-pearl-filter-validate-rejects-unknown-key ()
  "An unknown key is a user-error."
  (should-error (pearl--validate-issue-filter '(:bogus 1)) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-odd-plist ()
  "A plist with an odd number of elements is a user-error."
  (should-error (pearl--validate-issue-filter '(:open)) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-bad-priority-symbol ()
  "An unrecognized priority symbol is a user-error."
  (should-error (pearl--validate-issue-filter '(:priority huge)) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-priority-out-of-range ()
  "A priority integer outside 0-4 is a user-error."
  (should-error (pearl--validate-issue-filter '(:priority 9)) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-bad-assignee ()
  "An :assignee that is neither :me nor a string is a user-error."
  (should-error (pearl--validate-issue-filter '(:assignee 42)) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-empty-string ()
  "An empty string for a value key is a user-error."
  (should-error (pearl--validate-issue-filter '(:project "")) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-bad-order ()
  "An :order other than asc/desc is a user-error."
  (should-error (pearl--validate-issue-filter '(:order sideways)) :type 'user-error))

(ert-deftest test-pearl-filter-validate-rejects-non-string-label ()
  "A non-string entry in :labels is a user-error."
  (should-error (pearl--validate-issue-filter '(:labels ("bug" 7))) :type 'user-error))

(provide 'test-pearl-filter)
;;; test-pearl-filter.el ends here