Implement record-incomplete check, extensible verification framework

This commit is contained in:
contrapunctus 2022-01-12 02:51:37 +05:30
parent 806894fc5a
commit d33c1d0609
3 changed files with 171 additions and 56 deletions

View File

@ -835,14 +835,18 @@ Command to delete the interval currently being recorded. (bind to 'k')
** fix handling of tagged alist group values :bug: ** fix handling of tagged alist group values :bug:
** put each list element on its own line if length of list exceeds fill-column :feature: ** put each list element on its own line if length of list exceeds fill-column :feature:
* verify command [16%] :feature: * verify command [33%] :feature:
1. [X] Iterate over errors
2. [ ] Next/previous error
3. [ ] Quit iteration
4. [ ] Action to fix errors
With different checks, for different levels of speed/thoroughness - With different checks, for different levels of speed/thoroughness -
1. [X] (plist group) Sequence of plist group dates 1. [X] (plist group) Sequence of plist group dates
2. [ ] Check that every record (except the last) has a =:stop= 2. [X] Check that every record (except the last) has a =:stop=
3. [ ] Intersecting timestamps 3. [ ] Check that every record's =:stop= is greater than or equal to the last one's =:start=
4. [ ] Sequence of records 4. [ ] (plist group) Midnight spanning interval check (first and last intervals)
5. [ ] (plist group) Midnight spanning interval check (first and last intervals) 5. [ ] (plist group) Check that plist timestamps have the correct date. Only applicable [[dates-in-timestamps][as long as they have a date.]]
6. [ ] (plist group) Check that plist timestamps have the correct date. Only applicable [[dates-in-timestamps][as long as they have a date.]]
There are two approaches I can think of - There are two approaches I can think of -
1. an interactive Emacs command like =checkdoc= 1. an interactive Emacs command like =checkdoc=

View File

@ -1037,9 +1037,12 @@ hash table values must be in chronological order.")
;; setup-file-watch:1 ends here ;; setup-file-watch:1 ends here
;; [[file:chronometrist.org::*edit-backend][edit-backend:1]] ;; [[file:chronometrist.org::*edit-backend][edit-backend:1]]
(cl-defmethod chronometrist-edit-backend ((backend chronometrist-file-backend-mixin)) (cl-defmethod chronometrist-edit-backend ((backend chronometrist-file-backend-mixin) &key position)
(find-file-other-window (chronometrist-backend-file backend)) (with-slots (file) backend
(goto-char (point-max))) (let ((buffer (find-file-noselect file)))
(with-current-buffer buffer
(goto-char (or position (point-max))))
(pop-to-buffer buffer))))
;; edit-backend:1 ends here ;; edit-backend:1 ends here
;; [[file:chronometrist.org::*initialize-instance][initialize-instance:1]] ;; [[file:chronometrist.org::*initialize-instance][initialize-instance:1]]
@ -1552,17 +1555,18 @@ This is meant to be run in `chronometrist-file' when using an s-expression backe
(defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin) (defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin)
((extension :initform "plg" ((extension :initform "plg"
:accessor chronometrist-backend-ext :accessor chronometrist-backend-ext
:custom 'string))) :custom 'string)
(verify-functions :initform '(chronometrist-verify-backend-group-order
chronometrist-verify-backend-record-incomplete)
:accessor chronometrist-backend-verify-functions-alist)))
(chronometrist-register-backend (chronometrist-register-backend :plist-group "Store records as plists grouped by date."
:plist-group "Store records as plists grouped by date." (make-instance 'chronometrist-plist-group-backend :path chronometrist-file))
(make-instance 'chronometrist-plist-group-backend
:path chronometrist-file))
;; backend:1 ends here ;; backend:1 ends here
;; [[file:chronometrist.org::*backward-read-sexp][backward-read-sexp:1]] ;; [[file:chronometrist.org::*backward-read-sexp][backward-read-sexp:1]]
(defun chronometrist-backward-read-sexp (buffer) (defun chronometrist-backward-read-sexp (buffer)
(backward-list) (backward-sexp)
(save-excursion (read buffer))) (save-excursion (read buffer)))
;; backward-read-sexp:1 ends here ;; backward-read-sexp:1 ends here
@ -1770,32 +1774,81 @@ Return value is either a list in the form
;; on-remove:1 ends here ;; on-remove:1 ends here
;; [[file:chronometrist.org::*verify][verify:1]] ;; [[file:chronometrist.org::*verify][verify:1]]
(cl-defmethod chronometrist-verify-backend ((backend chronometrist-plist-group-backend)) (defclass chronometrist-verify-error ()
(with-slots (file hash-table) backend ((format-string :initarg :format-string)
;; incorrectly ordered groups check (format-args :initarg :format-args)
(error-location
:initform nil
:initarg :error-location
:documentation "Position of error. For file backends, the value of `point'."))
:documentation "docstring")
(cl-defmethod chronometrist-verify-backend-group-order ((backend chronometrist-plist-group-backend))
"Check BACKEND for incorrectly-ordered groups.
Return a list of `chronometrist-verify-error' objects, or nil if
there were no errors."
(with-slots (file) backend
(chronometrist-loop-sexp-file for group in file (chronometrist-loop-sexp-file for group in file
with old-iso with old-unix with new-iso with new-unix ;; dates with old-iso with old-unix with new-iso with new-unix ;; dates
do (setq new-iso (cl-first group) do (setq new-iso (cl-first group)
new-unix (parse-iso8601-time-string new-iso)) new-unix (parse-iso8601-time-string new-iso))
when (and old-unix when (and old-unix
(time-less-p old-unix new-unix)) (time-less-p old-unix new-unix))
do (let ((line (line-number-at-pos))) collect
(message "%S appears before %S" new-iso old-iso) (make-instance 'chronometrist-verify-error
(cl-return line)) :format-string "%S appears before %S"
else do (setq old-iso new-iso old-unix new-unix) :format-args (list new-iso old-iso)
finally return "Yay, no errors! (...that I could find 💀)"))) :error-location (point))
do (setq old-iso new-iso
old-unix new-unix))))
(cl-defmethod chronometrist-verify-backend-record-incomplete ((backend chronometrist-plist-group-backend))
"Check if records in BACKEND are complete.
All records except the most recent one must have a :stop key.
Return a list of `chronometrist-verify-error' objects, or nil if
there were no errors."
(with-slots (file) backend
(chronometrist-sexp-in-file file
(goto-char (point-max))
(cl-loop with sexp with first = t
while (not (bobp))
;; do (message "position: %s" (point))
when (not sexp) do (down-list -1) ;; initial move
do (setq sexp (chronometrist-backward-read-sexp (current-buffer)))
;; moving between groups
when (stringp sexp)
do (up-list -1)
(unless (progn (backward-sexp) (bobp)) ;; termination check
(forward-sexp)
(down-list -1))
else when (or (not (plist-get sexp :start))
(and (not first)
(or (not (plist-get sexp :start))
(not (plist-get sexp :stop)))))
collect
(make-instance 'chronometrist-verify-error
:format-string "Plist does not contain :start and/or :stop - %S"
:format-args (list sexp)
:error-location (point))
when first do (setq first nil)))))
(cl-defmethod chronometrist-verify-backend ((backend chronometrist-plist-group-backend))
(with-slots (verify-functions) backend
(cl-loop for fn in verify-functions
when (funcall fn backend) append it)))
;; TODO - with prefix arg, prompt (`completing-read-multiple') to select tests to run
(defun chronometrist-verify () (defun chronometrist-verify ()
(interactive) (interactive)
(let* ((backend (chronometrist-active-backend)) (let* ((backend (chronometrist-active-backend))
(line (chronometrist-verify-backend backend)) (errors (chronometrist-verify-backend backend)))
(file (chronometrist-backend-file backend)) (if errors
(buffer (find-file-noselect file))) (cl-loop for error in errors do
(when line (with-slots (format-string format-args error-location) error
(with-current-buffer buffer (chronometrist-edit-backend backend :position error-location)
(goto-char (point-min)) (apply #'message format-string format-args)))
(forward-line (1- line))) (message "Yay, no errors! (...that I could find 💀)"))))
(pop-to-buffer buffer))))
;; verify:1 ends here ;; verify:1 ends here
;; [[file:chronometrist.org::*latest-record][latest-record:1]] ;; [[file:chronometrist.org::*latest-record][latest-record:1]]

View File

@ -1683,9 +1683,12 @@ These can be implemented in terms of the minimal protocol above.
**** edit-backend :method: **** edit-backend :method:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-edit-backend ((backend chronometrist-file-backend-mixin)) (cl-defmethod chronometrist-edit-backend ((backend chronometrist-file-backend-mixin) &key position)
(find-file-other-window (chronometrist-backend-file backend)) (with-slots (file) backend
(goto-char (point-max))) (let ((buffer (find-file-noselect file)))
(with-current-buffer buffer
(goto-char (or position (point-max))))
(pop-to-buffer buffer))))
#+END_SRC #+END_SRC
**** initialize-instance :method: **** initialize-instance :method:
@ -2457,18 +2460,19 @@ Concerns specific to the plist group backend -
(defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin) (defclass chronometrist-plist-group-backend (chronometrist-elisp-sexp-backend chronometrist-file-backend-mixin)
((extension :initform "plg" ((extension :initform "plg"
:accessor chronometrist-backend-ext :accessor chronometrist-backend-ext
:custom 'string))) :custom 'string)
(verify-functions :initform '(chronometrist-verify-backend-group-order
chronometrist-verify-backend-record-incomplete)
:accessor chronometrist-backend-verify-functions-alist)))
(chronometrist-register-backend (chronometrist-register-backend :plist-group "Store records as plists grouped by date."
:plist-group "Store records as plists grouped by date." (make-instance 'chronometrist-plist-group-backend :path chronometrist-file))
(make-instance 'chronometrist-plist-group-backend
:path chronometrist-file))
#+END_SRC #+END_SRC
**** backward-read-sexp :reader: **** backward-read-sexp :reader:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun chronometrist-backward-read-sexp (buffer) (defun chronometrist-backward-read-sexp (buffer)
(backward-list) (backward-sexp)
(save-excursion (read buffer))) (save-excursion (read buffer)))
#+END_SRC #+END_SRC
@ -2702,33 +2706,87 @@ Return value is either a list in the form
#+END_SRC #+END_SRC
**** verify :reader:method: **** verify :reader:method:
1. Each =verify-function= checks the backend independently, each for a specific issue
2. =verify-backend= collects errors from =verify-functions=
3. =verify= walks through errors, displaying the error message based on the format-string and jumping to the position of each error
1. Use =edit-backend= to display the backend, rather than accessing slots. Extend =edit-backend= to take an optional =position= argument.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(cl-defmethod chronometrist-verify-backend ((backend chronometrist-plist-group-backend)) (defclass chronometrist-verify-error ()
(with-slots (file hash-table) backend ((format-string :initarg :format-string)
;; incorrectly ordered groups check (format-args :initarg :format-args)
(error-location
:initform nil
:initarg :error-location
:documentation "Position of error. For file backends, the value of `point'."))
:documentation "docstring")
(cl-defmethod chronometrist-verify-backend-group-order ((backend chronometrist-plist-group-backend))
"Check BACKEND for incorrectly-ordered groups.
Return a list of `chronometrist-verify-error' objects, or nil if
there were no errors."
(with-slots (file) backend
(chronometrist-loop-sexp-file for group in file (chronometrist-loop-sexp-file for group in file
with old-iso with old-unix with new-iso with new-unix ;; dates with old-iso with old-unix with new-iso with new-unix ;; dates
do (setq new-iso (cl-first group) do (setq new-iso (cl-first group)
new-unix (parse-iso8601-time-string new-iso)) new-unix (parse-iso8601-time-string new-iso))
when (and old-unix when (and old-unix
(time-less-p old-unix new-unix)) (time-less-p old-unix new-unix))
do (let ((line (line-number-at-pos))) collect
(message "%S appears before %S" new-iso old-iso) (make-instance 'chronometrist-verify-error
(cl-return line)) :format-string "%S appears before %S"
else do (setq old-iso new-iso old-unix new-unix) :format-args (list new-iso old-iso)
finally return "Yay, no errors! (...that I could find 💀)"))) :error-location (point))
do (setq old-iso new-iso
old-unix new-unix))))
(cl-defmethod chronometrist-verify-backend-record-incomplete ((backend chronometrist-plist-group-backend))
"Check if records in BACKEND are complete.
All records except the most recent one must have a :stop key.
Return a list of `chronometrist-verify-error' objects, or nil if
there were no errors."
(with-slots (file) backend
(chronometrist-sexp-in-file file
(goto-char (point-max))
(cl-loop with sexp with first = t
while (not (bobp))
;; do (message "position: %s" (point))
when (not sexp) do (down-list -1) ;; initial move
do (setq sexp (chronometrist-backward-read-sexp (current-buffer)))
;; moving between groups
when (stringp sexp)
do (up-list -1)
(unless (progn (backward-sexp) (bobp)) ;; termination check
(forward-sexp)
(down-list -1))
else when (or (not (plist-get sexp :start))
(and (not first)
(or (not (plist-get sexp :start))
(not (plist-get sexp :stop)))))
collect
(make-instance 'chronometrist-verify-error
:format-string "Plist does not contain :start and/or :stop - %S"
:format-args (list sexp)
:error-location (point))
when first do (setq first nil)))))
(cl-defmethod chronometrist-verify-backend ((backend chronometrist-plist-group-backend))
(with-slots (verify-functions) backend
(cl-loop for fn in verify-functions
when (funcall fn backend) append it)))
;; TODO - with prefix arg, prompt (`completing-read-multiple') to select tests to run
(defun chronometrist-verify () (defun chronometrist-verify ()
(interactive) (interactive)
(let* ((backend (chronometrist-active-backend)) (let* ((backend (chronometrist-active-backend))
(line (chronometrist-verify-backend backend)) (errors (chronometrist-verify-backend backend)))
(file (chronometrist-backend-file backend)) (if errors
(buffer (find-file-noselect file))) (cl-loop for error in errors do
(when line (with-slots (format-string format-args error-location) error
(with-current-buffer buffer (chronometrist-edit-backend backend :position error-location)
(goto-char (point-min)) (apply #'message format-string format-args)))
(forward-line (1- line))) (message "Yay, no errors! (...that I could find 💀)"))))
(pop-to-buffer buffer))))
#+END_SRC #+END_SRC
**** extended protocol **** extended protocol