From 5f3b4640e82727a8ca680037ecb7f6d40e648f8e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 5 Jan 2019 19:56:23 -0500 Subject: [PATCH] refactor: use a python script to translate from shellcheck output to junit The checkstyle xslt wasn't quite good enough, resulting in missing/malformatted output. Note that this now excludes _passing_ tests, which also isn't quite right, but a fair bit closer to the xsd here: https://github.com/windyroad/JUnit-Schema --- tests.mk | 10 +-- tests/checkstyle2junit.xslt | 44 ---------- tests/shellcheck-to-junit | 160 ++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 51 deletions(-) delete mode 100644 tests/checkstyle2junit.xslt create mode 100755 tests/shellcheck-to-junit diff --git a/tests.mk b/tests.mk index fb88ad16c..dd353e8aa 100644 --- a/tests.mk +++ b/tests.mk @@ -80,13 +80,9 @@ endif lint: # these are disabled due to their expansive existence in the codebase. we should clean it up though @cat tests/shellcheck-exclude | sed -n -e '/^# SC/p' -ifeq ($(CIRCLECI),true) - @echo creating junit output... - @mkdir -p test-results/shellcheck - @$(QUIET) find . -not -path '*/\.*' -not -path './debian/*' -type f | xargs file | grep text | awk -F ':' '{ print $$1 }' | xargs head -n1 | egrep -B1 "bash" | grep "==>" | awk '{ print $$2 }' | xargs shellcheck -e $(shell cat tests/shellcheck-exclude | sed -n -e '/^# SC/p' | cut -d' ' -f2 | paste -d, -s) -f checkstyle | xmlstarlet tr tests/checkstyle2junit.xslt > test-results/shellcheck/results.xml -endif @echo linting... - @$(QUIET) find . -not -path '*/\.*' -not -path './debian/*' -type f | xargs file | grep text | awk -F ':' '{ print $$1 }' | xargs head -n1 | egrep -B1 "bash" | grep "==>" | awk '{ print $$2 }' | xargs shellcheck -e $(shell cat tests/shellcheck-exclude | sed -n -e '/^# SC/p' | cut -d' ' -f2 | paste -d, -s) + @mkdir -p test-results/shellcheck + @$(QUIET) find . -not -path '*/\.*' -not -path './debian/*' -type f | xargs file | grep text | awk -F ':' '{ print $$1 }' | xargs head -n1 | egrep -B1 "bash" | grep "==>" | awk '{ print $$2 }' | xargs shellcheck -e $(shell cat tests/shellcheck-exclude | sed -n -e '/^# SC/p' | cut -d' ' -f2 | paste -d, -s) | tests/shellcheck-to-junit --output test-results/shellcheck/results.xml ci-go-coverage: docker run --rm -ti \ @@ -223,6 +219,6 @@ deploy-tests: test: setup-deploy-tests lint unit-tests deploy-tests test-ci: - mkdir -p test-results/bats + @mkdir -p test-results/bats @cd tests/unit && echo "executing tests: $(shell cd tests/unit ; circleci tests glob *.bats | circleci tests split --split-by=timings | xargs)" cd tests/unit && bats --formatter bats-format-junit -e -T -o ../../test-results/bats $(shell cd tests/unit ; circleci tests glob *.bats | circleci tests split --split-by=timings | xargs) diff --git a/tests/checkstyle2junit.xslt b/tests/checkstyle2junit.xslt deleted file mode 100644 index 0fea4528c..000000000 --- a/tests/checkstyle2junit.xslt +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Line - - : - - See https://www.shellcheck.net/wiki/ - - - - diff --git a/tests/shellcheck-to-junit b/tests/shellcheck-to-junit new file mode 100755 index 000000000..a1b62e547 --- /dev/null +++ b/tests/shellcheck-to-junit @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import argparse +import collections +import datetime +import re +import socket +import sys + +from xml.etree import ElementTree + + +def CDATA(text=None): + element = ElementTree.Element('![CDATA[') + element.text = text + return element + + +def _serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs): + + if elem.tag == '![CDATA[': + write("\n<{}{}]]>\n".format(elem.tag, elem.text)) + if elem.tail: + write(ElementTree._escape_cdata(elem.tail)) + else: + return ElementTree._original_serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs) + + +ElementTree._original_serialize_xml = ElementTree._serialize_xml +ElementTree._serialize_xml = ElementTree._serialize['xml'] = _serialize_xml + + +def read_in(): + lines = sys.stdin.readlines() + for i in range(len(lines)): + lines[i] = lines[i].rstrip() + return lines + + +def process_lines(lines): + files = {} + current_file = None + previous_line = None + line_no = None + new_issues = [] + code = None + + RE_VIOLATION = re.compile(r"\^-- (SC[\w]+): (.*)") + + for line in lines: + # start a new block + if line == '': + if current_file is not None: + file_data = files.get(current_file, {}) + files[current_file] = file_data + + issue_data = file_data.get(line_no, {}) + issue_data['code'] = code + files[current_file][line_no] = issue_data + + issues = issue_data.get('issues', []) + issues.extend(new_issues) + issue_data['issues'] = issues + + files[current_file][line_no] = issue_data + + code = None + current_file = None + line_no = None + elif line.startswith('In ./') and not previous_line: + current_file = line.split(' ')[1].replace('./', '') + line_no = line.split(' ')[3] + new_issues = [] + code = None + elif code is None and len(new_issues) == 0: + code = line + else: + match = RE_VIOLATION.match(line.strip()) + if not match: + print('Error: Issue parsing line "{0}"'.format(line.strip())) + else: + new_issues.append({ + 'shellcheck_id': match.group(1), + 'message': match.group(2), + 'original_message': line + }) + + previous_line = line + + return files + + +def output_junit(files, output=None): + timestamp = datetime.datetime.now().replace(microsecond=0).isoformat() + failures = 0 + for file, data in files.items(): + for line, issue_data in data.items(): + code = issue_data.get('code') + for issue in issue_data.get('issues', []): + failures += 1 + + root = ElementTree.Element("testsuite", + name="shellcheck", + tests="0", + failures="{0}".format(failures), + errors="0", + skipped="0", + timestamp=timestamp, + time="0", + hostname=socket.gethostname()) + + ElementTree.SubElement(root, "properties") + for file, data in files.items(): + for line, issue_data in data.items(): + code = issue_data.get('code') + for issue in issue_data.get('issues', []): + testcase = ElementTree.SubElement(root, + "testcase", + classname=file, + name=file, + time="0") + shellcheck_id = issue.get('shellcheck_id') + message = 'line {0}: {1}'.format(line, issue.get('message')) + original_message = issue.get('original_message') + e = ElementTree.Element("failure", + type=shellcheck_id, + message=message) + cdata = CDATA("\n".join([code, original_message])) + e.append(cdata) + testcase.append(e) + + ElementTree.SubElement(root, "system-out") + ElementTree.SubElement(root, "system-err") + + content = ElementTree.tostring(root, encoding='utf8', method='xml') + if output: + with open(output, 'w') as f: + f.write(content) + + +def main(): + parser = argparse.ArgumentParser( + description='Process shellcheck output to junit.') + parser.add_argument('--output', + dest='output', + action='store', + default=None, + help='file to write shellcheck output') + args = parser.parse_args() + + lines = read_in() + files = process_lines(lines) + files = collections.OrderedDict(sorted(files.items())) + output_junit(files, args.output) + for line in lines: + print(line) + + +if __name__ == '__main__': + main()