diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..a61c5d3 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,35 @@ +codecov: + notify: + # will no delay sending notifications until all ci is finished + require_ci_to_pass: no + +coverage: + precision: 2 + round: down + range: "65...90" + + status: + project: + default: + # the amount that coverage can drop while still posting a success + threshold: 1% + patch: no + changes: no + +comment: + layout: diff + behavior: default + require_changes: false + +ignore: + - ".git" + - "*.yml" + - "*.json" + - "*.md" + - "*.mk" + - "*.txt" + + # ignore folders and all its contents + - ".cask/.*" + - ".github/.*" + - "test/.*" diff --git a/.ert-runner b/.ert-runner deleted file mode 100644 index cfdfc68..0000000 --- a/.ert-runner +++ /dev/null @@ -1 +0,0 @@ --l esup-child.el \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea085bd..0dadeea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,3 +75,12 @@ jobs: - name: After Success Reporting if: success() run: git log --format=fuller -5 + + - name: Upload Code Coverage Report + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + yml: ./.codecov.yml + name: codecov-umbrella + flags: unittests + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 1096153..ede0eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ # Directories to ignore (do not add trailing '/'s, they skip symlinks). /.cask + +# Code coverage report. +coverage-final.json diff --git a/Cask b/Cask index f2eef8c..e0942e9 100644 --- a/Cask +++ b/Cask @@ -8,6 +8,7 @@ (development (depends-on "dash") - (depends-on "ert-runner") + (depends-on "buttercup") + (depends-on "undercover") (depends-on "el-mock") (depends-on "noflet")) diff --git a/Makefile b/Makefile index 624d22e..16e4b38 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,15 @@ -## Sane defaults - -SHELL := $(shell which bash) -ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) - -EMACS ?= emacs -CASK ?= cask - -EMACSFLAGS ?= -TESTFLAGS ?= --reporter ert+duration - -VERSION = undefined - -.DEFAULT_GOAL = build - -## File lists - -SRCS = esup-child.el esup.el -OBJS = $(SRCS:.el=.elc) - -## Internal variables - -EMACSBATCH = $(EMACS) -Q --batch -L . $(EMACSFLAGS) -RUNEMACS = - -## Program availability - -HAVE_CASK := $(shell sh -c "command -v $(CASK)") -ifndef HAVE_CASK -$(warning "$(CASK) is not available. Please run make help") -RUNEMACS = $(EMACSBATCH) -else -RUNEMACS = $(CASK) exec $(EMACSBATCH) -VERSION = $(shell $(CASK) version) -endif +include default.mk %.elc: %.el - @$(RUNEMACS) --eval '(setq byte-compile-error-on-warn t)' -f batch-byte-compile $< + @printf "Compiling $<\n" + @$(RUNEMACS) --eval '(setq byte-compile-error-on-warn t)' \ + -f batch-byte-compile $< ## Public targets .PHONY: .title .title: - $(info Esup $(VERSION)) + @echo Esup $(VERSION) .PHONY: init init: Cask @@ -49,7 +17,7 @@ init: Cask .PHONY: test test: - @$(CASK) exec ert-runner $(TESTFLAGS) + @$(CASK) exec buttercup $(TESTFLAGS) .PHONY: build build: $(OBJS) @@ -57,7 +25,7 @@ build: $(OBJS) .PHONY: clean clean: $(info Remove all byte compiled Elisp files...) - @$(RM) -f $(OBJS) + @$(CASK) clean-elc .PHONY: help help: .title diff --git a/default.mk b/default.mk new file mode 100644 index 0000000..ec5c80d --- /dev/null +++ b/default.mk @@ -0,0 +1,26 @@ +# Run “make build” by default +.DEFAULT_GOAL = build + +EMACS ?= emacs +CASK ?= cask + +EMACSFLAGS ?= +TESTFLAGS ?= -L . + +EMACSBATCH = $(EMACS) -Q --batch -L . $(EMACSFLAGS) +RUNEMACS = + +# Program availability +HAVE_CASK := $(shell sh -c "command -v $(CASK)") +ifndef HAVE_CASK +$(warning "$(CASK) is not available. Please run make help") +RUNEMACS = $(EMACSBATCH) +else +RUNEMACS = $(CASK) exec $(EMACSBATCH) +endif + +VERSION="$(shell sed -nre '/^;; Version:/ { s/^;; Version:[ \t]+//; p }' esup.el)" + +# File lists +SRCS = esup-child.el esup.el +OBJS = $(SRCS:.el=.elc) diff --git a/test/esup-test.el b/test/esup-test.el deleted file mode 100644 index 110889c..0000000 --- a/test/esup-test.el +++ /dev/null @@ -1,510 +0,0 @@ -;;; esup-test.el --- tests for esup -*- lexical-binding: t -*- - -;;; Commentary: - -;; Tests for esup-child.el and esup.el functionality. - -;;; Code: - -(eval-when-compile - (require 'cl-lib)) - -(require 'esup-child) -(require 'noflet) -(require 'el-mock) - -(defconst esup-test/fake-port -1) - -(defvar esup-debug-enabled nil - "Enable debug messages for the test utilities. -Also sends all esup-child log messages to stdout.") - -(defun esup-debug-test (str &rest format-args) - "Output STR with FORMAT-ARGS if debug-mode is t." - (when esup-debug-enabled - (apply 'message str format-args))) - -;; TODO(jschaf): There's a bug when using the same mock directory that -;; causes it to bleed into other tests. For a quick fix, don't use -;; the same directory. - -(ert-deftest esup-child-run__loads-file () - (with-esup-mock - '(:load-path ("/fake") - :files (("/fake/foof.el" . "(progn 'qux)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "/fake/foof.el" -1) - (list (esup-result - :file "/fake/foof.el" - :expression-string "(progn 'qux)" - :start-point 1 - :end-point 13)))))) - - -(ert-deftest esup-child-run__handles-empty-file() - (with-esup-mock - '(:load-path ("/fake") - :files (("/fake/foo-bar.el" . ""))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "/fake/foo-bar.el" -1) - (list))))) - -(ert-deftest esup-child-run__counts-gc() - (with-esup-mock - '(:load-path ("/fake") - :files - (("/fake/bar-qux.el" . "(progn (garbage-collect) (garbage-collect))"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "/fake/bar-qux.el" esup-test/fake-port) - (list (make-esup-result - "/fake/bar-qux.el" - "(progn (garbage-collect) (garbage-collect))" - :gc-number 2)))))) - -(ert-deftest esup-child-run__uses-load-path() - (with-esup-mock - '(:load-path ("/fake1" "/fake2") - :files (("/fake2/qux.el" . "(require 'baz) (progn 'qux)") - ("/fake2/baz.el" . "(progn 'baz)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "qux.el" esup-test/fake-port) - (list - (make-esup-result "/fake2/baz.el" "(progn 'baz)") - (make-esup-result "/fake2/qux.el" "(progn 'qux)" - :start-point 16 :end-point 28)))))) - -(ert-deftest esup-child-run__steps-into-requires() - (with-esup-mock - '(:load-path ("/fake3") - :files (("/fake3/qux.el" . "(require 'baz)") - ("/fake3/baz.el" . "(progn 'baz)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "qux.el" esup-test/fake-port) - (list - (make-esup-result "/fake3/baz.el" "(progn 'baz)")))))) - -(ert-deftest esup-child-run__handles-dynamic-docstring() - (with-esup-mock - '(:load-path ("/fake1") - :files (("/fake1/qux.el" . "#@2 A\n(defvar var 1)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "qux.el" esup-test/fake-port) - (list - (make-esup-result "/fake1/qux.el" "(defvar var 1)" - :start-point 7 :end-point 21 :line-number 2)))))) - -(ert-deftest esup-child-run__respects-require-level-of-1 () - (with-esup-mock - '(:load-path ("/fake8") - :files (("/fake8/a.el" . "(require 'c)") - ("/fake8/c.el" . "(require 'd)") - ("/fake8/d.el" . "(progn 'd)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (let ((depth 1)) - (esup-child-run "a.el" esup-test/fake-port depth)) - (list - (make-esup-result "/fake8/c.el" "(require 'd)")))))) - -(ert-deftest esup-child-run__respects-require-level-of-2 () - (with-esup-mock - '(:load-path ("/fake9") - :files (("/fake9/a.el" . "(require 'c)") - ("/fake9/c.el" . "(require 'd)") - ("/fake9/d.el" . "(progn 'd)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (let ((depth 2)) - (esup-child-run "a.el" esup-test/fake-port depth)) - (list - (make-esup-result "/fake9/d.el" "(progn 'd)")))))) - -(ert-deftest esup-child-run__handles_require_with_sexp_filename () - (with-esup-mock - '(:load-path ("/fake10") - :files - (("/fake10/bar.el" . "(require 'core (concat \"/specified/qux/\" \"core\"))") - ("/specified/qux/core.el" . "(progn 'core)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "/fake10/bar.el" esup-test/fake-port) - (list (make-esup-result "/specified/qux/core.el" "(progn 'core)")))))) - -(ert-deftest esup-child-run__doesnt_step_into_already_required_feature() - (with-esup-mock - '(:load-path ("/fake12") - :files (("/fake12/qux.el" . "(require 'baz) (require 'baz)") - ("/fake12/baz.el" . "(progn 'baz) (provide 'baz)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "qux.el" esup-test/fake-port) - (list - (make-esup-result "/fake12/baz.el" "(progn 'baz)") - (make-esup-result "/fake12/baz.el" "(provide 'baz)" - :start-point 14 :end-point 28) - (make-esup-result "/fake12/qux.el" "(require 'baz)" - :start-point 16 :end-point 30)))))) - -(ert-deftest esup-child-run__advises_require() - (with-esup-mock - '(:load-path ("/fake13") - :files (("/fake13/qux.el" . "(defun my-require (feat) (require feat)) (my-require 'baz)") - ("/fake13/baz.el" . "(progn 'baz) (provide 'baz)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "qux.el" esup-test/fake-port) - (list - (make-esup-result "/fake13/qux.el" "(defun my-require (feat) (require feat))") - (make-esup-result "/fake13/baz.el" "(progn 'baz)") - (make-esup-result "/fake13/baz.el" "(provide 'baz)" - :start-point 14 :end-point 28)))))) - -(ert-deftest esup-child-run__advises_load() - (with-esup-mock - '(:load-path ("/fake14") - :files - (("/fake14/qux.el" . "(defun my-load (file) (load file)) (my-load \"baz\")") - ("/fake14/baz.el" . "(progn 'baz) (provide 'baz)"))) - - (should - (esup-results-equal-p - '(:gc-time :exec-time) - (esup-child-run "qux.el" esup-test/fake-port) - (list - (make-esup-result "/fake14/qux.el" "(defun my-load (file) (load file))") - (make-esup-result "/fake14/baz.el" "(progn 'baz)") - (make-esup-result "/fake14/baz.el" "(provide 'baz)" - :start-point 14 :end-point 28)))))) - - -;; Test Utilities -(defun esup-results-equal-p (ignoring-slots a b) - "Compare a list of `esup-results' with an ignore spec." - (and - (eq (length a) (length b)) - (--all? (not (null it)) - (--zip-with (esup-results-single-equal-p - ignoring-slots it other) - a b)))) - -(defun esup-results-single-equal-p (ignoring-slots a b) - "Compare `esup-results' with an ignore spec." - (--all? (not (null it)) - (--map (equal (eieio-oref a it) (eieio-oref b it)) - (esup-test--slots-to-compare ignoring-slots)))) - - -(defun esup-test--slots-to-compare (ignoring-slots) - "Return a list of slots to compare for an `esup-result'." - (-difference (esup-test--all-slots) ignoring-slots)) - -(defun esup-test--all-slots () - "Return a list of all slots for an `esup-result.'" - (--map (intern (concat ":" (symbol-name it))) - (object-slots (make-instance 'esup-result)))) - -(defun esup-test--explain-esup-results-equal-p (ignoring-slots a b) - "Explain why `esup-results-equal-p' returned t or nil." - (pcase a - ;; Actual is nil, but expected is not. - ((guard (and (null a) (not (null b)))) - `(actual is nil but expected ,b)) - - ;; Different types. - ((guard (not (equal (type-of a) (type-of b)))) - `(different-types ,a ,b)) - - ;; A list of esup-results. - ((pred listp) - (esup-test--explain-list-of-esup-results ignoring-slots a b)) - - ;; A single esup-result. - ((app eieio-object-class esup-result) - (esup-test--explain-single-esup-result ignoring-slots a b)) - - ;; Unknown types. - (_ `(unknown-types ,a ,b)))) -(put 'esup-results-equal-p 'ert-explainer - 'esup-test--explain-esup-results-equal-p) - -(defun esup-test--explain-list-of-esup-results (ignoring-slots a b) - (cond - ((not (eq (length a) (length b))) - `(different-lengths ,a ,b)) - (t - ;; Compare each index - (cl-loop - for (actual . expected) in (-zip-pair a b) - for i = 0 then (1+ i) - collect - `(index ,i ,@(esup-test--explain-single-esup-result - ignoring-slots actual expected)))))) - -(defun esup-test--explain-single-esup-result (ignoring-slots a b) - (if (esup-results-single-equal-p ignoring-slots a b) - 'MATCH - ;; Compare each slot - (-non-nil - (cl-loop for slot in (esup-test--all-slots) - collect - (cond - ((-contains? ignoring-slots slot) - `(,slot IGNORED)) - ;; Got a match - ((equal (eieio-oref a slot) (eieio-oref b slot)) - `(,slot MATCHED on ,(eieio-oref a slot))) - ;; Explain the mismatch - (t - `(,slot MISMATCH - actual was ,(eieio-oref b slot) - but expected ,(eieio-oref a slot)))))))) -(put 'esup-results-single-equal-p 'ert-explainer - 'esup-test--explain-single-esup-result) - -(defun make-esup-result (file sexp &rest args) - (apply - #'esup-result - :file file - :expression-string sexp - :end-point (1+ (length sexp)) - args)) - -(defun esup-test-make-locate-file-fn (mock-fs) - (lambda (filename path &optional suffixes predicate) - (esup-debug-test - (concat "starting generated locate-file-fn: " - "filename=%s path=%s suffixes=%s predicate=%s") - filename path suffixes predicate) - (let* ((all-files-no-suffix - (-table-flat 'esup--join-paths (cons "" path) (list filename))) - (all-files - (-table-flat 'concat all-files-no-suffix (cons "" load-suffixes))) - (matching-files-in-mock-fs - (-non-nil - ;; Find files that exist in the mock-fs - (-map - (lambda (path) - (car-safe - (or (assoc path mock-fs) - (assoc (concat "./" path) mock-fs)))) - all-files)))) - (esup-debug-test "searching for file match: matching-files=%s all-files=%s" - matching-files-in-mock-fs all-files) - (car-safe matching-files-in-mock-fs)))) - -(defun esup--join-paths (dir file) - (cond - ((file-name-absolute-p file) file) - ((string= " " dir) file) - (t (concat (file-name-as-directory dir) file)))) - -(defmacro with-esup-mock (props &rest body) - (let ((old-features (-clone features))) - `(let* ((load-path (plist-get ,props :load-path)) - (mock-fs (plist-get ,props :files)) - (locate-fn (esup-test-make-locate-file-fn mock-fs))) - (esup-debug-test "starting with-esup-mock: load-path=%s mock-fs=%s" - load-path mock-fs) - (noflet - ((find-file-noselect - (filename &optional nowarn rawfile wildcards) - (esup-debug-test - (concat - "starting mock find-file-no-select: " - "filename=%s nowarn=%s rawfile=%s wildcards=%s") - filename nowarn rawfile wildcards) - - (let ((mock-file-exists (assoc filename mock-fs)) - (contents (alist-get filename mock-fs))) - (if mock-file-exists - (with-current-buffer (get-buffer-create filename) - (setq-local buffer-file-name filename) - (setq-local buffer-read-only nil) - (insert contents) - (current-buffer)) - (error "Unknown file %s not in mock-fs" filename)))) - - (locate-file - (filename path &optional suffixes predicate) - (esup-debug-test - "starting mock locate-file: filename=%s path=%s suffixes=%s pred=%s" - filename path suffixes predicate) - - (let ((results (funcall locate-fn filename path suffixes predicate))) - (esup-debug-test "locate-file mock returned '%s'" results) - results)) - - (require (feature &optional filename noerror) - (esup-debug-test - "starting mock require: feature=%s filename=%s noerror=%s" - feature filename noerror) - (if filename - (funcall locate-fn filename load-path) - (funcall locate-fn (symbol-name feature) load-path))) - - - ;; Stub out network calls. - (esup-child-init-streams (port)) - (kill-emacs (&optional arg)) - (process-send-string (process string) - (when esup-debug-enabled (message string))) - (process-send-eof (&optional process))) - - ,@body - - (esup-debug-test "test added features %s" - (-difference features ',old-features)) - ;; Reset the features list in case any tests provided features. - (setq features ',old-features) - - ;; Reset the max depth since the tests re-use the same environment. - (when (and (boundp 'esup-child-max-depth) - (not (eq esup-child-max-depth 2))) - (esup-debug-test "Resetting esup-child-max-depth back to 2 from %d" - esup-child-max-depth) - (setq esup-child-max-depth 2)))))) - - -;; Test Utility Tests - -(ert-deftest esup-results-equal-p__empty-list__is-equal () - (should - (esup-results-equal-p '() '() '()))) - -(ert-deftest esup-results-equal-p__single-elem__is-equal () - (should - (esup-results-equal-p - '() - (list (make-esup-result "file" "sexp")) - (list (make-esup-result "file" "sexp"))))) - -(ert-deftest esup-results-equal-p__three-elem__is-equal () - (should - (esup-results-equal-p - '() - (list - (make-esup-result "file1" "sexp1" :start-point 10) - (make-esup-result "file2" "sexp2" :gc-time 20) - (make-esup-result "file3" "sexp3")) - (list - (make-esup-result "file1" "sexp1" :start-point 10) - (make-esup-result "file2" "sexp2" :gc-time 20) - (make-esup-result "file3" "sexp3"))))) - -(ert-deftest esup-results-equal-p__ignoring-gc-time__is-equal () - (should - (esup-results-equal-p - '(:gc-time) - (list (make-esup-result "file" "sexp" :gc-time 30)) - (list (make-esup-result "file" "sexp" :gc-time 50))))) - -(ert-deftest esup-results-equal-p__gc-time-mismatch__is-false () - (should - (not - (esup-results-equal-p - '() - (list (make-esup-result "file" "sexp" :gc-time 30)) - (list (make-esup-result "file" "sexp" :gc-time 50)))))) - -(ert-deftest esup-results-equal-p__single-elem-mismatch__is-false () - (should - (not - (esup-results-equal-p - '() - (list (make-esup-result "file" "sexp1")) - (list (make-esup-result "file" "sexp2")))))) - -(ert-deftest esup-results-single-equal-p__ignoring-no-slots__is-equal () - (should - (esup-results-single-equal-p - '() - (make-esup-result "/fake/file-1.el" "(progn 'file-1)") - (make-esup-result "/fake/file-1.el" "(progn 'file-1)")))) - -(ert-deftest esup-results-single-equal-p__sexp-mismatch__is-false () - (should - (not - (esup-results-single-equal-p - '() - (make-esup-result "/fake/file-1.el" "(progn 'file-1)") - (make-esup-result "/fake/file-1.el" "(progn 'file-2)"))))) - -(ert-deftest esup-results-single-equal-p__ignoring-gc-time__is-equal () - (should - (esup-results-single-equal-p - '(:gc-time) - (esup-result - :file "file" :expression-string "sexp" :end-point 20 :gc-time 444) - (esup-result - :file "file" :expression-string "sexp" :end-point 20 :gc-time 555)))) - -(ert-deftest esup-results-single-equal-p__gc-time-mismatch__is-false () - (should - (not - (esup-results-single-equal-p - '() - (esup-result - :file "file" :expression-string "sexp" :end-point 20 :gc-time 444) - (esup-result - :file "file" :expression-string "sexp" :end-point 20 :gc-time 555))))) - -(ert-deftest esup-results-single-equal-p__sexp-mismatch__is-false () - (should - (not - (esup-results-single-equal-p - '() - (esup-result - :file "file" :expression-string "sexp") - (esup-result - :file "file" :expression-string "sexp2"))))) - -(ert-deftest make-esup-result__no-extra-args__is-same () - (should - (equal - (make-esup-result "file" "sexp") - (esup-result - :file "file" - :expression-string "sexp" - :end-point 5)))) - -(ert-deftest make-esup-result__with-extra-args__is-same () - (should - (equal - (make-esup-result "file" "sexp" :gc-time 20 :exec-time 40) - (esup-result - :file "file" - :expression-string "sexp" - :end-point 5 - :gc-time 20 - :exec-time 40)))) - -(provide 'esup-test) -;;; esup-test.el ends here diff --git a/test/test-esup.el b/test/test-esup.el new file mode 100644 index 0000000..e6642c9 --- /dev/null +++ b/test/test-esup.el @@ -0,0 +1,372 @@ +;;; test-esup.el --- Tests for esup -*- lexical-binding: t -*- + +;;; Commentary: + +;; Tests for esup-child.el and esup.el functionality using `buttercup'. + +;;; Code: + +(require 'buttercup) +(load (concat (file-name-directory (or load-file-name (buffer-file-name) + default-directory)) + "utils.el") nil 'nomessage 'nosuffix) + +(defconst esup-test/fake-port -1) + +;;;; Tests: + +;; TODO(jschaf): There's a bug when using the same mock directory that +;; causes it to bleed into other tests. For a quick fix, don't use +;; the same directory. + +(describe "Child run" + (it "loads file" + (with-esup-mock + '(:load-path ("/fake") + :files (("/fake/foof.el" . "(progn 'qux)"))) + + (should + (esup-results-equal-p + '(:gc-time :exec-time) + (esup-child-run "/fake/foof.el" esup-test/fake-port) + (list (esup-result + :file "/fake/foof.el" + :expression-string "(progn 'qux)" + :start-point 1 + :end-point 13)))))) + + (it "handles empty file" + (with-esup-mock + '(:load-path ("/fake") + :files (("/fake/foo-bar.el" . ""))) + + (should + (esup-results-equal-p + '(:gc-time :exec-time) + (esup-child-run "/fake/foo-bar.el" -1) + (list))))) + + (it "counts gc" + (with-esup-mock + '(:load-path ("/fake") + :files (("/fake/bar-qux.el" . "(progn (garbage-collect) (garbage-collect))"))) + + (should + (esup-results-equal-p + '(:gc-time :exec-time) + (esup-child-run "/fake/bar-qux.el" esup-test/fake-port) + (list (make-esup-result + "/fake/bar-qux.el" + "(progn (garbage-collect) (garbage-collect))" + :gc-number 2)))))) + + (it "uses load-path" + (with-esup-mock + '(:load-path ("/fake1" "/fake2") + :files (("/fake2/qux.el" . "(require 'baz) (progn 'qux)") + ("/fake2/baz.el" . "(progn 'baz)"))) + + (should + (esup-results-equal-p + '(:gc-time :exec-time) + (esup-child-run "qux.el" esup-test/fake-port) + (list + (make-esup-result "/fake2/baz.el" "(progn 'baz)") + (make-esup-result "/fake2/qux.el" "(progn 'qux)" + :start-point 16 :end-point 28))))))) + +;; (ert-deftest esup-child-run__steps-into-requires() +;; (with-esup-mock +;; '(:load-path ("/fake3") +;; :files (("/fake3/qux.el" . "(require 'baz)") +;; ("/fake3/baz.el" . "(progn 'baz)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (esup-child-run "qux.el" esup-test/fake-port) +;; (list +;; (make-esup-result "/fake3/baz.el" "(progn 'baz)")))))) + +;; (ert-deftest esup-child-run__handles-dynamic-docstring() +;; (with-esup-mock +;; '(:load-path ("/fake1") +;; :files (("/fake1/qux.el" . "#@2 A\n(defvar var 1)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (esup-child-run "qux.el" esup-test/fake-port) +;; (list +;; (make-esup-result "/fake1/qux.el" "(defvar var 1)" +;; :start-point 7 :end-point 21 :line-number 2)))))) + +;; (ert-deftest esup-child-run__respects-require-level-of-1 () +;; (with-esup-mock +;; '(:load-path ("/fake8") +;; :files (("/fake8/a.el" . "(require 'c)") +;; ("/fake8/c.el" . "(require 'd)") +;; ("/fake8/d.el" . "(progn 'd)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (let ((depth 1)) +;; (esup-child-run "a.el" esup-test/fake-port depth)) +;; (list +;; (make-esup-result "/fake8/c.el" "(require 'd)")))))) + +;; (ert-deftest esup-child-run__respects-require-level-of-2 () +;; (with-esup-mock +;; '(:load-path ("/fake9") +;; :files (("/fake9/a.el" . "(require 'c)") +;; ("/fake9/c.el" . "(require 'd)") +;; ("/fake9/d.el" . "(progn 'd)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (let ((depth 2)) +;; (esup-child-run "a.el" esup-test/fake-port depth)) +;; (list +;; (make-esup-result "/fake9/d.el" "(progn 'd)")))))) + +;; (ert-deftest esup-child-run__handles_require_with_sexp_filename () +;; (with-esup-mock +;; '(:load-path ("/fake10") +;; :files +;; (("/fake10/bar.el" . "(require 'core (concat \"/specified/qux/\" \"core\"))") +;; ("/specified/qux/core.el" . "(progn 'core)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (esup-child-run "/fake10/bar.el" esup-test/fake-port) +;; (list (make-esup-result "/specified/qux/core.el" "(progn 'core)")))))) + +;; (ert-deftest esup-child-run__doesnt_step_into_already_required_feature() +;; (with-esup-mock +;; '(:load-path ("/fake12") +;; :files (("/fake12/qux.el" . "(require 'baz) (require 'baz)") +;; ("/fake12/baz.el" . "(progn 'baz) (provide 'baz)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (esup-child-run "qux.el" esup-test/fake-port) +;; (list +;; (make-esup-result "/fake12/baz.el" "(progn 'baz)") +;; (make-esup-result "/fake12/baz.el" "(provide 'baz)" +;; :start-point 14 :end-point 28) +;; (make-esup-result "/fake12/qux.el" "(require 'baz)" +;; :start-point 16 :end-point 30)))))) + +;; (ert-deftest esup-child-run__advises_require() +;; (with-esup-mock +;; '(:load-path ("/fake13") +;; :files (("/fake13/qux.el" . "(defun my-require (feat) (require feat)) (my-require 'baz)") +;; ("/fake13/baz.el" . "(progn 'baz) (provide 'baz)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (esup-child-run "qux.el" esup-test/fake-port) +;; (list +;; (make-esup-result "/fake13/qux.el" "(defun my-require (feat) (require feat))") +;; (make-esup-result "/fake13/baz.el" "(progn 'baz)") +;; (make-esup-result "/fake13/baz.el" "(provide 'baz)" +;; :start-point 14 :end-point 28)))))) + +;; (ert-deftest esup-child-run__advises_load() +;; (with-esup-mock +;; '(:load-path ("/fake14") +;; :files +;; (("/fake14/qux.el" . "(defun my-load (file) (load file)) (my-load \"baz\")") +;; ("/fake14/baz.el" . "(progn 'baz) (provide 'baz)"))) + +;; (should +;; (esup-results-equal-p +;; '(:gc-time :exec-time) +;; (esup-child-run "qux.el" esup-test/fake-port) +;; (list +;; (make-esup-result "/fake14/qux.el" "(defun my-load (file) (load file))") +;; (make-esup-result "/fake14/baz.el" "(progn 'baz)") +;; (make-esup-result "/fake14/baz.el" "(provide 'baz)" +;; :start-point 14 :end-point 28)))))) + +;; +;; ;; Test Utilities + +;; (defun esup-test--explain-esup-results-equal-p (ignoring-slots a b) +;; "Explain why `esup-results-equal-p' returned t or nil." +;; (pcase a +;; ;; Actual is nil, but expected is not. +;; ((guard (and (null a) (not (null b)))) +;; `(actual is nil but expected ,b)) + +;; ;; Different types. +;; ((guard (not (equal (type-of a) (type-of b)))) +;; `(different-types ,a ,b)) + +;; ;; A list of esup-results. +;; ((pred listp) +;; (esup-test--explain-list-of-esup-results ignoring-slots a b)) + +;; ;; A single esup-result. +;; ((app eieio-object-class esup-result) +;; (esup-test--explain-single-esup-result ignoring-slots a b)) + +;; ;; Unknown types. +;; (_ `(unknown-types ,a ,b)))) +;; (put 'esup-results-equal-p 'ert-explainer +;; 'esup-test--explain-esup-results-equal-p) + +;; (defun esup-test--explain-list-of-esup-results (ignoring-slots a b) +;; (cond +;; ((not (eq (length a) (length b))) +;; `(different-lengths ,a ,b)) +;; (t +;; ;; Compare each index +;; (cl-loop +;; for (actual . expected) in (-zip-pair a b) +;; for i = 0 then (1+ i) +;; collect +;; `(index ,i ,@(esup-test--explain-single-esup-result +;; ignoring-slots actual expected)))))) + +;; (defun esup-test--explain-single-esup-result (ignoring-slots a b) +;; (if (esup-results-single-equal-p ignoring-slots a b) +;; 'MATCH +;; ;; Compare each slot +;; (-non-nil +;; (cl-loop for slot in (esup-test--all-slots) +;; collect +;; (cond +;; ((-contains? ignoring-slots slot) +;; `(,slot IGNORED)) +;; ;; Got a match +;; ((equal (eieio-oref a slot) (eieio-oref b slot)) +;; `(,slot MATCHED on ,(eieio-oref a slot))) +;; ;; Explain the mismatch +;; (t +;; `(,slot MISMATCH +;; actual was ,(eieio-oref b slot) +;; but expected ,(eieio-oref a slot)))))))) +;; (put 'esup-results-single-equal-p 'ert-explainer +;; 'esup-test--explain-single-esup-result) + +;; +;; ;; Test Utility Tests + +;; (ert-deftest esup-results-equal-p__empty-list__is-equal () +;; (should +;; (esup-results-equal-p '() '() '()))) + +;; (ert-deftest esup-results-equal-p__single-elem__is-equal () +;; (should +;; (esup-results-equal-p +;; '() +;; (list (make-esup-result "file" "sexp")) +;; (list (make-esup-result "file" "sexp"))))) + +;; (ert-deftest esup-results-equal-p__three-elem__is-equal () +;; (should +;; (esup-results-equal-p +;; '() +;; (list +;; (make-esup-result "file1" "sexp1" :start-point 10) +;; (make-esup-result "file2" "sexp2" :gc-time 20) +;; (make-esup-result "file3" "sexp3")) +;; (list +;; (make-esup-result "file1" "sexp1" :start-point 10) +;; (make-esup-result "file2" "sexp2" :gc-time 20) +;; (make-esup-result "file3" "sexp3"))))) + +;; (ert-deftest esup-results-equal-p__ignoring-gc-time__is-equal () +;; (should +;; (esup-results-equal-p +;; '(:gc-time) +;; (list (make-esup-result "file" "sexp" :gc-time 30)) +;; (list (make-esup-result "file" "sexp" :gc-time 50))))) + +;; (ert-deftest esup-results-equal-p__gc-time-mismatch__is-false () +;; (should +;; (not +;; (esup-results-equal-p +;; '() +;; (list (make-esup-result "file" "sexp" :gc-time 30)) +;; (list (make-esup-result "file" "sexp" :gc-time 50)))))) + +;; (ert-deftest esup-results-equal-p__single-elem-mismatch__is-false () +;; (should +;; (not +;; (esup-results-equal-p +;; '() +;; (list (make-esup-result "file" "sexp1")) +;; (list (make-esup-result "file" "sexp2")))))) + +;; (ert-deftest esup-results-single-equal-p__ignoring-no-slots__is-equal () +;; (should +;; (esup-results-single-equal-p +;; '() +;; (make-esup-result "/fake/file-1.el" "(progn 'file-1)") +;; (make-esup-result "/fake/file-1.el" "(progn 'file-1)")))) + +;; (ert-deftest esup-results-single-equal-p__sexp-mismatch__is-false () +;; (should +;; (not +;; (esup-results-single-equal-p +;; '() +;; (make-esup-result "/fake/file-1.el" "(progn 'file-1)") +;; (make-esup-result "/fake/file-1.el" "(progn 'file-2)"))))) + +;; (ert-deftest esup-results-single-equal-p__ignoring-gc-time__is-equal () +;; (should +;; (esup-results-single-equal-p +;; '(:gc-time) +;; (esup-result +;; :file "file" :expression-string "sexp" :end-point 20 :gc-time 444) +;; (esup-result +;; :file "file" :expression-string "sexp" :end-point 20 :gc-time 555)))) + +;; (ert-deftest esup-results-single-equal-p__gc-time-mismatch__is-false () +;; (should +;; (not +;; (esup-results-single-equal-p +;; '() +;; (esup-result +;; :file "file" :expression-string "sexp" :end-point 20 :gc-time 444) +;; (esup-result +;; :file "file" :expression-string "sexp" :end-point 20 :gc-time 555))))) + +;; (ert-deftest esup-results-single-equal-p__sexp-mismatch__is-false () +;; (should +;; (not +;; (esup-results-single-equal-p +;; '() +;; (esup-result +;; :file "file" :expression-string "sexp") +;; (esup-result +;; :file "file" :expression-string "sexp2"))))) + +;; (ert-deftest make-esup-result__no-extra-args__is-same () +;; (should +;; (equal +;; (make-esup-result "file" "sexp") +;; (esup-result +;; :file "file" +;; :expression-string "sexp" +;; :end-point 5)))) + +;; (ert-deftest make-esup-result__with-extra-args__is-same () +;; (should +;; (equal +;; (make-esup-result "file" "sexp" :gc-time 20 :exec-time 40) +;; (esup-result +;; :file "file" +;; :expression-string "sexp" +;; :end-point 5 +;; :gc-time 20 +;; :exec-time 40)))) + +;;; test-esup.el ends here diff --git a/test/utils.el b/test/utils.el new file mode 100644 index 0000000..9fcf529 --- /dev/null +++ b/test/utils.el @@ -0,0 +1,207 @@ +;;; utils.el --- Esup: Non-interactive unit-test setup -*- lexical-binding: t; -*- + +;; Copyright (C) 2017, 2018, 2019, 2020 Serghei Iakovlev + +;; Author: Joe Schafer +;; Maintainer: Serghei Iakovlev +;; Version: 0.7.1 +;; URL: http://github.com/jschaf/esup + +;; This file is NOT part of GNU Emacs. + +;;;; License + +;; This file 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 file 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 file. If not, see . + +;;; Commentary: + +;; Esup's non-interactive test suite setup tp use `buttercup'. + +;;; Code: + +(require 'buttercup) + +(require 'time-date) + +(require 'dash) ; `-clone', `-table-flat', `-non-nil', `-map', etc +(require 'cl-lib) ; `cl-defmacro' +(require 'noflet) ; `noflet' +(require 'el-mock) ; TODO(serghei): Do we still need this? + +;; Make sure the exact Emacs version can be found in the build output +(message "Running tests on Emacs %s" emacs-version) + +(defvar esup-debug-enabled nil + "Enable debug messages for the test utilities. +Also sends all esup-child log messages to stdout.") + +(when (require 'undercover nil t) + ;; Track coverage, but don't send to coverage serivice. Save in parent + ;; directory as undercover saves paths relative to the repository root. + (undercover "*.el" + (:report-file "coverage-final.json") + (:send-report nil))) + +(let* ((current-file (if load-in-progress load-file-name (buffer-file-name))) + (source-directory (locate-dominating-file current-file "Cask")) + ;; Don't load old byte-compiled versions + (load-prefer-newer t)) + ;; Load the file under test + (add-to-list 'load-path source-directory) + (load (expand-file-name "esup")) + (load (expand-file-name "esup-child"))) + +;;;; Utulity: + +(defun make-esup-result (file expression-string &rest args) + "Create `esup-result' with desired FILE and EXPRESSION-STRING. +In addition apply rest ARGS if any." + (apply + #'esup-result + :file file + :expression-string expression-string + :end-point (1+ (length expression-string)) + args)) + +(defun esup-test--all-slots () + "Return a list of all slots for an `esup-result'." + (--map (intern (concat ":" (symbol-name it))) + (object-slots (make-instance 'esup-result)))) + +(defun esup-test--slots-to-compare (ignoring-slots) + "Return a list of slots to compare for an `esup-result'. +Create a new list with only the members of IGNORING-SLOTS that are not in +`esup-test--all-slots' result." + (-difference (esup-test--all-slots) ignoring-slots)) + +(defun esup-results-single-equal-p (ignoring-slots a b) + "Compare `esup-result' objects with an IGNORING-SLOTS. +A test for equality of A and B is performed by using `eq' defun." + (--all? (not (null it)) + (--map (equal (eieio-oref a it) (eieio-oref b it)) + (esup-test--slots-to-compare ignoring-slots)))) + +(defun esup-results-equal-p (ignoring-slots a b) + "Compare a list of `esup-result' objects with an IGNORING-SLOTS. +A test for equality of A and B is performed by using `eq' defun." + (and + (eq (length a) (length b)) + (--all? (not (null it)) + (--zip-with (esup-results-single-equal-p + ignoring-slots it other) + a b)))) + +(defun esup-debug-test (str &rest format-args) + "Output STR with FORMAT-ARGS if debug-mode is t." + (when esup-debug-enabled + (apply 'message str format-args))) + +(defun esup--join-paths (dir file) + "Ensure FILE is abolute file name, otherwise use DIR as a base path." + (cond + ((file-name-absolute-p file) file) + ((string= " " dir) file) + (t (concat (file-name-as-directory dir) file)))) + +(defun esup-test-make-locate-file-fn (mock-fs) + "Create locate file defun using MOCK-FS." + (lambda (filename path &optional suffixes predicate) + (esup-debug-test + (concat "starting generated locate-file-fn: " + "filename=%s path=%s suffixes=%s predicate=%s") + filename path suffixes predicate) + (let* ((all-files-no-suffix + (-table-flat 'esup--join-paths (cons "" path) (list filename))) + (all-files + (-table-flat 'concat all-files-no-suffix (cons "" load-suffixes))) + (matching-files-in-mock-fs + (-non-nil + ;; Find files that exist in the mock-fs + (-map + (lambda (path) + (car-safe + (or (assoc path mock-fs) (assoc (concat "./" path) mock-fs)))) + all-files)))) + (esup-debug-test "searching for file match: matching-files=%s all-files=%s" + matching-files-in-mock-fs all-files) + (car-safe matching-files-in-mock-fs)))) + +(defmacro with-esup-mock (props &rest body) + "Evaluate BODY with local esup state variables. +Use PROPS as a property list to create mock filesystem." + (let ((old-features (-clone features))) + `(let* ((load-path (plist-get ,props :load-path)) + (mock-fs (plist-get ,props :files)) + (locate-fn (esup-test-make-locate-file-fn mock-fs))) + (esup-debug-test "starting with-esup-mock: load-path=%s mock-fs=%s" + load-path mock-fs) + (noflet + ((find-file-noselect + (filename &optional nowarn rawfile wildcards) + (esup-debug-test + (concat + "starting mock find-file-no-select: " + "filename=%s nowarn=%s rawfile=%s wildcards=%s") + filename nowarn rawfile wildcards) + + (let ((mock-file-exists (assoc filename mock-fs)) + (contents (alist-get filename mock-fs))) + (if mock-file-exists + (with-current-buffer (get-buffer-create filename) + (setq-local buffer-file-name filename) + (setq-local buffer-read-only nil) + (insert contents) + (current-buffer)) + (error "Unknown file %s not in mock-fs" filename)))) + + (locate-file + (filename path &optional suffixes predicate) + (esup-debug-test + "starting mock locate-file: filename=%s path=%s suffixes=%s pred=%s" + filename path suffixes predicate) + + (let ((results (funcall locate-fn filename path suffixes predicate))) + (esup-debug-test "locate-file mock returned '%s'" results) + results)) + + (require (feature &optional filename noerror) + (esup-debug-test + "starting mock require: feature=%s filename=%s noerror=%s" + feature filename noerror) + (if filename + (funcall locate-fn filename load-path) + (funcall locate-fn (symbol-name feature) load-path))) + + ;; Stub out network calls. + (esup-child-init-streams (port)) + (kill-emacs (&optional arg)) + (process-send-string (process string) + (when esup-debug-enabled (message string))) + (process-send-eof (&optional process))) + + ,@body + + (esup-debug-test "test added features %s" + (-difference features ',old-features)) + ;; Reset the features list in case any tests provided features. + (setq features ',old-features) + + ;; Reset the max depth since the tests re-use the same environment. + (when (and (boundp 'esup-child-max-depth) + (not (eq esup-child-max-depth 2))) + (esup-debug-test "Resetting esup-child-max-depth back to 2 from %d" + esup-child-max-depth) + (setq esup-child-max-depth 2)))))) + +;;; utils.el ends here