aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-compose.el3
-rw-r--r--sx-inbox.el5
-rw-r--r--sx-interaction.el23
-rw-r--r--sx-load.el1
-rw-r--r--sx-question-list.el9
-rw-r--r--sx-question-mode.el1
-rw-r--r--sx-question.el15
-rw-r--r--sx-request.el2
-rw-r--r--sx-search.el112
-rw-r--r--sx-tab.el8
-rw-r--r--sx.el223
-rw-r--r--test/test-api.el13
-rw-r--r--test/test-macros.el22
-rw-r--r--test/test-printing.el73
-rw-r--r--test/test-search.el53
-rw-r--r--test/test-util.el31
-rw-r--r--test/tests.el177
17 files changed, 503 insertions, 268 deletions
diff --git a/sx-compose.el b/sx-compose.el
index 5201435..ab4a58d 100644
--- a/sx-compose.el
+++ b/sx-compose.el
@@ -149,7 +149,8 @@ respectively added locally to `sx-compose-before-send-hook' and
(error "Invalid PARENT"))
(let ((is-question
(and (listp parent)
- (cdr (assoc 'title parent)))))
+ (or (null parent)
+ (cdr (assoc 'title parent))))))
(with-current-buffer (sx-compose--get-buffer-create site parent)
(sx-compose-mode)
(setq sx-compose--send-function
diff --git a/sx-inbox.el b/sx-inbox.el
index 07453d4..d0be379 100644
--- a/sx-inbox.el
+++ b/sx-inbox.el
@@ -170,14 +170,13 @@ is an alist containing the elements:
(list
(propertize
" " 'display
- (concat "\n " .title "\n"
+ (concat "\n " (propertize .title 'face 'sx-question-list-date) "\n"
(let ((col fill-column))
(with-temp-buffer
(setq fill-column col)
(insert " " .body)
(fill-region (point-min) (point-max))
- (propertize (buffer-string)
- 'face 'font-lock-function-name-face))))
+ (buffer-string))))
'face 'default))))))
diff --git a/sx-interaction.el b/sx-interaction.el
index 619f259..3877035 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -359,11 +359,24 @@ from context at point."
(let ((default (or sx-question-list--site
(sx-assoc-let sx-question-mode--data .site_par)
sx-default-site)))
- (funcall (if ido-mode #'ido-completing-read #'completing-read)
- (format "Site (%s): " default)
- (sx-site-get-api-tokens) nil t nil nil
- default)))
-
+ (sx-completing-read
+ (format "Site (%s): " default)
+ (sx-site-get-api-tokens) nil t nil nil
+ default)))
+
+(defun sx--maybe-site-prompt (arg)
+ "Get a site token conditionally in an interactive context.
+If ARG is non-nil, use `sx--interactive-site-prompt'.
+Otherwise, use `sx-question-list--site' if non-nil.
+If nil, use `sx--interactive-site-prompt' anyway."
+ ;; This could eventually be generalized into (sx--maybe-prompt
+ ;; prefix-arg value-if-non-nil #'prompt-function).
+ (if arg
+ (sx--interactive-site-prompt)
+ (or sx-question-list--site
+ (sx--interactive-site-prompt))))
+
+;;;###autoload
(defun sx-ask (site)
"Start composing a question for SITE.
SITE is a string, indicating where the question will be posted."
diff --git a/sx-load.el b/sx-load.el
index 481dba3..e7cb6b0 100644
--- a/sx-load.el
+++ b/sx-load.el
@@ -41,6 +41,7 @@
sx-question-mode
sx-question-print
sx-request
+ sx-search
sx-site
sx-tab
))
diff --git a/sx-question-list.el b/sx-question-list.el
index 4f71251..cf849db 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -317,6 +317,7 @@ into consideration.
(":" sx-question-list-switch-site)
("t" sx-tab-switch)
("a" sx-ask)
+ ("s" sx-search)
("v" sx-visit-externally)
("u" sx-toggle-upvote)
("d" sx-toggle-downvote)
@@ -333,6 +334,11 @@ Non-interactively, DATA is a question alist."
(tabulated-list-get-id)
(sx-user-error "Not in `sx-question-list-mode'"))))
(sx-question--mark-hidden data)
+ ;; The current entry will not be present after the list is
+ ;; redisplayed. To avoid `tabulated-list-mode' getting lost (and
+ ;; sending us to the top) we move to the next entry before
+ ;; redisplaying.
+ (forward-line 1)
(when (called-interactively-p 'any)
(sx-question-list-refresh 'redisplay 'noupdate)))
@@ -554,12 +560,11 @@ This does not update `sx-question-mode--window'."
(defun sx-question-list-switch-site (site)
"Switch the current site to SITE and display its questions.
-Use `ido-completing-read' if variable `ido-mode' is active.
Retrieve completions from `sx-site-get-api-tokens'.
Sets `sx-question-list--site' and then call
`sx-question-list-refresh' with `redisplay'."
(interactive
- (list (funcall (if ido-mode #'ido-completing-read #'completing-read)
+ (list (sx-completing-read
"Switch to site: " (sx-site-get-api-tokens)
(lambda (site) (not (equal site sx-question-list--site)))
t)))
diff --git a/sx-question-mode.el b/sx-question-mode.el
index 7d61167..721f935 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -231,6 +231,7 @@ Letters do not insert themselves; instead, they are commands.
(" " scroll-up-command)
("a" sx-answer)
("e" sx-edit)
+ ("s" sx-search)
(,(kbd "S-SPC") scroll-down-command)
([backspace] scroll-down-command)
([tab] forward-button)
diff --git a/sx-question.el b/sx-question.el
index a890d37..1df67cd 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -160,14 +160,13 @@ If no cache exists for it, initialize one with SITE."
(let ((site-cell (assoc .site_par sx-question--user-hidden-list)))
;; If question already hidden, do nothing.
(unless (memq .question_id site-cell)
- ;; First question from this site.
- (push (list .site_par .question_id) sx-question--user-hidden-list)
- ;; Question wasn't present.
- ;; Add it in, but make sure it's sorted (just in case we need
- ;; it later).
- (sx-sorted-insert-skip-first .question_id site-cell >)
- ;; This causes a small lag on `j' and `k' as the list gets large.
- ;; Should we do this on a timer?
+ (if (null site-cell)
+ ;; First question from this site.
+ (push (list .site_par .question_id) sx-question--user-hidden-list)
+ ;; Not first question and question wasn't present.
+ ;; Add it in, but make sure it's sorted (just in case we
+ ;; decide to rely on it later).
+ (sx-sorted-insert-skip-first .question_id site-cell >))
;; Save the results.
(sx-cache-set 'hidden-questions sx-question--user-hidden-list)))))
diff --git a/sx-request.el b/sx-request.el
index 1031ea7..bc34f9c 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -162,7 +162,7 @@ the main content of the response is returned."
.method .error_id .error_name .error_message))
(when (< (setq sx-request-remaining-api-requests .quota_remaining)
sx-request-remaining-api-requests-message-threshold)
- (sx-message "%d API requests reamining"
+ (sx-message "%d API requests remaining"
sx-request-remaining-api-requests))
(sx-encoding-clean-content-deep .items)))))))
diff --git a/sx-search.el b/sx-search.el
new file mode 100644
index 0000000..2633da9
--- /dev/null
+++ b/sx-search.el
@@ -0,0 +1,112 @@
+;;; sx-search.el --- Searching for questions. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2014 Artur Malabarba
+
+;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
+
+;; 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:
+
+;; Implements sarch functionality. The basic function is
+;; `sx-search-get-questions', which returns an array of questions
+;; according to a search term.
+;;
+;; This also defines a user-level command, `sx-search', which is an
+;; interactive wrapper around `sx-search-get-questions' and
+;; `sx-question-list-mode'.
+
+
+;;; Code:
+
+(require 'sx)
+(require 'sx-question-list)
+
+(defvar sx-search--query-history nil
+ "Query history for interactive prompts.")
+
+(defvar sx-search--tag-history nil
+ "Tags history for interactive prompts.")
+
+
+;;; Basic function
+(defun sx-search-get-questions (site page query &optional tags excluded-tags keywords)
+ "Like `sx-question-get-questions', but restrict results by a search.
+
+Perform search on SITE. PAGE is an integer indicating which page
+of results to return. QUERY, TAGS, and EXCLUDED-TAGS restrict the
+possible returned questions as per `sx-search'.
+
+Either QUERY or TAGS must be non-nil, or the search will
+fail. EXCLUDED-TAGS is only is used if TAGS is also provided.
+
+KEYWORDS is passed to `sx-method-call'."
+ (sx-method-call 'search
+ :keywords `((page . ,page)
+ (sort . activity)
+ (intitle . ,query)
+ (tagged . ,tags)
+ (nottagged . ,excluded-tags)
+ ,@keywords)
+ :site site
+ :auth t
+ :filter sx-browse-filter))
+
+
+;;; User command
+(defun sx-search (site query &optional tags excluded-tags)
+ "Display search on SITE for question titles containing QUERY.
+When TAGS is given, it is a lists of tags, one of which must
+match. When EXCLUDED-TAGS is given, it is a list of tags, none
+of which is allowed to match.
+
+Interactively, the user is asked for SITE and QUERY. With a
+prefix argument, the user is asked for everything."
+ (interactive
+ (let ((site (sx--maybe-site-prompt current-prefix-arg))
+ (query (read-string
+ (format "Query (%s): "
+ (if current-prefix-arg "optional" "mandatory"))
+ ""
+ 'sx-search--query-history))
+ tags excluded-tags)
+ (when (string= query "")
+ (setq query nil))
+ (when current-prefix-arg
+ (setq tags (sx--multiple-read
+ (format "Tags (%s)"
+ (if query "optional" "mandatory"))
+ 'sx-search--tag-history))
+ (when (and (not query) (string= "" tags))
+ (sx-user-error "Must supply either QUERY or TAGS"))
+ (setq excluded-tags
+ (sx--multiple-read
+ "Excluded tags (optional)" 'sx-search--tag-history)))
+ (list site query tags excluded-tags)))
+
+ ;; Here starts the actual function
+ (sx-initialize)
+ (with-current-buffer (get-buffer-create "*sx-search-result*")
+ (sx-question-list-mode)
+ (setq sx-question-list--next-page-function
+ (lambda (page)
+ (sx-search-get-questions
+ sx-question-list--site page
+ query tags excluded-tags)))
+ (setq sx-question-list--site site)
+ (sx-question-list-refresh 'redisplay)
+ (switch-to-buffer (current-buffer))))
+
+(provide 'sx-search)
+;;; sx-search.el ends here
diff --git a/sx-tab.el b/sx-tab.el
index 4b9d50b..6a2552f 100644
--- a/sx-tab.el
+++ b/sx-tab.el
@@ -34,10 +34,10 @@
(defun sx-tab-switch (tab)
"Switch to another question-list tab."
(interactive
- (list (funcall (if ido-mode #'ido-completing-read #'completing-read)
- "Switch to tab: " sx-tab--list
- (lambda (tab) (not (equal tab sx-question-list--current-tab)))
- t)))
+ (list (sx-completing-read
+ "Switch to tab: " sx-tab--list
+ (lambda (tab) (not (equal tab sx-question-list--current-tab)))
+ t)))
(funcall (intern (format "sx-tab-%s" (downcase tab)))))
diff --git a/sx.el b/sx.el
index 1b15ad3..62484b7 100644
--- a/sx.el
+++ b/sx.el
@@ -6,7 +6,7 @@
;; URL: https://github.com/vermiculus/sx.el/
;; Version: 0.1
;; Keywords: help, hypermedia, tools
-;; Package-Requires: ((emacs "24.1") (cl-lib "0.5") (json "1.3") (markdown-mode "2.0") (let-alist "1.0.1"))
+;; Package-Requires: ((emacs "24.1") (cl-lib "0.5") (json "1.3") (markdown-mode "2.0") (let-alist "1.0.3"))
;; 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
@@ -51,12 +51,111 @@
(browse-url "https://github.com/vermiculus/sx.el/issues/new"))
+;;; Site
+(defun sx--site (data)
+ "Get the site in which DATA belongs.
+DATA can be a question, answer, comment, or user (or any object
+with a `link' property).
+DATA can also be the link itself."
+ (let ((link (if (stringp data) data
+ (cdr (assoc 'link data)))))
+ (when (stringp link)
+ (replace-regexp-in-string
+ (rx string-start
+ "http" (optional "s") "://"
+ (or
+ (sequence
+ (group-n 1 (+ (not (any "/"))))
+ ".stackexchange")
+ (group-n 2 (+ (not (any "/")))))
+ "." (+ (not (any ".")))
+ "/" (* any)
+ string-end)
+ "\\1\\2" link))))
+
+(defun sx--ensure-site (data)
+ "Add a `site' property to DATA if it doesn't have one. Return DATA.
+DATA can be a question, answer, comment, or user (or any object
+with a `link' property)."
+ (when data
+ (let-alist data
+ (unless .site_par
+ ;; @TODO: Change this to .site.api_site_parameter sometime
+ ;; after February.
+ (setcdr data (cons (cons 'site_par
+ (or (cdr (assq 'api_site_parameter .site))
+ (sx--site data)))
+ (cdr data)))))
+ data))
+
+(defun sx--link-to-data (link)
+ "Convert string LINK into data that can be displayed."
+ (let ((result (list (cons 'site (sx--site link)))))
+ ;; Try to strip a question or answer ID
+ (when (or
+ ;; Answer
+ (and (or (string-match
+ ;; From 'Share' button
+ (rx "/a/"
+ ;; Question ID
+ (group (+ digit))
+ ;; User ID
+ "/" (+ digit)
+ ;; Answer ID
+ (group (or (sequence "#" (* any)) ""))
+ string-end) link)
+ (string-match
+ ;; From URL
+ (rx "/questions/" (+ digit) "/"
+ (+ (not (any "/"))) "/"
+ ;; User ID
+ (optional (group (+ digit)))
+ (optional "/")
+ (group (or (sequence "#" (* any)) ""))
+ string-end) link))
+ (push '(type . answer) result))
+ ;; Question
+ (and (or (string-match
+ ;; From 'Share' button
+ (rx "/q/"
+ ;; Question ID
+ (group (+ digit))
+ ;; User ID
+ (optional "/" (+ digit))
+ ;; Answer or Comment ID
+ (group (or (sequence "#" (* any)) ""))
+ string-end) link)
+ (string-match
+ ;; From URL
+ (rx "/questions/"
+ ;; Question ID
+ (group (+ digit))
+ "/") link))
+ (push '(type . question) result)))
+ (push (cons 'id (string-to-number (match-string-no-properties 1 link)))
+ result))
+ result))
+
+(defmacro sx-assoc-let (alist &rest body)
+ "Use ALIST with `let-alist' to execute BODY.
+`.site_par' has a special meaning, thanks to `sx--ensure-site'.
+If ALIST doesn't have a `site' property, one is created using the
+`link' property."
+ (declare (indent 1) (debug t))
+ (require 'let-alist)
+ `(progn
+ (sx--ensure-site ,alist)
+ ,(macroexpand
+ `(let-alist ,alist ,@body))))
+
+
;;; Browsing filter
(defvar sx-browse-filter
'((question.body_markdown
question.comments
question.answers
question.last_editor
+ question.last_activity_date
question.accepted_answer_id
question.link
question.upvoted
@@ -77,6 +176,7 @@
comment.comment_id
answer.answer_id
answer.last_editor
+ answer.last_activity_date
answer.link
answer.share_link
answer.owner
@@ -90,6 +190,29 @@ See `sx-question-get-questions' and `sx-question-get-question'.")
;;; Utility Functions
+(defun sx-completing-read (&rest args)
+ "Like `completing-read', but possibly use ido.
+All ARGS are passed to `completing-read' or `ido-completing-read'."
+ (apply (if ido-mode #'ido-completing-read #'completing-read)
+ args))
+
+(defun sx--multiple-read (prompt hist-var)
+ "Interactively query the user for a list of strings.
+Call `read-string' multiple times, until the input is empty.
+
+PROMPT is a string displayed to the user and should not end with
+a space nor a colon. HIST-VAR is a quoted symbol, indicating a
+list in which to store input history."
+ (let (list input)
+ (while (not (string=
+ ""
+ (setq input (read-string
+ (concat prompt " ["
+ (mapconcat #'identity list ",")
+ "]: ")
+ "" hist-var))))
+ (push input list))
+ list))
(defmacro sx-sorted-insert-skip-first (newelt list &optional predicate)
"Inserted NEWELT into LIST sorted by PREDICATE.
@@ -147,50 +270,6 @@ and sequences of strings."
(funcall first-f sequence-sep)
";"))))))
-(defun sx--filter-data (data desired-tree)
- "Filter DATA and return the DESIRED-TREE.
-
-For example:
-
- (sx--filter-data
- '((prop1 . value1)
- (prop2 . value2)
- (prop3
- (test1 . 1)
- (test2 . 2))
- (prop4 . t))
- '(prop1 (prop3 test2)))
-
-would yield
-
- ((prop1 . value1)
- (prop3
- (test2 . 2)))"
- (if (vectorp data)
- (apply #'vector
- (mapcar (lambda (entry)
- (sx--filter-data
- entry desired-tree))
- data))
- (delq
- nil
- (mapcar (lambda (cons-cell)
- ;; @TODO the resolution of `f' is O(2n) in the worst
- ;; case. It may be faster to implement the same
- ;; functionality as a `while' loop to stop looking the
- ;; list once it has found a match. Do speed tests.
- ;; See edfab4443ec3d376c31a38bef12d305838d3fa2e.
- (let ((f (or (memq (car cons-cell) desired-tree)
- (assoc (car cons-cell) desired-tree))))
- (when f
- (if (and (sequencep (cdr cons-cell))
- (sequencep (elt (cdr cons-cell) 0)))
- (cons (car cons-cell)
- (sx--filter-data
- (cdr cons-cell) (cdr f)))
- cons-cell))))
- data))))
-
(defun sx--shorten-url (url)
"Shorten URL hiding anything other than the domain.
Paths after the domain are replaced with \"...\".
@@ -262,7 +341,7 @@ Return the result of BODY."
("ĥ" . "h")
("ĵ" . "j")
("^[:ascii:]" . ""))
- "List of replacements to use for non-ascii characters
+ "List of replacements to use for non-ascii characters.
Used to convert user names into @mentions.")
(defun sx--user-@name (user)
@@ -285,58 +364,6 @@ removed from the display name before it is returned."
string))
-;;; Site
-(defun sx--site (data)
- "Get the site in which DATA belongs.
-DATA can be a question, answer, comment, or user (or any object
-with a `link' property).
-DATA can also be the link itself."
- (let ((link (if (stringp data) data
- (cdr (assoc 'link data)))))
- (when (stringp link)
- (replace-regexp-in-string
- "^https?://\\(?:\\(?1:[^/]+\\)\\.stackexchange\\|\\(?2:[^/]+\\)\\)\\.[^.]+/.*$"
- "\\1\\2" link))))
-
-(defun sx--ensure-site (data)
- "Add a `site' property to DATA if it doesn't have one. Return DATA.
-DATA can be a question, answer, comment, or user (or any object
-with a `link' property)."
- (when data
- (let-alist data
- (unless .site_par
- (setcdr data (cons (cons 'site_par
- (or .site.api_site_parameter
- (sx--site data)))
- (cdr data)))))
- data))
-
-(defmacro sx-assoc-let (alist &rest body)
- "Identical to `let-alist', except `.site' has a special meaning.
-If ALIST doesn't have a `site' property, one is created using the
-`link' property."
- (declare (indent 1) (debug t))
- `(progn
- (require 'let-alist)
- (sx--ensure-site ,alist)
- (let-alist ,alist ,@body)))
-
-(defun sx--link-to-data (link)
- "Convert string LINK into data that can be displayed."
- (let ((result (list (cons 'site_par (sx--site link)))))
- (when (or
- ;; Answer
- (and (or (string-match "/a/\\([0-9]+\\)/[0-9]+\\(#.*\\|\\)\\'" link)
- (string-match "/questions/[0-9]+/[^/]+/\\([0-9]\\)/?\\(#.*\\|\\)\\'" link))
- (push (cons 'type 'answer) result))
- ;; Question
- (and (or (string-match "/q/\\([0-9]+\\)/[0-9]+\\(#.*\\|\\)\\'" link)
- (string-match "/questions/\\([0-9]+\\)/" link))
- (push (cons 'type 'question) result)))
- (push (cons 'id (string-to-number (match-string-no-properties 1 link)))
- result))
- result))
-
(defcustom sx-init-hook nil
"Hook run when SX initializes.
Run after `sx-init--internal-hook'."
diff --git a/test/test-api.el b/test/test-api.el
new file mode 100644
index 0000000..ca775ff
--- /dev/null
+++ b/test/test-api.el
@@ -0,0 +1,13 @@
+(ert-deftest test-basic-request ()
+ "Test basic request functionality"
+ (should (sx-request-make "sites")))
+
+(ert-deftest test-question-retrieve ()
+ "Test the ability to receive a list of questions."
+ (should (sx-question-get-questions 'emacs)))
+
+(ert-deftest test-bad-request ()
+ "Test a method given a bad set of keywords"
+ (should-error
+ (sx-request-make "questions" '(()))))
+
diff --git a/test/test-macros.el b/test/test-macros.el
new file mode 100644
index 0000000..b6bf20b
--- /dev/null
+++ b/test/test-macros.el
@@ -0,0 +1,22 @@
+(defmacro sx-test-with-json-data (cell &rest body)
+ "Run BODY with sample data let-bound to CELL"
+ (declare (indent 1))
+ `(let ((,cell '((test . nil) (test-one . 1) (test-two . 2)
+ (link . "http://meta.emacs.stackexchange.com/"))))
+ ,@body))
+
+(ert-deftest macro-test--sx-assoc-let ()
+ "Test `sx-assoc-let'"
+ (sx-test-with-json-data data
+ (should
+ (null (let-alist data .site_par))))
+
+ (sx-test-with-json-data data
+ (should
+ (equal (sx-assoc-let data .site_par)
+ "meta.emacs")))
+
+ (sx-test-with-json-data data
+ (should
+ (equal (sx-assoc-let data (cons .test-one .test-two))
+ '(1 . 2)))))
diff --git a/test/test-printing.el b/test/test-printing.el
new file mode 100644
index 0000000..2857cb7
--- /dev/null
+++ b/test/test-printing.el
@@ -0,0 +1,73 @@
+
+;;; Setup
+(require 'cl-lib)
+
+(defmacro line-should-match (regexp)
+ "Test if the line at point matches REGEXP"
+ `(let ((line (buffer-substring-no-properties
+ (line-beginning-position)
+ (line-end-position))))
+ (sx-test-message "Line here is: %S" line)
+ (should (string-match ,regexp line))))
+
+(defmacro question-list-regex (title votes answers &rest tags)
+ "Construct a matching regexp for TITLE, VOTES, and ANSWERS.
+Each element of TAGS is appended at the end of the expression
+after being run through `sx-question--tag-format'."
+ `(rx line-start
+ (+ whitespace) ,(number-to-string votes)
+ (+ whitespace) ,(number-to-string answers)
+ (+ whitespace)
+ ,title
+ (+ (any whitespace digit))
+ (or "y" "d" "h" "m" "mo" "s") " ago"
+ (+ whitespace)
+ (eval (mapconcat #'sx-question--tag-format
+ (list ,@tags) " "))))
+
+
+;;; Tests
+(ert-deftest question-list-tag ()
+ "Test `sx-question--tag-format'."
+ (should
+ (string=
+ (sx-question--tag-format "tag")
+ "[tag]")))
+
+(ert-deftest question-list-display ()
+ (cl-letf (((symbol-function #'sx-request-make)
+ (lambda (&rest _) sx-test-data-questions)))
+ (sx-tab-frontpage nil "emacs")
+ (switch-to-buffer "*question-list*")
+ (goto-char (point-min))
+ (should (equal (buffer-name) "*question-list*"))
+ (line-should-match
+ (question-list-regex
+ "Focus-hook: attenuate colours when losing focus"
+ 1 0 "frames" "hooks" "focus"))
+ (sx-question-list-next 5)
+ (line-should-match
+ (question-list-regex
+ "Babel doesn&#39;t wrap results in verbatim"
+ 0 1 "org-mode" "org-export" "org-babel"))
+ ;; ;; Use this when we have a real sx-question buffer.
+ ;; (call-interactively 'sx-question-list-display-question)
+ ;; (should (equal (buffer-name) "*sx-question*"))
+ (switch-to-buffer "*question-list*")
+ (sx-question-list-previous 4)
+ (line-should-match
+ (question-list-regex
+ "&quot;Making tag completion table&quot; Freezes/Blocks -- how to disable"
+ 2 1 "autocomplete" "performance" "ctags"))))
+
+(ert-deftest sx--user-@name ()
+ "Test `sx--user-@name' character substitution"
+ (should
+ (string=
+ (sx--user-@name '((display_name . "ĥÞßđłřğĝýÿñńśşšŝżźžçćčĉùúûüŭůòóôõöøőðìíîïıèéêëęàåáâäãåąĵ★")))
+ "@hTHssdlrggyynnsssszzzccccuuuuuuooooooooiiiiieeeeeaaaaaaaaj"))
+ (should
+ (string=
+ (sx--user-@name '((display_name . "ĤÞßĐŁŘĞĜÝŸÑŃŚŞŠŜŻŹŽÇĆČĈÙÚÛÜŬŮÒÓÔÕÖØŐÐÌÍÎÏıÈÉÊËĘÀÅÁÂÄÃÅĄĴ")))
+ "@HTHssDLRGGYYNNSSSSZZZCCCCUUUUUUOOOOOOOOIIIIiEEEEEAAAAAAAAJ")))
+
diff --git a/test/test-search.el b/test/test-search.el
new file mode 100644
index 0000000..72dbcdc
--- /dev/null
+++ b/test/test-search.el
@@ -0,0 +1,53 @@
+(defmacro test-with-bogus-string (cell &rest body)
+ "Let-bind a bogus string to CELL and execute BODY."
+ (declare (indent 1))
+ `(let ((,cell "E7631BCF-A94B-4507-8F0C-02CFB3207F55"))
+ ,@body))
+
+
+(ert-deftest test-search-basic ()
+ "Test basic search functionality"
+ (should
+ (sx-search-get-questions
+ "emacs" 1 "emacs")))
+
+(ert-deftest test-search-empty ()
+ "Test bogus search returns empty vector"
+ (test-with-bogus-string query
+ (should
+ (equal
+ []
+ (sx-search-get-questions "emacs" 1 query)))))
+
+(ert-deftest test-search-invalid ()
+ "Test invalid search"
+ (should-error
+ ;; @todo: test the interactive call
+ (sx-search
+ "emacs" nil nil ["emacs"])))
+
+(ert-deftest test-search-full-page ()
+ "Test retrieval of the full search page"
+ (should
+ (= 30 (length (sx-search-get-questions
+ "stackoverflow" 1 "jquery")))))
+
+(ert-deftest test-search-exclude-tags ()
+ "Test excluding tags from a search"
+ (should
+ (cl-every
+ (lambda (p)
+ (sx-assoc-let p
+ (not (member "org-export" .tags))))
+ (sx-search-get-questions
+ "emacs" 1 nil "org-mode" "org-export")))
+ (should
+ (cl-every
+ (lambda (p)
+ (sx-assoc-let p
+ (not (or (member "org-export" .tags)
+ (member "org-agenda" .tags)))))
+ (sx-search-get-questions
+ "emacs" 1 nil "org-mode"
+ ["org-export" "org-agenda"]))))
+
diff --git a/test/test-util.el b/test/test-util.el
new file mode 100644
index 0000000..5db1691
--- /dev/null
+++ b/test/test-util.el
@@ -0,0 +1,31 @@
+(ert-deftest thing-as-string ()
+ "Test `sx--thing-as-string'"
+ (should
+ (string= (sx--thing-as-string
+ '(hello world (this is a test))
+ '(";" "+"))
+ "hello;world;this+is+a+test"))
+ (should
+ (string= (sx--thing-as-string
+ '(this is a test) '(";" "+"))
+ "this;is;a;test"))
+ (should
+ (string= (sx--thing-as-string
+ '(this is a test) "+")
+ "this+is+a+test"))
+ (should
+ (string= (sx--thing-as-string
+ '(this is a test))
+ "this;is;a;test"))
+ (should
+ (string= (sx--thing-as-string
+ 'test)
+ "test"))
+ (should
+ (string= (sx--thing-as-string
+ 'test&)
+ "test&"))
+ (should
+ (string= (sx--thing-as-string
+ 'test& nil t)
+ "test%26")))
diff --git a/test/tests.el b/test/tests.el
index 66d8d88..d06c0ff 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -1,3 +1,5 @@
+
+;;; SX Settings
(defun -sx--nuke ()
(interactive)
(mapatoms
@@ -5,11 +7,17 @@
(if (string-prefix-p "sx-" (symbol-name symbol))
(unintern symbol)))))
-;;; Tests
+(setq
+ sx-initialized t
+ sx-request-remaining-api-requests-message-threshold 50000
+ debug-on-error t
+ user-emacs-directory "."
+ sx-test-base-dir (file-name-directory (or load-file-name "./")))
+
+
+;;; Test Data
(defvar sx-test-data-dir
- (expand-file-name
- "data-samples/"
- (file-name-directory (or load-file-name "./"))))
+ (expand-file-name "data-samples/" sx-test-base-dir))
(defun sx-test-sample-data (method &optional directory)
(let ((file (concat (when directory (concat directory "/"))
@@ -20,157 +28,34 @@
(insert-file-contents file)
(read (buffer-string))))))
-(defmacro line-should-match (regexp)
- ""
- `(let ((line (buffer-substring-no-properties
- (line-beginning-position)
- (line-end-position))))
- (message "Line here is: %S" line)
- (should (string-match ,regexp line))))
-
(setq
- sx-initialized t
- sx-request-remaining-api-requests-message-threshold 50000
- debug-on-error t
- user-emacs-directory "."
-
sx-test-data-questions
(sx-test-sample-data "questions")
sx-test-data-sites
(sx-test-sample-data "sites"))
-(setq package-user-dir
- (expand-file-name (format "../../.cask/%s/elpa" emacs-version)
- sx-test-data-dir))
-(package-initialize)
-
-(require 'cl-lib)
-(require 'sx)
-(require 'sx-question)
-(require 'sx-question-list)
-(require 'sx-tab)
-
-(ert-deftest test-basic-request ()
- "Test basic request functionality"
- (should (sx-request-make "sites")))
-
-(ert-deftest test-question-retrieve ()
- "Test the ability to receive a list of questions."
- (should (sx-question-get-questions 'emacs)))
-
-(ert-deftest test-bad-request ()
- "Test a method given a bad set of keywords"
- (should-error
- (sx-request-make "questions" '(()))))
+
+;;; General Settings
+(setq
+ package-user-dir (expand-file-name
+ (format "../../.cask/%s/elpa" emacs-version)
+ sx-test-data-dir))
-(ert-deftest test-tree-filter ()
- "`sx-core-filter-data'"
- ;; flat
- (should
- (equal
- '((1 . t) (2 . [1 2]) (3))
- (sx--filter-data '((0 . 3) (1 . t) (a . five) (2 . [1 2])
- ("5" . bop) (3) (p . 4))
- '(1 2 3))))
- ;; complex
- (should
- (equal
- '((1 . [a b c])
- (2 . [((a . 1) (c . 3))
- ((a . 4) (c . 6))])
- (3 . peach))
- (sx--filter-data '((1 . [a b c])
- (2 . [((a . 1) (b . 2) (c . 3))
- ((a . 4) (b . 5) (c . 6))])
- (3 . peach)
- (4 . banana))
- '(1 (2 a c) 3))))
+(package-initialize)
- ;; vector
- (should
- (equal
- [((1 . 2) (2 . 3) (3 . 4))
- ((1 . a) (2 . b) (3 . c))
- nil ((1 . alpha) (2 . beta))]
- (sx--filter-data [((1 . 2) (2 . 3) (3 . 4))
- ((1 . a) (2 . b) (3 . c) (5 . seven))
- ((should-not-go))
- ((1 . alpha) (2 . beta))]
- '(1 2 3)))))
+(require 'sx-load)
-(ert-deftest question-list-display ()
- (cl-letf (((symbol-function #'sx-request-make)
- (lambda (&rest _) sx-test-data-questions)))
- (sx-tab-frontpage nil "emacs")
- (switch-to-buffer "*question-list*")
- (goto-char (point-min))
- (should (equal (buffer-name) "*question-list*"))
- (line-should-match
- "^\\s-+1\\s-+0\\s-+Focus-hook: attenuate colours when losing focus [ 0-9]+\\(y\\|d\\|h\\|mo?\\|s\\) ago\\s-+\\[frames\\] \\[hooks\\] \\[focus\\]")
- (sx-question-list-next 5)
- (line-should-match
- "^\\s-+0\\s-+1\\s-+Babel doesn&#39;t wrap results in verbatim [ 0-9]+\\(y\\|d\\|h\\|mo?\\|s\\) ago\\s-+\\[org-mode\\]")
- ;; ;; Use this when we have a real sx-question buffer.
- ;; (call-interactively 'sx-question-list-display-question)
- ;; (should (equal (buffer-name) "*sx-question*"))
- (switch-to-buffer "*question-list*")
- (sx-question-list-previous 4)
- (line-should-match
- "^\\s-+2\\s-+1\\s-+&quot;Making tag completion table&quot; Freezes/Blocks -- how to disable [ 0-9]+\\(y\\|d\\|h\\|mo?\\|s\\) ago\\s-+\\[autocomplete\\]")))
+(defun sx-load-test (test)
+ (load-file
+ (format "%s/test-%s.el"
+ sx-test-base-dir
+ (symbol-name test))))
-(ert-deftest macro-test--sx-assoc-let ()
- "Tests macro expansion for `sx-assoc-let'"
- (should
- (equal '(progn (require 'let-alist)
- (sx--ensure-site data)
- (let-alist data .test))
- (macroexpand '(sx-assoc-let data .test))))
- (should
- (equal '(progn (require 'let-alist)
- (sx--ensure-site data)
- (let-alist data (cons .test-one .test-two)))
- (macroexpand
- '(sx-assoc-let data (cons .test-one .test-two))))))
+(setq sx-test-enable-messages nil)
-(ert-deftest sx--user-@name ()
- "Tests macro expansion for `sx-assoc-let'"
- (should
- (string=
- (sx--user-@name '((display_name . "ĥÞßđłřğĝýÿñńśşšŝżźžçćčĉùúûüŭůòóôõöøőðìíîïıèéêëęàåáâäãåąĵ★")))
- "@hTHssdlrggyynnsssszzzccccuuuuuuooooooooiiiiieeeeeaaaaaaaaj"))
- (should
- (string=
- (sx--user-@name '((display_name . "ĤÞßĐŁŘĞĜÝŸÑŃŚŞŠŜŻŹŽÇĆČĈÙÚÛÜŬŮÒÓÔÕÖØŐÐÌÍÎÏıÈÉÊËĘÀÅÁÂÄÃÅĄĴ")))
- "@HTHssDLRGGYYNNSSSSZZZCCCCUUUUUUOOOOOOOOIIIIiEEEEEAAAAAAAAJ")))
+(defun sx-test-message (message &rest args)
+ (when sx-test-enable-messages
+ (apply #'message message args)))
-(ert-deftest thing-as-string ()
- "Tests `sx--thing-as-string'"
- (should
- (string= (sx--thing-as-string
- '(hello world (this is a test))
- '(";" "+"))
- "hello;world;this+is+a+test"))
- (should
- (string= (sx--thing-as-string
- '(this is a test) '(";" "+"))
- "this;is;a;test"))
- (should
- (string= (sx--thing-as-string
- '(this is a test) "+")
- "this+is+a+test"))
- (should
- (string= (sx--thing-as-string
- '(this is a test))
- "this;is;a;test"))
- (should
- (string= (sx--thing-as-string
- 'test)
- "test"))
- (should
- (string= (sx--thing-as-string
- 'test&)
- "test&"))
- (should
- (string= (sx--thing-as-string
- 'test& nil t)
- "test%26")))
+(mapc #'sx-load-test
+ '(api macros printing util search))