Add problem matchers for Github actions

- Add a basic problem matcher for illuaminate errors.
 - Add a script (tools/parse-reports.py) which parses the XML reports
   generated by checkstyle and junit, extracts source locations, and
   emits them in a manner which can be consumed by another set of
   matchers.

This should make it a little easier to see problems for folks who just
rely on CI to test things (though also, please don't do this if you can
help it).
This commit is contained in:
Jonathan Coates 2021-05-17 16:31:58 +00:00
parent e10e30f82b
commit 953b94fd08
6 changed files with 177 additions and 2 deletions

17
.github/matchers/checkstyle.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"problemMatcher": [
{
"owner": "checkstyle",
"pattern": [
{
"regexp": "^([a-z]+) ([\\w./-]+):(\\d+):(\\d+): (.*)$",
"severity": 1,
"file": 2,
"line": 3,
"column": 4,
"message": 5
}
]
}
]
}

18
.github/matchers/illuaminate.json vendored Normal file
View File

@ -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
}
]
}
]
}

15
.github/matchers/junit.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"problemMatcher": [
{
"owner": "junit",
"pattern": [
{
"regexp": "^## ([\\w./-]+):(\\d+): (.*)$",
"file": 1,
"line": 2,
"message": 3
}
]
}
]
}

View File

@ -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

View File

@ -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

114
tools/parse-reports.py Executable file
View File

@ -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()