branch: elpa/flymake-collection
commit eb8a7eb96e200cada6559db436298136cd0d5fd1
Author: Mohsin Kaleem <[email protected]>
Commit: Mohsin Kaleem <[email protected]>
tests: Add test harness
---
Makefile | 11 +++
src/flymake-collection-define.el | 3 +
tests/checkers/Dockerfile | 49 ++++++++++
tests/checkers/README.org | 0
tests/checkers/installers/pylint.bash | 1 +
tests/checkers/run-test-case | 166 ++++++++++++++++++++++++++++++++++
tests/checkers/test-cases/pylint.yml | 29 ++++++
7 files changed, 259 insertions(+)
diff --git a/Makefile b/Makefile
index 6b2afc0719..b3cf917aac 100644
--- a/Makefile
+++ b/Makefile
@@ -25,6 +25,17 @@ compile: ## Check for byte-compiler errors
| grep . && exit 1 || true ;\
done
+.PHONY: test
+test: ## Run all defined test cases.
+ @echo "[test] Running all test cases"
+ @docker build -t flymake-collection-test ./tests/checkers
+ @docker run \
+ --rm \
+ --volume "$$(pwd)":/src:ro \
+ --volume "$$(pwd)/tests/checkers":/test:ro \
+ flymake-collection-test \
+ sh -c 'find /test/test-cases/ -iname \*.yml | parallel -I{} chronic
/test/run-test-case {}'
+
.PHONY: clean
clean: ## Remove build artifacts
@echo "[clean]" $(subst .el,.elc,$(SRC))
diff --git a/src/flymake-collection-define.el b/src/flymake-collection-define.el
index 27ff03abe3..038a8bf0ea 100644
--- a/src/flymake-collection-define.el
+++ b/src/flymake-collection-define.el
@@ -293,6 +293,9 @@ exit status %d\nStderr: %s"
;; Finished linting, cleanup any temp-files and then kill
;; the process buffer.
,@cleanup-form
+ (when (eq (plist-get flymake-collection-define--procs
',name)
+ ,proc-symb)
+ (cl-remf flymake-collection-define--procs ',name))
(kill-buffer (process-buffer ,proc-symb)))))))
;; Push the new-process to the process to the process alist.
(setq flymake-collection-define--procs
diff --git a/tests/checkers/Dockerfile b/tests/checkers/Dockerfile
new file mode 100644
index 0000000000..8a43b66778
--- /dev/null
+++ b/tests/checkers/Dockerfile
@@ -0,0 +1,49 @@
+# Ubuntu 20.04 LTS supported until April 2025
+FROM ubuntu:20.04
+
+# Suppress some interactive prompts by answering them with environment
+# variables.
+ENV DEBIAN_FRONTEND=noninteractive
+ENV TZ=Etc/UTC
+
+# Install emacs. Instructions adapted from
[[https://www.masteringemacs.org/article/speed-up-emacs-libjansson-native-elisp-compilation][here]].
+WORKDIR /build/emacs
+RUN apt-get update && \
+ apt-get install -y \
+ apt-transport-https \
+ ca-certificates \
+ curl \
+ gnupg-agent \
+ software-properties-common \
+ libjansson4 \
+ libjansson-dev \
+ git && \
+ git clone -b emacs-28 --single-branch git://git.savannah.gnu.org/emacs.git
. && \
+ sed -i 's/# deb-src/deb-src/' /etc/apt/sources.list && \
+ apt-get update && \
+ apt-get build-dep -y emacs && \
+ ./autogen.sh && \
+ ./configure && \
+ make -j4 && \
+ make install && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+COPY installers /build/installers
+RUN apt-get update && \
+ apt-get install -y \
+ parallel python3.8 python3-pip moreutils git && \
+ python3.8 -m pip install pyyaml && \
+ find installers/ -type f -iname '*.bash' -exec {} \; && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install the latest available version of flymake for these tests.
+RUN cd "$(mktemp -d)" && \
+ git clone https://github.com/emacs-straight/flymake.git . && \
+ mkdir /tmp/emacs && \
+ cp flymake.el /etc/emacs/flymake.el && \
+ rm -rf "$(pwd)"
+
+WORKDIR /src
diff --git a/tests/checkers/README.org b/tests/checkers/README.org
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/checkers/installers/pylint.bash
b/tests/checkers/installers/pylint.bash
new file mode 100755
index 0000000000..febb9baf66
--- /dev/null
+++ b/tests/checkers/installers/pylint.bash
@@ -0,0 +1 @@
+python3.8 -m pip install pylint
diff --git a/tests/checkers/run-test-case b/tests/checkers/run-test-case
new file mode 100755
index 0000000000..5e309f07bc
--- /dev/null
+++ b/tests/checkers/run-test-case
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3.8
+"""Test case runner for flymake-collection.
+"""
+import json
+import logging
+import pathlib
+import subprocess
+import sys
+import tempfile
+from dataclasses import dataclass
+from typing import List, Optional, Tuple
+
+import yaml
+
+
+@dataclass
+class TestLint:
+ """A flymake diagnostic."""
+
+ point: Tuple[int, int] # Line and column
+ level: str
+ message: str
+
+
+@dataclass
+class TestCase:
+ """A test case called `name`, given `file` that should give back
`lints`."""
+
+ name: str
+ file: str
+ lints: List[TestLint]
+
+ def __post_init__(self):
+ self.lints = [TestLint(**it) for it in self.lints]
+
+ def run(self, checker: str) -> bool:
+ with tempfile.NamedTemporaryFile("w") as file:
+ file.write(self.file)
+ file.flush()
+ actual_lints = run_flymake(pathlib.Path(file.name), checker)
+ if actual_lints is None:
+ return False
+
+ failed = False
+ for lint in self.lints:
+ try:
+ pos = actual_lints.index(lint)
+ except ValueError:
+ logging.error("Expected to encounter lint: %s", lint)
+ failed = True
+ else:
+ actual_lints.pop(pos)
+ for lint in actual_lints:
+ logging.error("Encountered unexpected lint: %s", lint)
+ failed = True
+ return not failed
+
+
+@dataclass
+class TestConfig:
+ checker: str
+ tests: List[TestCase]
+
+ def __post_init__(self):
+ self.tests = [TestCase(**it) for it in self.tests]
+
+
+def run_flymake(src: pathlib.Path, checker: str) -> Optional[List[TestLint]]:
+ with tempfile.NamedTemporaryFile("w") as script,
tempfile.NamedTemporaryFile(
+ "r"
+ ) as out:
+ script.write(
+ f"""
+(require 'flymake)
+(require 'json)
+
+(add-to-list 'load-path "/src/src")
+(add-to-list 'load-path "/src/src/checkers")
+(require (intern "{checker}") "{checker}.el")
+
+(setq src (find-file-literally "{src}")
+ out (find-file "{out.name}"))
+
+(defun column-number (point)
+ "Returns the column number at POINT."
+ (interactive)
+ (save-excursion
+ (goto-char point)
+ (current-column)))
+
+(with-current-buffer src
+ ({checker}
+ (lambda (diags)
+ (with-current-buffer out
+ (cl-loop for diag in diags
+ collect
+ (insert
+ (json-encode
+ `((point . ,(with-current-buffer src
+ (let ((beg (flymake--diag-beg diag)))
+ (list (line-number-at-pos beg)
+ (column-number beg)))))
+ (level . ,(flymake--diag-type diag))
+ (message . ,(substring-no-properties (flymake--diag-text
diag)))))
+ "\n"))
+ (save-buffer))))
+
+;; Block until the checker process finishes.
+(while flymake-collection-define--procs
+ (sleep-for 0.25)))
+ """
+ )
+ script.flush()
+ proc = subprocess.run(
+ ["emacs", "-Q", "--script", script.name],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ if proc.returncode != 0:
+ logging.error("Failed to run checker using emacs")
+ logging.error("Emacs exited with stderr: %s", proc.stderr)
+ return None
+
+ lints = []
+ for line in out:
+ if line.strip() == "":
+ continue
+ lints.append(TestLint(**json.loads(line)))
+ return lints
+
+
+def main(args, vargs, parser) -> bool:
+ failed = False
+ logging.info("Loading test config file=%s", args.test)
+ with args.test.open("r") as test_file:
+ cfg_obj = yaml.load(test_file, Loader=yaml.SafeLoader)
+ try:
+ cfg = TestConfig(**cfg_obj)
+ except ValueError:
+ logging.exception("Failed to read test configuration")
+ return False
+
+ logging.info("Running tests with checker=%s", cfg.checker)
+ for i, test in enumerate(cfg.tests):
+ logging.info("Running test case %d name=%s", i, test.name)
+ if not test.run(cfg.checker):
+ failed = True
+
+ return not failed
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ "test", type=pathlib.Path, help="Path to test cases config file"
+ )
+
+ args = parser.parse_args()
+ vargs = vars(args)
+
+ logging.basicConfig(level=logging.DEBUG)
+
+ sys.exit(0 if main(args, vargs, parser) else 1)
diff --git a/tests/checkers/test-cases/pylint.yml
b/tests/checkers/test-cases/pylint.yml
new file mode 100644
index 0000000000..3052b66b80
--- /dev/null
+++ b/tests/checkers/test-cases/pylint.yml
@@ -0,0 +1,29 @@
+---
+checker: flymake-collection-pylint
+tests:
+ - name: no-lints
+ file: |
+ """A test case with no output from pylint."""
+
+ print("hello world")
+ lints: []
+ - name: notes
+ file: |
+ """A test case with a warning lint."""
+
+ print(f"hello world")
+ print(f"hello world")
+ lints:
+ - point: [3, 5]
+ level: warning
+ message: W1309 Using an f-string that does not have any interpolated
variables (pylint)
+ - point: [4, 5]
+ level: warning
+ message: W1309 Using an f-string that does not have any interpolated
variables (pylint)
+ - name: syntax-error
+ file: |
+ definitely should not work
+ lints:
+ - point: [1, 11]
+ level: error
+ message: E0001 invalid syntax (<unknown>, line 1) (pylint)