branch: elpa/flymake-collection commit eb8a7eb96e200cada6559db436298136cd0d5fd1 Author: Mohsin Kaleem <mohk...@kisara.moe> Commit: Mohsin Kaleem <mohk...@kisara.moe>
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)