diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 315853a..0c49810 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,12 +17,27 @@ jobs: - 26.3 - snapshot steps: - - name: Set up Emacs - uses: purcell/setup-emacs@v1.0 - with: - version: ${{ matrix.emacs_version }} - - uses: actions/checkout@v1 - - name: Install taskwarrior package - run: sudo apt-get install -y taskwarrior - - name: Run ERT test suite - run: make test + + - name: Set up Emacs + uses: purcell/setup-emacs@v1.0 + with: + version: ${{ matrix.emacs_version }} + + - uses: actions/checkout@v1 + + - name: Install taskwarrior package + run: sudo apt-get install -y taskwarrior + + - name: Create taskwarrior config + run: echo -e 'data.location=~/.task\nverbose=no' > ~/.taskrc + + - name: Install elisp dependencies + run: make ci-dependencies + + # TODO: Replace this with `make check` when packaging for MELPA + + - name: Run checkdoc + run: make lint-checkdoc + + - name: Run ERT test suite + run: make test diff --git a/.gitignore b/.gitignore index a59aa95..fc333bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .task -dash.el -transient.el \ No newline at end of file +makel.el \ No newline at end of file diff --git a/Makefile b/Makefile index 457d278..be0cd6b 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,30 @@ +ELPA_DEPENDENCIES=package-lint transient dash + +ELPA_ARCHIVES=melpa-stable gnu + +TEST_ERT_FILES = $(wildcard test/*.el) +LINT_CHECKDOC_FILES = "taskwarrior.el" ${TEST_ERT_FILES} +LINT_PACKAGE_LINT_FILES = ${LINT_CHECKDOC_FILES} +LINT_COMPILE_FILES = ${LINT_CHECKDOC_FILES} + +makel.mk: # Download makel + @if [ -f ../makel/makel.mk ]; then \ + ln -s ../makel/makel.mk .; \ + else \ + curl \ + --fail --silent --show-error --insecure --location \ + --retry 9 --retry-delay 9 \ + -O https://gitlab.petton.fr/DamienCassou/makel/raw/v0.5.3/makel.mk; \ + fi .PHONY: test-data test-data: ## Generate some example tasks for n in $$(seq 10); do task add "Example task $${n}"; done -.PHONY: test -test: fetch-deps ## Run ERT test suite - emacs -batch -l dash.el -l transient.el -l taskwarrior.el -l taskwarrior-test.el -f ert-run-tests-batch-and-exit +# .PHONY: fetch-deps +# fetch-deps: ## Fetch required dependencies +# curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magit/transient/master/lisp/transient.el" +# curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magnars/dash.el/master/dash.el" -.PHONY: fetch-deps -fetch-deps: ## Fetch required dependencies - curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magit/transient/master/lisp/transient.el" - curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magnars/dash.el/master/dash.el" +# Include makel.mk if present +-include makel.mk diff --git a/makel.mk b/makel.mk new file mode 100644 index 0000000..ec2db05 --- /dev/null +++ b/makel.mk @@ -0,0 +1,154 @@ +MAKEL_VERSION=0.5.3 + +MAKEL_LOAD_PATH=-L . $(patsubst %,-L ../%,$(ELPA_DEPENDENCIES)) + +MAKEL_SET_ARCHIVES0=${ELPA_ARCHIVES} +MAKEL_SET_ARCHIVES1=$(patsubst gnu,(cons \"gnu\" \"https://elpa.gnu.org/packages/\"),${MAKEL_SET_ARCHIVES0}) +MAKEL_SET_ARCHIVES2=$(patsubst melpa,(cons \"melpa\" \"https://melpa.org/packages/\"),${MAKEL_SET_ARCHIVES1}) +MAKEL_SET_ARCHIVES3=$(patsubst melpa-stable,(cons \"melpa-stable\" \"https://stable.melpa.org/packages/\"),${MAKEL_SET_ARCHIVES2}) +MAKEL_SET_ARCHIVES4=$(patsubst org,(cons \"org\" \"https://orgmode.org/elpa/\"),${MAKEL_SET_ARCHIVES3}) +MAKEL_SET_ARCHIVES=(setq package-archives (list ${MAKEL_SET_ARCHIVES4})) + +EMACSBIN?=emacs +BATCH=$(EMACSBIN) -Q --batch $(MAKEL_LOAD_PATH) \ + --eval "(setq load-prefer-newer t)" \ + --eval "(require 'package)" \ + --eval "${MAKEL_SET_ARCHIVES}" \ + --eval "(setq enable-dir-local-variables nil)" \ + --funcall package-initialize + +CURL = curl --fail --silent --show-error --insecure \ + --location --retry 9 --retry-delay 9 \ + --remote-name-all + +# Definition of a utility function `split_with_commas`. +# Argument 1: a space-separated list of filenames +# Return: a comma+space-separated list of filenames +comma:=, +empty:= +space:=$(empty) $(empty) +split_with_commas=$(subst ${space},${comma}${space},$(1)) + + +.PHONY: debug install-elpa-dependencies download-non-elpa-dependencies ci-dependencies check test test-ert test-buttercup lint lint-checkdoc lint-package-lint lint-compile + +makel-version: + @echo "makel v${MAKEL_VERSION}" + +debug: + @echo "MAKEL_LOAD_PATH=${MAKEL_LOAD_PATH}" + @echo "MAKEL_SET_ARCHIVES=${MAKEL_SET_ARCHIVES}" + @${BATCH} --eval "(message \"%S\" package-archives)" + +install-elpa-dependencies: + @if [ -n "${ELPA_DEPENDENCIES}" ]; then \ + echo "# Install ELPA dependencies: $(call split_with_commas,${ELPA_DEPENDENCIES})…"; \ + output=$$(mktemp --tmpdir "makel-ci-dependencies-XXXXX"); \ + $(BATCH) \ + --funcall package-refresh-contents \ + ${patsubst %,--eval "(package-install (quote %))",${ELPA_DEPENDENCIES}} \ + > $${output} 2>&1 || ( cat $${output} && exit 1 ); \ + fi + +download-non-elpa-dependencies: + @if [ -n "${DOWNLOAD_DEPENDENCIES}" ]; then \ + echo "# Download non-ELPA dependencies: $(call split_with_commas,${DOWNLOAD_DEPENDENCIES})…"; \ + $(CURL) $(patsubst %,"%",${DOWNLOAD_DEPENDENCIES}); \ + fi + +ci-dependencies: install-elpa-dependencies download-non-elpa-dependencies + +check: test lint + +#################################### +# Tests +#################################### + +test: test-ert test-buttercup + +#################################### +# Tests - ERT +#################################### + +MAKEL_TEST_ERT_FILES0=$(filter-out %-autoloads.el,${TEST_ERT_FILES}) +MAKEL_TEST_ERT_FILES=$(patsubst %,(load-file \"%\"),${MAKEL_TEST_ERT_FILES0}) + +test-ert: + # Run ert tests from $(call split_with_commas,${MAKEL_TEST_ERT_FILES0})… + @output=$$(mktemp --tmpdir "makel-test-ert-XXXXX"); \ + ${BATCH} \ + $(if ${TEST_ERT_OPTIONS},${TEST_ERT_OPTIONS}) \ + --eval "(progn ${MAKEL_TEST_ERT_FILES} (ert-run-tests-batch-and-exit))" \ + > $${output} 2>&1 || ( cat $${output} && exit 1 ) + +#################################### +# Tests - Buttercup +#################################### + +test-buttercup: + @if [ -n "${TEST_BUTTERCUP_OPTIONS}" ]; then \ + echo "# Run buttercup tests on $(call split_with_commas,${TEST_BUTTERCUP_OPTIONS})"; \ + output=$$(mktemp --tmpdir "makel-test-buttercup-XXXXX"); \ + ${BATCH} \ + --eval "(require 'buttercup)" \ + -f buttercup-run-discover ${TEST_BUTTERCUP_OPTIONS} \ + > $${output} 2>&1 || ( cat $${output} && exit 1 ); \ + fi; + +#################################### +# Lint +#################################### + +lint: lint-checkdoc lint-package-lint lint-compile + +#################################### +# Lint - Checkdoc +#################################### + +MAKEL_LINT_CHECKDOC_FILES0=$(filter-out %-autoloads.el,${LINT_CHECKDOC_FILES}) +MAKEL_LINT_CHECKDOC_FILES=$(patsubst %,\"%\",${MAKEL_LINT_CHECKDOC_FILES0}) + +# This rule has to work around the fact that checkdoc doesn't throw +# errors, it always succeeds. We thus have to check if checkdoc +# printed anything to decide the exit status of the rule. +lint-checkdoc: + # Run checkdoc on $(call split_with_commas,${MAKEL_LINT_CHECKDOC_FILES0})… + @output=$$(mktemp --tmpdir "makel-lint-checkdoc-XXXXX"); \ + ${BATCH} \ + $(if ${LINT_CHECKDOC_OPTIONS},${LINT_CHECKDOC_OPTIONS}) \ + --eval "(mapcar #'checkdoc-file (list ${MAKEL_LINT_CHECKDOC_FILES}))" \ + > $${output} 2>&1; \ + if [ "$$(stat --printf='%s' $${output})" -eq 0 ]; then \ + exit 0; \ + else \ + cat $${output}; \ + exit 1; \ + fi + +#################################### +# Lint - Package-lint +#################################### + +MAKEL_LINT_PACKAGE_LINT_FILES=$(filter-out %-autoloads.el,${LINT_PACKAGE_LINT_FILES}) + +lint-package-lint: + # Run package-lint on $(call split_with_commas,${MAKEL_LINT_PACKAGE_LINT_FILES})… + @${BATCH} \ + --eval "(require 'package-lint)" \ + $(if ${LINT_PACKAGE_LINT_OPTIONS},${LINT_PACKAGE_LINT_OPTIONS}) \ + --funcall package-lint-batch-and-exit \ + ${MAKEL_LINT_PACKAGE_LINT_FILES} + +#################################### +# Lint - Compilation +#################################### + +MAKEL_LINT_COMPILE_FILES=$(filter-out %-autoloads.el,${LINT_COMPILE_FILES}) + +lint-compile: + # Run byte compilation on $(call split_with_commas,${MAKEL_LINT_COMPILE_FILES})… + @${BATCH} \ + --eval "(setq byte-compile-error-on-warn t)" \ + $(if ${LINT_COMPILE_OPTIONS},${LINT_COMPILE_OPTIONS}) \ + --funcall batch-byte-compile \ + ${MAKEL_LINT_COMPILE_FILES} diff --git a/taskwarrior.el b/taskwarrior.el index 6cce5f0..0025799 100644 --- a/taskwarrior.el +++ b/taskwarrior.el @@ -1,5 +1,29 @@ -;; Frontend for taskwarrior -;; +;;; taskwarrior.el --- An interactive taskwarrior interface -*- lexical-binding: t; -*- + +;; Copyright (C) 2019-2020 Patrick Winter + +;; Author: Patrick Winter +;; Keywords: Tools +;; Url: https://gitlab/winpat/taskwarrior.el +;; Package-requires: ((emacs "26.3")) +;; Version: 0.0.1 + +;; 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 . + +;;; Commentary: + +;; The following todo exist: ;; TODO: Implement modeline indicator for deadline and entries ;; TODO: Update buffer if command modifies the state ;; TODO: Extract "1-1000" id filter into variable @@ -7,6 +31,8 @@ ;; (and why alist-get does not work with strings) ;; TODO: Restore position after taskwarrior-update-buffer is called +;;; Code: + (require 'json) (require 'dash) (require 'transient) @@ -72,7 +98,7 @@ (" M " . taskwarrior-priority-medium-face) (" H " . taskwarrior-priority-high-face))) -(defvar taskwarrior-mode-map nil "Keymap for `taskwarrior-mode'") +(defvar taskwarrior-mode-map nil "Keymap for `taskwarrior-mode'.") (progn (setq taskwarrior-mode-map (make-sparse-keymap)) (define-key taskwarrior-mode-map (kbd "a") 'taskwarrior-add) @@ -96,6 +122,7 @@ (define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-edit-project)) (defun taskwarrior-load-profile (profile) + "Load a predefined taskwarrior PROFILE." (interactive (list (completing-read "Profile: " (-map 'car taskwarrior-profile-alist)))) (let ((filter (cdr (assoc-string profile taskwarrior-profile-alist)))) @@ -105,6 +132,7 @@ (taskwarrior-update-buffer)))) (defun taskwarrior-unmark-task () + "Unmark task." (interactive) (let ((id (taskwarrior-id-at-point))) (if (local-variable-p 'taskwarrior-marks) @@ -119,6 +147,7 @@ (next-line))) (defun taskwarrior-mark-task () + "Mark current task." (interactive) (let ((id (taskwarrior-id-at-point))) (if (local-variable-p 'taskwarrior-marks) @@ -134,32 +163,38 @@ (next-line)))) (defun taskwarrior-open-annotation () + "Open annotation on task." (interactive) (let* ((id (taskwarrior-id-at-point)) (task (taskwarrior-export-task id)) - (annotations (-map (lambda (x) (alist-get 'description x)) (vector-to-list (alist-get 'annotations task)))) + (annotations (-map (lambda (x) (alist-get 'description x)) (taskwarrior-vector-to-list (alist-get 'annotations task)))) (choice (completing-read "Tags: " annotations))) (org-open-link-from-string choice))) (defun taskwarrior-info () + "Display detailed information about task." (interactive) (let* ((id (taskwarrior-id-at-point)) (buf (get-buffer-create "*taskwarrior info*"))) (progn (switch-to-buffer-other-window buf) - (insert (taskwarrior--shell-command "info" "" id))))) + (erase-buffer) + (insert (taskwarrior--shell-command "info" id))))) (defun taskwarrior--parse-created-task-id (output) + "Extract task id from shell OUTPUT of `task add`." (when (string-match "^.*Created task \\([0-9]+\\)\\.*$" output) (message (match-string 1 output)))) (defun taskwarrior--parse-org-link (link) + "Extract 'org-mode' link from LINK." (string-match org-bracket-link-regexp link) (list (match-string 1 link) (match-string 3 link))) (defun taskwarrior-capture (arg) + "Capture a taskwarrior task with content ARG." (interactive "P") (let* ((link (car (cdr (taskwarrior--parse-org-link (org-store-link arg))))) (description (read-from-minibuffer "Description: ")) @@ -168,17 +203,20 @@ (shell-command-to-string (format "task %s annotate %s" id link)))) (defun taskwarrior-id-at-point () + "Get id of task at point." (let ((line (thing-at-point 'line t))) (string-match "^ [0-9]*" line) (string-trim-left (match-string 0 line)))) (defun taskwarrior-reset-filter () + "Reset the currently set filter." (interactive) (progn (setq-local taskwarrior-active-filter nil) (taskwarrior-update-buffer))) (defun taskwarrior-set-filter () + "Set or edit the current filter." (interactive) (let ((new-filter (read-from-minibuffer "Filter: " taskwarrior-active-filter))) (progn @@ -186,6 +224,7 @@ (taskwarrior-update-buffer)))) (defun taskwarrior--shell-command (command &optional filter modifications miscellaneous confirm) + "Run a taskwarrior COMMAND with specified FILTER MODIFICATIONS MISCELLANEOUS CONFIRM." (let* ((confirmation (if confirm (concat "echo " confirm " |") "")) (cmd (format "%s task %s %s %s %s" (or confirmation "") @@ -197,18 +236,19 @@ (message cmd) (shell-command-to-string cmd)))) -(defun vector-to-list (vector) - "Convert a vector to a list" +(defun taskwarrior-vector-to-list (vector) + "Convert a VECTOR to a list." (append vector nil)) (defun taskwarrior--concat-tag-list (tags) + "Concat a list of TAGS in to readable format." (mapconcat (function (lambda (x) (format "+%s" x))) - (vector-to-list tags) + (taskwarrior-vector-to-list tags) " ")) (defun taskwarrior-export (&optional filter) - "Turn task export into the tabulated list entry form" + "Turn task export into the tabulated list entry form filted by FILTER." (let ((filter (concat filter " id.not:0"))) (mapcar (lambda (entry) @@ -220,11 +260,12 @@ (tags (or (taskwarrior--concat-tag-list (alist-get 'tags entry)) "")) (description (format "%s" (alist-get 'description entry)))) `(,id [,id ,urgency ,priority ,annotations ,project ,tags ,description]))) - (vector-to-list + (taskwarrior-vector-to-list (json-read-from-string (taskwarrior--shell-command "export" filter)))))) (defun taskwarrior-update-buffer () + "Update the taskwarrior buffer." (interactive) (progn (setq tabulated-list-entries @@ -234,15 +275,16 @@ (tabulated-list-print t))) (defun taskwarrior-export-task (id) - (let ((task (vector-to-list + "Export task with ID." + (let ((task (taskwarrior-vector-to-list (json-read-from-string (taskwarrior--shell-command "export" (concat "id:" id)))))) (if (< (length task) 1) - (error "Seems like two task have the same id.") + (error "Seems like two task have the same id") (car task)))) (defun taskwarrior--change-attribute (attribute) - "Change an attribute of a task" + "Change an ATTRIBUTE of a task." (let* ((prefix (concat attribute ":")) (id (taskwarrior-id-at-point)) (task (taskwarrior-export-task id)) @@ -252,11 +294,12 @@ (taskwarrior--mutable-shell-command "modify" id (concat prefix quoted-value)))) (defun taskwarrior-edit-tags () + "Edit tags on task." (interactive) (let* ((id (taskwarrior-id-at-point)) (task (taskwarrior-export-task id)) (options (split-string (shell-command-to-string "task _tags") "\n")) - (old (vector-to-list (alist-get 'tags task))) + (old (taskwarrior-vector-to-list (alist-get 'tags task))) (current-tags (mapconcat 'identity old " ")) (new (split-string (completing-read "Tags: " options nil nil current-tags) " ")) (added-tags (mapconcat @@ -268,7 +311,7 @@ (taskwarrior--mutable-shell-command "modify" id (concat added-tags " " removed-tags)))) (defun taskwarrior-edit-project () - "Change the project of a task" + "Change the project of a task." (interactive) (let* ((id (taskwarrior-id-at-point)) (task (taskwarrior-export-task id)) @@ -278,48 +321,55 @@ (taskwarrior--mutable-shell-command "modify" id (concat "project:" new)))) (defun taskwarrior-change-description () - "Change the description of a task" + "Change the description of a task." (interactive) (taskwarrior--change-attribute "description")) (defun taskwarrior-edit-priority () - "Change the priority of a task" + "Change the priority of a task." (interactive) (let* ((id (taskwarrior-id-at-point)) (options '("" "H" "M" "L")) (new (completing-read "Priority: " options))) (taskwarrior--mutable-shell-command "modify" id (concat "priority:" new)))) + + (defun taskwarrior-add (description) + "Add new task with DESCRIPTION." (interactive "sDescription: ") (progn (taskwarrior--add description) (taskwarrior--revert-buffer))) (defun taskwarrior--add (description) + "Add new task with DESCRIPTION." (let ((output (taskwarrior--shell-command "add" "" description))) (when (string-match "Created task \\([[:digit:]]+\\)." output) (match-string 1 output)))) (defun taskwarrior-mark-p () - "Whether there are any marked tasks" + "Whether there are any marked tasks." (and (boundp 'taskwarrior-marks) (> (length taskwarrior-marks) 0))) (defun taskwarrior-delete () + "Delete task at point." (interactive) (taskwarrior-multi-action 'taskwarrior--delete "Delete?")) (defun taskwarrior--delete (id) - "Delete task with id." + "Delete task with ID." (taskwarrior--mutable-shell-command "delete" id "" "" "yes")) (defun taskwarrior-done () + "Mark task at point as done." (interactive) (taskwarrior-multi-action 'taskwarrior--done "Done?")) (defun taskwarrior-multi-action (action confirmation-text) + "Run a ACTION after processing the CONFIRMATION-TEXT." (when (yes-or-no-p confirmation-text) (if (taskwarrior-mark-p) (dolist (id taskwarrior-marks) @@ -328,11 +378,11 @@ (funcall action id))))) (defun taskwarrior--done (id) - "Mark task as done." + "Mark task with ID as done." (taskwarrior--mutable-shell-command "done" id)) (defun taskwarrior-annotate (annotation) - "Delete current task." + "Add ANNOTATION to task at point." (interactive "sAnnotation: ") (let ((id (taskwarrior-id-at-point))) (taskwarrior--mutable-shell-command "annotate" id annotation))) @@ -344,13 +394,14 @@ (goto-line line-number))) (defun taskwarrior--mutable-shell-command (command &optional filter modifications misc confirm) - "Run shell command and restore taskwarrior buffer." + "Run shell COMMAND with FILTER MODIFICATIONS MISC and CONFIRM." (let ((line-number (line-number-at-pos))) (taskwarrior--shell-command command filter modifications misc confirm) (taskwarrior-update-buffer) (goto-line line-number))) (defun taskwarrior--urgency-predicate (A B) + "Compare urgency of task A to task B." (let ((a (aref (cadr A) 1)) (b (aref (cadr B) 1))) (> @@ -377,8 +428,7 @@ ;;; Externally visible functions ;;;###autoload (defun taskwarrior () - "Open the taskwarrior buffer. If one already exists, bring it to -the front and focus it. Otherwise, create one and load the data." + "Open the taskwarrior buffer. If one already exists, bring it to the front and focus it. Otherwise, create one and load the data." (interactive) (let* ((buf (get-buffer-create taskwarrior-buffer-name))) (progn @@ -389,18 +439,22 @@ the front and focus it. Otherwise, create one and load the data." (hl-line-mode)))) (defun taskwarrior-set-due () + "Set due date on task." (interactive) (taskwarrior--change-attribute "due")) (defun taskwarrior-set-scheduled () + "Set schedule date on task at point." (interactive) (taskwarrior--change-attribute "scheduled")) (defun taskwarrior-set-wait () + "Set wait date on task at point." (interactive) (taskwarrior--change-attribute "wait")) (defun taskwarrior-set-untl () + "Set until date on task at point." (interactive) (taskwarrior--change-attribute "until")) @@ -411,3 +465,6 @@ the front and focus it. Otherwise, create one and load the data." ("s" "scheduled" taskwarrior-set-scheduled) ("w" "wait" taskwarrior-set-wait) ("u" "until" taskwarrior-set-untl)]]) + +(provide 'taskwarrior) +;;; taskwarrior.el ends here diff --git a/taskwarrior-test.el b/test/taskwarrior-test.el similarity index 65% rename from taskwarrior-test.el rename to test/taskwarrior-test.el index d03ff30..c676f84 100644 --- a/taskwarrior-test.el +++ b/test/taskwarrior-test.el @@ -1,17 +1,22 @@ -;;; taskwarrior-test.el --- taskwarrior unit tests +;;; taskwarrior-test.el --- Taskwarrior test suite -*- lexical-binding: t; -*- + +;; Copyright (C) 2019-2020 Patrick Winter +;; +;; License: GPLv3 ;;; Commentary: -;; Run through make target `m̀ake test` - ;;; Code: -(require 'ert) +(require 'taskwarrior) -(ert-deftest taskwarrior-add-task-test () +(ert-deftest taskwarrior-add-task () "Ensure that special characters such as quotes and parens are properly escaped when adding new tasks" (let* ((task-id (taskwarrior--add "project:ert +emacs \"Write test suite for taskwarrior.el (using ERT)\"")) (task (taskwarrior-export-task task-id))) (should (string= (alist-get 'project task) "ert")) (should (string= (aref (alist-get 'tags task) 0) "emacs")) (should (string= (alist-get 'description task) "Write test suite for taskwarrior.el (using ERT)")))) + +(provide 'taskwarrior-test) +;;; taskwarrior-test.el ends here