diff --git a/.github/matchers/checkstyle.json b/.github/matchers/checkstyle.json new file mode 100644 index 000000000..ca6e30e63 --- /dev/null +++ b/.github/matchers/checkstyle.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "checkstyle", + "pattern": [ + { + "regexp": "^([a-z]+) ([\\w./-]+):(\\d+):(\\d+): (.*)$", + "severity": 1, + "file": 2, + "line": 3, + "column": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/matchers/illuaminate.json b/.github/matchers/illuaminate.json new file mode 100644 index 000000000..f59036028 --- /dev/null +++ b/.github/matchers/illuaminate.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "illuaminate", + "severity": "warning", + "pattern": [ + { + "regexp": "^([\\w./-]+):\\[(\\d+):(\\d+)\\-(?:\\d+):(?:\\d+)\\]: (.*) \\[([a-z:-]+)\\]$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/matchers/junit.json b/.github/matchers/junit.json new file mode 100644 index 000000000..1655a5f66 --- /dev/null +++ b/.github/matchers/junit.json @@ -0,0 +1,15 @@ +{ + "problemMatcher": [ + { + "owner": "junit", + "pattern": [ + { + "regexp": "^## ([\\w./-]+):(\\d+): (.*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ] +} diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 646f3123c..d3a482960 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -41,8 +41,11 @@ jobs: path: build/libs - name: Upload Coverage - run: bash <(curl -s https://codecov.io/bash) - continue-on-error: true + uses: codecov/codecov-action@v1 + + - name: Parse test reports + run: ./tools/parse-reports.py + if: ${{ failure() }} - name: Cache pre-commit uses: actions/cache@v2 @@ -51,6 +54,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('config/pre-commit/config.yml') }} restore-keys: | ${{ runner.os }}-pre-commit- + - name: Run linters run: | pip install pre-commit diff --git a/config/pre-commit/illuaminate-lint.sh b/config/pre-commit/illuaminate-lint.sh index cc89c58fd..ac7b762bf 100755 --- a/config/pre-commit/illuaminate-lint.sh +++ b/config/pre-commit/illuaminate-lint.sh @@ -5,5 +5,12 @@ test -d bin || mkdir bin test -f bin/illuaminate || curl -s -obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate chmod +x bin/illuaminate +if [ -n ${GITHUB_ACTIONS+x} ]; then + # Register a problem matcher (see https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md) + # for illuaminate. + echo "::add-matcher::.github/matchers/illuaminate.json" + trap 'echo "::remove-matcher owner=illuaminate::"' EXIT +fi + ./gradlew luaJavadoc bin/illuaminate lint diff --git a/tools/parse-reports.py b/tools/parse-reports.py new file mode 100755 index 000000000..5471af6f6 --- /dev/null +++ b/tools/parse-reports.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Parse reports generated by Gradle and convert them into GitHub annotations. + +See https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md and +https://github.com/actions/toolkit/blob/master/docs/commands.md. +""" + +from typing import Optional, Tuple +import pathlib +import xml.etree.ElementTree as ET +import re +import os.path + + +LUA_ERROR_LOCATION = re.compile(r"^\s+(/[\w./-]+):(\d+):", re.MULTILINE) +JAVA_ERROR_LOCATION = re.compile(r"^\tat ([\w.]+)\.[\w]+\([\w.]+:(\d+)\)$", re.MULTILINE) +ERROR_MESSAGE = re.compile(r"(.*)\nstack traceback:", re.DOTALL) + +SPACES = re.compile(r"\s+") + +SOURCE_LOCATIONS = [ + "src/main/java", + "src/main/resources/data/computercraft/lua", + "src/test/java", + "src/test/resources", +] + + +def find_file(path: str) -> Optional[str]: + while len(path) > 0 and path[0] == '/': + path = path[1:] + + for source_dir in SOURCE_LOCATIONS: + child_path = os.path.join(source_dir, path) + if os.path.exists(child_path): + return child_path + + return None + + +def find_location(message: str) -> Optional[Tuple[str, str]]: + location = LUA_ERROR_LOCATION.search(message) + if location: + file = find_file(location[1]) + if file: + return file, location[2] + + for location in JAVA_ERROR_LOCATION.findall(message): + file = find_file(location[0].replace(".", "/") + ".java") + if file: + return file, location[1] + + return None + + +def parse_junit() -> None: + """ + Scrape JUnit test reports for errors. We determine the location from the Lua + or Java stacktrace. + """ + print("::add-matcher::.github/matchers/junit.json") + + for path in pathlib.Path("build/test-results/test").glob("TEST-*.xml"): + for testcase in ET.parse(path).getroot(): + if testcase.tag != "testcase": + continue + + for result in testcase: + if result.tag != "failure": + continue + + name = f'{testcase.attrib["classname"]}.{testcase.attrib["name"]}' + message = result.attrib.get('message') + + location = find_location(result.text) + error = ERROR_MESSAGE.match(message) + if error: + error = error[1] + else: + error = message + + if location: + print(f'## {location[0]}:{location[1]}: {name} failed: {SPACES.sub(" ", error)}') + else: + print(f'::error::{name} failed') + + print("::group::Full error message") + print(result.text) + print("::endgroup") + + print("::remove-matcher owner=junit::") + + +def parse_checkstyle() -> None: + """ + Scrape JUnit test reports for errors. We determine the location from the Lua + or Java stacktrace. + """ + print("::add-matcher::.github/matchers/checkstyle.json") + + for path in pathlib.Path("build/reports/checkstyle/").glob("*.xml"): + for file in ET.parse(path).getroot(): + for error in file: + filename = os.path.relpath(file.attrib['name']) + + attrib = error.attrib + print(f'{attrib["severity"]} {filename}:{attrib["line"]}:{attrib.get("column", 1)}: {SPACES.sub(" ", attrib["message"])}') + + print("::remove-matcher owner=checkstyle::") + +if __name__ == '__main__': + parse_junit() + parse_checkstyle()