Hello everybody,
At Friday's inaugural Calgary Lisp user group meeting, Michael Beauregard suggested a feature to SLIME that would let you take an expression-result pair from REPL interaction and easily add it as a new regression test to a test suite. Here is some code that does something like that:
(defvar *slime-last-repl-input*)
(defvar *slime-last-repl-result*)
(defvar *slime-last-repl-input-matches-last-result* nil) ;; set to T if last input expression returned normally and it is safe to save the input-result pair as a unit test, nil otherwise
(defvar *slime-regression-test-file* nil "Pathname of file containing test cases to which the next test case will be appended to.")
(defvar *slime-regression-test-framework* nil "The unit test framework should be a function that takes as arguments the input and expected result of the unit test as strings and produces an object that is suitable to be serialized and appended to a file containing unit tests for that particular framework.")
(defun slime-MIT-regression-testing-framework (input result)
(princ "(deftest " (current-buffer))
(princ (gensym) (current-buffer))
(newline)
(princ "(multiple-value-list " (current-buffer))
(princ input (current-buffer))
(princ ")" (current-buffer))
(newline)
(princ result (current-buffer))
(princ ")" (current-buffer)))
(defun slime-save-last-input-as-regression-test ()
"When called, takes the last REPL expression sent to Lisp by SLIME and the output it returns, and adds them as a unit test to the unit test file specified by *slime-regression-test-file*. If the last REPL command resulted in an error, does nothing. The specific brand of regression testing framework you're using is controlled by *slime-regression-test-framework*."
(interactive)
(unless *slime-regression-test-file*
(call-interactively #'slime-set-regression-test-file))
(when *slime-last-repl-input-matches-last-result*
(with-temp-buffer
(lisp-mode)
(newline)
(let* ((res *slime-last-repl-result*)
(results (cons 'list (cond ((equal :values (car res)) (second res))
((equal :present (car res)) (mapcar #'car (second res)))
(t "nil")))))
(funcall *slime-regression-test-framework* *slime-last-repl-input* results))
(newline)
(indent-region (point-min) (point-max) nil)
(append-to-file (point-min) (point-max) *slime-regression-test-file*))))
(defun slime-set-regression-test-file (file)
(interactive "FRegression test file: ")
(setf *slime-regression-test-file* file))
(defadvice slime-repl-eval-string (before regression-testing-string-capturer (string))
"Get the string passed to slime-repl-eval-string before it gets mangled."
(setf *slime-last-repl-input* (copy-seq string)))
;;; need to redefine slime-repl-eval-string
(defun slime-repl-eval-string (string)
(slime-rex ()
((list 'swank:listener-eval string) (slime-lisp-package))
((:ok result)
;;; modified code be here
(setf *slime-last-repl-result* result
*slime-last-repl-input-matches-last-result* t)
;;; end modified code
(with-current-buffer (slime-output-buffer)
(slime-repl-insert-prompt result)))
((:abort)
;;; modified code be here
(setf *slime-last-repl-input-matches-last-result* nil)
;;; end modified code
(slime-repl-show-abort))))
Comments? I don't use regression testing tools very much myself, so the first thing I want to ask, is this approach going to work for the commonly used CL testing frameworks, or is something more flexible necessary? If this were to become part of SLIME, the big question is, what key should slime-save-last-input-as-regression-test be bound to? Having just the last repl-expression-result pair is probably good enough, but is there anyone wanting a history of expression-result pair interactions and a nice colorful-buffer way of choosing which ones to save?
Vladimir