From dd6c641112066531480f4fd11a994786f2130b70 Mon Sep 17 00:00:00 2001 From: Enrico Minack Date: Wed, 6 Jul 2022 11:33:12 +0100 Subject: [PATCH] Log the actual exception that caused a ParseError (#322) --- python/publish/github_action.py | 7 +- python/publish/junit.py | 61 ++++++++++------- python/publish/nunit.py | 22 ++---- python/publish/publisher.py | 8 ++- python/publish/trx.py | 22 ++---- python/publish/unittestresults.py | 67 +++++++++++++++++-- python/publish/xunit.py | 22 ++---- python/publish_unit_test_results.py | 2 +- python/test/files/junit-xml/empty.exception | 2 +- python/test/files/junit-xml/non-junit.results | 3 +- python/test/files/junit-xml/non-xml.exception | 2 +- .../junit-xml/pytest/corrupt-xml.exception | 2 +- .../nunit3/jenkins/NUnit-issue17521.exception | 2 +- .../nunit3/jenkins/NUnit-issue47367.exception | 2 +- python/test/test_action_script.py | 4 +- python/test/test_action_yml.py | 2 +- python/test/test_cicd_yml.py | 2 +- python/test/test_junit.py | 31 +++++---- python/test/test_nunit.py | 8 +-- python/test/test_publish.py | 11 +-- python/test/test_publisher.py | 9 ++- python/test/test_readme_md.py | 4 +- python/test/test_trx.py | 6 +- python/test/test_unittestresults.py | 29 ++++++-- python/test/test_xunit.py | 8 +-- 25 files changed, 206 insertions(+), 132 deletions(-) diff --git a/python/publish/github_action.py b/python/publish/github_action.py index c5ad3c21..d2f4bd78 100644 --- a/python/publish/github_action.py +++ b/python/publish/github_action.py @@ -62,8 +62,11 @@ def warning(self, message: str, file: Optional[str] = None, line: Optional[int] params.update(col=column) self._command(self._file, 'warning', message, params) - def error(self, message: str, file: Optional[str] = None, line: Optional[int] = None, column: Optional[int] = None): - logger.error(message) + def error(self, + message: str, + file: Optional[str] = None, line: Optional[int] = None, column: Optional[int] = None, + exception: Optional[BaseException] = None): + logger.error(message, exc_info=exception) params = {} if file is not None: diff --git a/python/publish/junit.py b/python/publish/junit.py index 5b0bbe1b..3bb87181 100644 --- a/python/publish/junit.py +++ b/python/publish/junit.py @@ -120,50 +120,61 @@ def close(self) -> Element: JUnitTree = etree.ElementTree -JUnitTreeOrException = Union[JUnitTree, BaseException] -ParsedJUnitFile = Tuple[str, JUnitTreeOrException] +JUnitTreeOrParseError = Union[JUnitTree, ParseError] +JUnitXmlOrParseError = Union[JUnitXml, ParseError] +ParsedJUnitFile = Tuple[str, JUnitTreeOrParseError] + + +def safe_parse_xml_file(path: str, parse: Callable[[str], JUnitTree]) -> JUnitTreeOrParseError: + """Parses an xml file and returns either a JUnitTree or a ParseError.""" + if not os.path.exists(path): + return ParseError.from_exception(path, FileNotFoundError(f'File does not exist.')) + if os.stat(path).st_size == 0: + return ParseError.from_exception(path, Exception(f'File is empty.')) + + try: + return parse(path) + except BaseException as e: + return ParseError.from_exception(path, e) + + +def progress_safe_parse_xml_file(files: Iterable[str], + parse: Callable[[str], JUnitTree], + progress: Callable[[ParsedJUnitFile], ParsedJUnitFile]) -> Iterable[ParsedJUnitFile]: + return [progress((file, safe_parse_xml_file(file, parse))) for file in files] def parse_junit_xml_files(files: Iterable[str], drop_testcases: bool = False, progress: Callable[[ParsedJUnitFile], ParsedJUnitFile] = lambda x: x) -> Iterable[ParsedJUnitFile]: """Parses junit xml files.""" - def parse(path: str) -> JUnitTreeOrException: - """Parses a junit xml file and returns either a JUnitTree or an Exception.""" - if not os.path.exists(path): - return FileNotFoundError(f'File does not exist.') - if os.stat(path).st_size == 0: - return Exception(f'File is empty.') - - try: - if drop_testcases: - builder = DropTestCaseBuilder() - parser = etree.XMLParser(target=builder, encoding='utf-8', huge_tree=True) - return etree.parse(path, parser=parser) - return etree.parse(path) - except BaseException as e: - return e + def parse(path: str) -> JUnitTree: + if drop_testcases: + builder = DropTestCaseBuilder() + parser = etree.XMLParser(target=builder, encoding='utf-8', huge_tree=True) + return etree.parse(path, parser=parser) + return etree.parse(path) - return [progress((result_file, parse(result_file))) for result_file in files] + return progress_safe_parse_xml_file(files, parse, progress) def process_junit_xml_elems(trees: Iterable[ParsedJUnitFile], time_factor: float = 1.0) -> ParsedUnitTestResults: - def create_junitxml(filepath: str, tree: JUnitTree) -> Union[JUnitXml, JUnitXmlError]: + def create_junitxml(filepath: str, tree: JUnitTree) -> JUnitXmlOrParseError: try: instance = JUnitXml.fromroot(tree.getroot()) instance.filepath = filepath return instance except JUnitXmlError as e: - return e + return ParseError.from_exception(filepath, e) - processed = [(result_file, create_junitxml(result_file, tree) if not isinstance(tree, BaseException) else tree) + processed = [(result_file, create_junitxml(result_file, tree) if not isinstance(tree, ParseError) else tree) for result_file, tree in trees] junits = [(result_file, junit) for result_file, junit in processed - if not isinstance(junit, BaseException)] - errors = [ParseError.from_exception(result_file, exception) - for result_file, exception in processed - if isinstance(exception, BaseException)] + if not isinstance(junit, ParseError)] + errors = [error + for _, error in processed + if isinstance(error, ParseError)] suites = [(result_file, suite) for result_file, junit in junits diff --git a/python/publish/nunit.py b/python/publish/nunit.py index a909573b..c866d953 100644 --- a/python/publish/nunit.py +++ b/python/publish/nunit.py @@ -1,29 +1,19 @@ -import os import pathlib from typing import Iterable, Callable from lxml import etree -from publish.junit import JUnitTreeOrException, ParsedJUnitFile +from publish.junit import JUnitTree, ParsedJUnitFile, progress_safe_parse_xml_file -with (pathlib.Path(__file__).parent / 'xslt' / 'nunit3-to-junit.xslt').open('r', encoding='utf-8') as r: +with (pathlib.Path(__file__).resolve().parent / 'xslt' / 'nunit3-to-junit.xslt').open('r', encoding='utf-8') as r: transform_nunit_to_junit = etree.XSLT(etree.parse(r)) def parse_nunit_files(files: Iterable[str], progress: Callable[[ParsedJUnitFile], ParsedJUnitFile] = lambda x: x) -> Iterable[ParsedJUnitFile]: """Parses nunit files.""" - def parse(path: str) -> JUnitTreeOrException: - """Parses an nunit file and returns either a JUnitTree or an Exception.""" - if not os.path.exists(path): - return FileNotFoundError(f'File does not exist.') - if os.stat(path).st_size == 0: - return Exception(f'File is empty.') + def parse(path: str) -> JUnitTree: + nunit = etree.parse(path) + return transform_nunit_to_junit(nunit) - try: - nunit = etree.parse(path) - return transform_nunit_to_junit(nunit) - except BaseException as e: - return e - - return [progress((result_file, parse(result_file))) for result_file in files] + return progress_safe_parse_xml_file(files, parse, progress) diff --git a/python/publish/publisher.py b/python/publish/publisher.py index c77d3007..2618a51f 100644 --- a/python/publish/publisher.py +++ b/python/publish/publisher.py @@ -98,8 +98,14 @@ def _formatted_stats_and_delta(cls, return d def _as_dict(self) -> Dict[str, Any]: + self_without_exceptions = dataclasses.replace( + self, + stats=self.stats.without_exceptions(), + stats_with_delta=self.stats_with_delta.without_exceptions() if self.stats_with_delta else None + ) # the dict_factory removes None values - return dataclasses.asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None}) + return dataclasses.asdict(self_without_exceptions, + dict_factory=lambda x: {k: v for (k, v) in x if v is not None}) def to_dict(self, thousands_separator: str) -> Mapping[str, Any]: d = self._as_dict() diff --git a/python/publish/trx.py b/python/publish/trx.py index dcabc9c2..047bf97e 100644 --- a/python/publish/trx.py +++ b/python/publish/trx.py @@ -1,29 +1,19 @@ -import os import pathlib from typing import Iterable, Callable from lxml import etree -from publish.junit import JUnitTreeOrException, ParsedJUnitFile +from publish.junit import JUnitTree, ParsedJUnitFile, progress_safe_parse_xml_file -with (pathlib.Path(__file__).parent / 'xslt' / 'trx-to-junit.xslt').open('r', encoding='utf-8') as r: +with (pathlib.Path(__file__).resolve().parent / 'xslt' / 'trx-to-junit.xslt').open('r', encoding='utf-8') as r: transform_trx_to_junit = etree.XSLT(etree.parse(r)) def parse_trx_files(files: Iterable[str], progress: Callable[[ParsedJUnitFile], ParsedJUnitFile] = lambda x: x) -> Iterable[ParsedJUnitFile]: """Parses trx files.""" - def parse(path: str) -> JUnitTreeOrException: - """Parses a trx file and returns either a JUnitTree or an Exception.""" - if not os.path.exists(path): - return FileNotFoundError(f'File does not exist.') - if os.stat(path).st_size == 0: - return Exception(f'File is empty.') + def parse(path: str) -> JUnitTree: + trx = etree.parse(path) + return transform_trx_to_junit(trx) - try: - trx = etree.parse(path) - return transform_trx_to_junit(trx) - except BaseException as e: - return e - - return [progress((result_file, parse(result_file))) for result_file in files] + return progress_safe_parse_xml_file(files, parse, progress) diff --git a/python/publish/unittestresults.py b/python/publish/unittestresults.py index e77c278a..af42db6b 100644 --- a/python/publish/unittestresults.py +++ b/python/publish/unittestresults.py @@ -1,5 +1,5 @@ -from collections import defaultdict import dataclasses +from collections import defaultdict from dataclasses import dataclass from typing import Optional, List, Mapping, Any, Union, Dict, Callable from xml.etree.ElementTree import ParseError as XmlParseError @@ -31,6 +31,7 @@ class ParseError: message: str line: Optional[int] = None column: Optional[int] = None + exception: Optional[BaseException] = None @staticmethod def from_exception(file: str, exception: BaseException): @@ -44,8 +45,12 @@ def from_exception(file: str, exception: BaseException): msg = f'File is not a valid XML file:\n{msg}' elif msg.startswith('Invalid format.'): msg = f'File is not a valid JUnit file:\n{msg}' - return ParseError(file=file, message=msg, line=line, column=column) - return ParseError(file=file, message=str(exception)) + return ParseError(file=file, message=msg, line=line, column=column, exception=exception) + return ParseError(file=file, message=str(exception), exception=exception) + + # exceptions can be arbitrary types and might not be serializable + def without_exception(self) -> 'ParseError': + return dataclasses.replace(self, exception=None) @dataclass(frozen=True) @@ -208,7 +213,7 @@ def is_different_in_failures(self, other: 'UnitTestRunResultsOrDeltaResults'): def is_different_in_errors(self, other: 'UnitTestRunResultsOrDeltaResults'): return self.is_different(other, self._error_fields) - def with_errors(self, errors: List[ParseError]): + def with_errors(self, errors: List[ParseError]) -> 'UnitTestRunResults': return UnitTestRunResults( files=self.files, errors=errors, @@ -230,8 +235,32 @@ def with_errors(self, errors: List[ParseError]): commit=self.commit ) + # exceptions can be arbitrary types and might not be serializable + def without_exceptions(self) -> 'UnitTestRunResults': + return UnitTestRunResults( + files=self.files, + errors=[error.without_exception() for error in self.errors], + suites=self.suites, + duration=self.duration, + + tests=self.tests, + tests_succ=self.tests_succ, + tests_skip=self.tests_skip, + tests_fail=self.tests_fail, + tests_error=self.tests_error, + + runs=self.runs, + runs_succ=self.runs_succ, + runs_skip=self.runs_skip, + runs_fail=self.runs_fail, + runs_error=self.runs_error, + + commit=self.commit + ) + def to_dict(self) -> Dict[str, Any]: - return dataclasses.asdict(self) + # dict is usually used to serialize, but exceptions are likely not serializable, so we exclude them + return dataclasses.asdict(self.without_exceptions()) @staticmethod def from_dict(values: Mapping[str, Any]) -> 'UnitTestRunResults': @@ -315,7 +344,8 @@ def has_errors(self): return len(self.errors) > 0 or self.tests_error.get('number') > 0 or self.runs_error.get('number') > 0 def to_dict(self) -> Dict[str, Any]: - return dataclasses.asdict(self) + # dict is usually used to serialize, but exceptions are likely not serializable, so we exclude them + return dataclasses.asdict(self.without_exceptions()) def without_delta(self) -> UnitTestRunResults: def v(value: Numeric) -> int: @@ -329,6 +359,31 @@ def d(value: Numeric) -> int: runs=v(self.runs), runs_succ=v(self.runs_succ), runs_skip=v(self.runs_skip), runs_fail=v(self.runs_fail), runs_error=v(self.runs_error), commit=self.commit) + def without_exceptions(self) -> 'UnitTestRunDeltaResults': + return UnitTestRunDeltaResults( + files=self.files, + errors=[error.without_exception() for error in self.errors], + suites=self.suites, + duration=self.duration, + + tests=self.tests, + tests_succ=self.tests_succ, + tests_skip=self.tests_skip, + tests_fail=self.tests_fail, + tests_error=self.tests_error, + + runs=self.runs, + runs_succ=self.runs_succ, + runs_skip=self.runs_skip, + runs_fail=self.runs_fail, + runs_error=self.runs_error, + + commit=self.commit, + + reference_type=self.reference_type, + reference_commit=self.reference_commit + ) + UnitTestRunResultsOrDeltaResults = Union[UnitTestRunResults, UnitTestRunDeltaResults] diff --git a/python/publish/xunit.py b/python/publish/xunit.py index b0a961d8..c67d588c 100644 --- a/python/publish/xunit.py +++ b/python/publish/xunit.py @@ -1,29 +1,19 @@ -import os import pathlib from typing import Iterable, Callable from lxml import etree -from publish.junit import JUnitTreeOrException, ParsedJUnitFile +from publish.junit import JUnitTree, ParsedJUnitFile, progress_safe_parse_xml_file -with (pathlib.Path(__file__).parent / 'xslt' / 'xunit-to-junit.xslt').open('r', encoding='utf-8') as r: +with (pathlib.Path(__file__).resolve().parent / 'xslt' / 'xunit-to-junit.xslt').open('r', encoding='utf-8') as r: transform_xunit_to_junit = etree.XSLT(etree.parse(r)) def parse_xunit_files(files: Iterable[str], progress: Callable[[ParsedJUnitFile], ParsedJUnitFile] = lambda x: x) -> Iterable[ParsedJUnitFile]: """Parses xunit files.""" - def parse(path: str) -> JUnitTreeOrException: - """Parses an xunit file and returns either a JUnitTree or an Exception.""" - if not os.path.exists(path): - return FileNotFoundError(f'File does not exist.') - if os.stat(path).st_size == 0: - return Exception(f'File is empty.') + def parse(path: str) -> JUnitTree: + xunit = etree.parse(path) + return transform_xunit_to_junit(xunit) - try: - xunit = etree.parse(path) - return transform_xunit_to_junit(xunit) - except BaseException as e: - return e - - return [progress((result_file, parse(result_file))) for result_file in files] + return progress_safe_parse_xml_file(files, parse, progress) diff --git a/python/publish_unit_test_results.py b/python/publish_unit_test_results.py index b3fe67fc..5f678c09 100644 --- a/python/publish_unit_test_results.py +++ b/python/publish_unit_test_results.py @@ -144,7 +144,7 @@ def main(settings: Settings, gha: GithubAction) -> None: # get the unit test results parsed = parse_files(settings, gha) - [gha.error(message=f'Error processing result file: {error.message}', file=error.file, line=error.line, column=error.column) + [gha.error(message=f'Error processing result file: {error.message}', file=error.file, line=error.line, column=error.column, exception=error.exception) for error in parsed.errors] # process the parsed results diff --git a/python/test/files/junit-xml/empty.exception b/python/test/files/junit-xml/empty.exception index 8d536046..d069ba1b 100644 --- a/python/test/files/junit-xml/empty.exception +++ b/python/test/files/junit-xml/empty.exception @@ -1 +1 @@ -Exception: 'File is empty.' \ No newline at end of file +ParseError: file='files/junit-xml/empty.xml', message='File is empty.', line=None, column=None, exception=Exception('File is empty.') \ No newline at end of file diff --git a/python/test/files/junit-xml/non-junit.results b/python/test/files/junit-xml/non-junit.results index 306fec7d..53be9694 100644 --- a/python/test/files/junit-xml/non-junit.results +++ b/python/test/files/junit-xml/non-junit.results @@ -3,7 +3,8 @@ publish.unittestresults.ParsedUnitTestResults( errors=[ publish.unittestresults.ParseError( file='non-junit.xml', - message='Invalid format.' + message='Invalid format.', + exception=junitparser.junitparser.JUnitXmlError('Invalid format.') ) ], suites=0, diff --git a/python/test/files/junit-xml/non-xml.exception b/python/test/files/junit-xml/non-xml.exception index 91e92f96..510d2150 100644 --- a/python/test/files/junit-xml/non-xml.exception +++ b/python/test/files/junit-xml/non-xml.exception @@ -1 +1 @@ -XMLSyntaxError: "Start tag expected, '<' not found, line 1, column 1" \ No newline at end of file +ParseError: file='files/junit-xml/non-xml.xml', message="Start tag expected, '<' not found, line 1, column 1 (non-xml.xml, line 1)", line=None, column=None, exception=XMLSyntaxError("Start tag expected, '<' not found, line 1, column 1") \ No newline at end of file diff --git a/python/test/files/junit-xml/pytest/corrupt-xml.exception b/python/test/files/junit-xml/pytest/corrupt-xml.exception index 89646644..56d64931 100644 --- a/python/test/files/junit-xml/pytest/corrupt-xml.exception +++ b/python/test/files/junit-xml/pytest/corrupt-xml.exception @@ -1 +1 @@ -XMLSyntaxError: 'Premature end of data in tag skipped line 9, line 11, column 22' \ No newline at end of file +ParseError: file='files/junit-xml/pytest/corrupt-xml.xml', message='Premature end of data in tag skipped line 9, line 11, column 22 (corrupt-xml.xml, line 11)', line=None, column=None, exception=XMLSyntaxError('Premature end of data in tag skipped line 9, line 11, column 22') \ No newline at end of file diff --git a/python/test/files/nunit/nunit3/jenkins/NUnit-issue17521.exception b/python/test/files/nunit/nunit3/jenkins/NUnit-issue17521.exception index cd5720e6..9ef72eae 100644 --- a/python/test/files/nunit/nunit3/jenkins/NUnit-issue17521.exception +++ b/python/test/files/nunit/nunit3/jenkins/NUnit-issue17521.exception @@ -1 +1 @@ -XMLSyntaxError: 'Char 0x0 out of allowed range, line 33, column 16' \ No newline at end of file +ParseError: file='files/nunit/nunit3/jenkins/NUnit-issue17521.xml', message='Char 0x0 out of allowed range, line 33, column 16 (NUnit-issue17521.xml, line 33)', line=None, column=None, exception=XMLSyntaxError('Char 0x0 out of allowed range, line 33, column 16') \ No newline at end of file diff --git a/python/test/files/nunit/nunit3/jenkins/NUnit-issue47367.exception b/python/test/files/nunit/nunit3/jenkins/NUnit-issue47367.exception index 8bff9993..2657d0d9 100644 --- a/python/test/files/nunit/nunit3/jenkins/NUnit-issue47367.exception +++ b/python/test/files/nunit/nunit3/jenkins/NUnit-issue47367.exception @@ -1 +1 @@ -XMLSyntaxError: 'attributes construct error, line 5, column 109' \ No newline at end of file +ParseError: file='files/nunit/nunit3/jenkins/NUnit-issue47367.xml', message='attributes construct error, line 5, column 109 (NUnit-issue47367.xml, line 5)', line=None, column=None, exception=XMLSyntaxError('attributes construct error, line 5, column 109') \ No newline at end of file diff --git a/python/test/test_action_script.py b/python/test/test_action_script.py index 63ed3889..a69bafa3 100644 --- a/python/test/test_action_script.py +++ b/python/test/test_action_script.py @@ -18,7 +18,7 @@ get_settings, get_annotations_config, Settings, get_files, throttle_gh_request_raw, is_float, parse_files, main from test_utils import chdir -test_files_path = pathlib.Path(__file__).parent / 'files' +test_files_path = pathlib.Path(__file__).resolve().parent / 'files' event = dict(pull_request=dict(head=dict(sha='event_sha'))) @@ -99,7 +99,7 @@ def test_get_conclusion_parse_errors(self): with self.subTest(fail_on_errors=fail_on_errors, fail_on_failures=fail_on_failures): actual = get_conclusion(ParsedUnitTestResults( files=2, - errors=[ParseError(file='file', message='error')], + errors=[ParseError(file='file', message='error', exception=ValueError("Invalid value"))], suites=1, suite_tests=4, suite_skipped=1, diff --git a/python/test/test_action_yml.py b/python/test/test_action_yml.py index c5a814f3..b8fc9cec 100644 --- a/python/test/test_action_yml.py +++ b/python/test/test_action_yml.py @@ -3,7 +3,7 @@ import yaml -project_root = pathlib.Path(__file__).parent.parent.parent +project_root = pathlib.Path(__file__).resolve().parent.parent.parent class TestActionYml(unittest.TestCase): diff --git a/python/test/test_cicd_yml.py b/python/test/test_cicd_yml.py index 55e5f8eb..351f859a 100644 --- a/python/test/test_cicd_yml.py +++ b/python/test/test_cicd_yml.py @@ -3,7 +3,7 @@ import yaml -project_root = pathlib.Path(__file__).parent.parent.parent +project_root = pathlib.Path(__file__).resolve().parent.parent.parent class TestActionYml(unittest.TestCase): diff --git a/python/test/test_junit.py b/python/test/test_junit.py index b997aee8..c06b9808 100644 --- a/python/test/test_junit.py +++ b/python/test/test_junit.py @@ -1,10 +1,11 @@ +import dataclasses import pathlib import re import sys import unittest from distutils.version import LooseVersion from glob import glob -from typing import Optional, Union, List +from typing import Optional, List import junitparser import prettyprinter as pp @@ -15,11 +16,12 @@ sys.path.append(str(pathlib.Path(__file__).resolve().parent)) from publish.junit import parse_junit_xml_files, process_junit_xml_elems, get_results, get_result, get_content, \ - get_message, Disabled, JUnitTree + get_message, Disabled, JUnitTreeOrParseError, ParseError from publish.unittestresults import ParsedUnitTestResults, UnitTestCase from test_utils import temp_locale -test_files_path = pathlib.Path(__file__).parent / 'files' / 'junit-xml' +test_path = pathlib.Path(__file__).resolve().parent +test_files_path = test_path / 'files' / 'junit-xml' pp.install_extras() @@ -51,7 +53,7 @@ def get_test_files() -> List[str]: raise NotImplementedError() @staticmethod - def parse_file(filename) -> Union[JUnitTree, BaseException]: + def parse_file(filename) -> JUnitTreeOrParseError: raise NotImplementedError() @staticmethod @@ -70,9 +72,11 @@ def do_test_parse_and_process_files(self, filename: str): with temp_locale(locale): actual = self.parse_file(filename) path = pathlib.Path(filename) - if isinstance(actual, BaseException): - expectation_path = path.parent / (path.stem + '.exception') + if isinstance(actual, ParseError): + # make file relative so the path in the exception file does not depend on where we checkout the sources + actual = dataclasses.replace(actual, file=pathlib.Path(actual.file).relative_to(test_path).as_posix()) actual = self.prettify_exception(actual) + expectation_path = path.parent / (path.stem + '.exception') self.assert_expectation(self.test, actual, expectation_path) else: xml_expectation_path = path.parent / (path.stem + '.junit-xml') @@ -80,7 +84,7 @@ def do_test_parse_and_process_files(self, filename: str): self.assert_expectation(self.test, actual_tree, xml_expectation_path) results_expectation_path = path.parent / (path.stem + '.results') - actual_results = process_junit_xml_elems([(self.shorten_filename(str(path.resolve().as_posix())), actual)]) + actual_results = process_junit_xml_elems([(self.shorten_filename(path.resolve().as_posix()), actual)]) self.assert_expectation(self.test, pp.pformat(actual_results, indent=2), results_expectation_path) def test_parse_and_process_files(self): @@ -93,8 +97,10 @@ def update_expectations(cls): for filename in cls.get_test_files(): print(f'- updating {filename}') actual = cls.parse_file(filename) - path = pathlib.Path(filename) - if isinstance(actual, BaseException): + path = pathlib.Path(filename).resolve() + if isinstance(actual, ParseError): + # make file relative so the path in the exception file does not depend on where we checkout the sources + actual = dataclasses.replace(actual, file=pathlib.Path(actual.file).relative_to(test_path).as_posix()) with open(path.parent / (path.stem + '.exception'), 'w', encoding='utf-8') as w: w.write(cls.prettify_exception(actual)) else: @@ -102,14 +108,15 @@ def update_expectations(cls): xml = etree.tostring(actual, encoding='utf-8', xml_declaration=True, pretty_print=True) w.write(xml.decode('utf-8')) with open(path.parent / (path.stem + '.results'), 'w', encoding='utf-8') as w: - results = process_junit_xml_elems([(cls.shorten_filename(str(path.resolve().as_posix())), actual)]) + results = process_junit_xml_elems([(cls.shorten_filename(path.resolve().as_posix()), actual)]) w.write(pp.pformat(results, indent=2)) @staticmethod def prettify_exception(exception) -> str: exception = exception.__repr__() exception = re.sub(r'\(', ': ', exception, 1) - exception = re.sub(r',?\s*\)$', '', exception) + exception = re.sub(r'file:.*/', '', exception) + exception = re.sub(r',?\s*\)\)$', ')', exception) return exception @@ -129,7 +136,7 @@ def get_test_files() -> List[str]: return glob(str(test_files_path / '**' / '*.xml'), recursive=True) @staticmethod - def parse_file(filename) -> Union[JUnitTree, BaseException]: + def parse_file(filename) -> JUnitTreeOrParseError: return list(parse_junit_xml_files([filename]))[0][1] def test_process_parse_junit_xml_files_with_no_files(self): diff --git a/python/test/test_nunit.py b/python/test/test_nunit.py index 057e079c..26ba5f33 100644 --- a/python/test/test_nunit.py +++ b/python/test/test_nunit.py @@ -2,16 +2,16 @@ import sys import unittest from glob import glob -from typing import List, Union +from typing import List sys.path.append(str(pathlib.Path(__file__).resolve().parent.parent)) sys.path.append(str(pathlib.Path(__file__).resolve().parent)) -from publish.junit import JUnitTree +from publish.junit import JUnitTreeOrParseError from publish.nunit import parse_nunit_files from test_junit import JUnitXmlParseTest -test_files_path = pathlib.Path(__file__).parent / 'files' / 'nunit' +test_files_path = pathlib.Path(__file__).resolve().parent / 'files' / 'nunit' class TestNunit(unittest.TestCase, JUnitXmlParseTest): @@ -30,7 +30,7 @@ def get_test_files() -> List[str]: return glob(str(test_files_path / '**' / '*.xml'), recursive=True) @staticmethod - def parse_file(filename) -> Union[JUnitTree, BaseException]: + def parse_file(filename) -> JUnitTreeOrParseError: return list(parse_nunit_files([filename]))[0][1] diff --git a/python/test/test_publish.py b/python/test/test_publish.py index a481c9ae..04416768 100644 --- a/python/test/test_publish.py +++ b/python/test/test_publish.py @@ -21,10 +21,10 @@ from publish.unittestresults import get_test_results from test_utils import temp_locale, d, n -test_files_path = pathlib.Path(__file__).parent / 'files' / 'junit-xml' +test_files_path = pathlib.Path(__file__).resolve().parent / 'files' / 'junit-xml' -errors = [ParseError('file', 'error', 1, 2)] +errors = [ParseError('file', 'error', 1, 2, exception=ValueError("Invalid value"))] class PublishTest(unittest.TestCase): @@ -1803,9 +1803,10 @@ def test_get_case_annotations_report_individual_runs(self): self.assertEqual(expected, annotations) def test_get_error_annotation(self): - self.assertEqual(Annotation(path='file', start_line=0, end_line=0, start_column=None, end_column=None, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', None, None))) - self.assertEqual(Annotation(path='file', start_line=12, end_line=12, start_column=None, end_column=None, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', 12, None))) - self.assertEqual(Annotation(path='file', start_line=12, end_line=12, start_column=34, end_column=34, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', 12, 34))) + self.assertEqual(Annotation(path='file', start_line=0, end_line=0, start_column=None, end_column=None, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', None, None, None))) + self.assertEqual(Annotation(path='file', start_line=12, end_line=12, start_column=None, end_column=None, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', 12, None, None))) + self.assertEqual(Annotation(path='file', start_line=12, end_line=12, start_column=34, end_column=34, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', 12, 34, None))) + self.assertEqual(Annotation(path='file', start_line=12, end_line=12, start_column=34, end_column=34, annotation_level='failure', message='message', title='Error processing result file', raw_details='file'), get_error_annotation(ParseError('file', 'message', 12, 34, ValueError('invalid value')))) def test_get_all_tests_list_annotation(self): results = UnitTestCaseResults([ diff --git a/python/test/test_publisher.py b/python/test/test_publisher.py index b543a6a8..8557a88d 100644 --- a/python/test/test_publisher.py +++ b/python/test/test_publisher.py @@ -31,7 +31,7 @@ from test_unittestresults import create_unit_test_run_results -errors = [ParseError('file', 'error', 1, 2)] +errors = [ParseError('file', 'error', 1, 2, exception=ValueError("Invalid value"))] @dataclasses.dataclass(frozen=True) @@ -1455,7 +1455,7 @@ def test_publish_check_with_multiple_annotation_pages(self): conclusion='conclusion', stats=UnitTestRunResults( files=12345, - errors=[ParseError('file', 'message', 1, 2)], + errors=[ParseError('file', 'message', 1, 2, exception=ValueError("Invalid value"))], suites=2, duration=3456, tests=4, tests_succ=5, tests_skip=6, tests_fail=7, tests_error=8901, @@ -1464,7 +1464,10 @@ def test_publish_check_with_multiple_annotation_pages(self): ), stats_with_delta=UnitTestRunDeltaResults( files={'number': 1234, 'delta': -1234}, - errors=[ParseError('file', 'message', 1, 2), ParseError('file2', 'message2', 2, 4)], + errors=[ + ParseError('file', 'message', 1, 2, exception=ValueError("Invalid value")), + ParseError('file2', 'message2', 2, 4) + ], suites={'number': 2, 'delta': -2}, duration={'number': 3456, 'delta': -3456}, tests={'number': 4, 'delta': -4}, tests_succ={'number': 5, 'delta': -5}, diff --git a/python/test/test_readme_md.py b/python/test/test_readme_md.py index 5b19e068..83ad3107 100644 --- a/python/test/test_readme_md.py +++ b/python/test/test_readme_md.py @@ -2,9 +2,9 @@ import unittest import yaml -import os -project_root = pathlib.Path(__file__).parent.parent.parent +project_root = pathlib.Path(__file__).resolve().parent.parent.parent + class TestActionYml(unittest.TestCase): diff --git a/python/test/test_trx.py b/python/test/test_trx.py index 8a59653b..78f84087 100644 --- a/python/test/test_trx.py +++ b/python/test/test_trx.py @@ -7,11 +7,11 @@ sys.path.append(str(pathlib.Path(__file__).resolve().parent.parent)) sys.path.append(str(pathlib.Path(__file__).resolve().parent)) -from publish.junit import JUnitTree +from publish.junit import JUnitTreeOrParseError from publish.trx import parse_trx_files from test_junit import JUnitXmlParseTest -test_files_path = pathlib.Path(__file__).parent / 'files' / 'trx' +test_files_path = pathlib.Path(__file__).resolve().parent / 'files' / 'trx' class TestTrx(unittest.TestCase, JUnitXmlParseTest): @@ -30,7 +30,7 @@ def get_test_files() -> List[str]: return glob(str(test_files_path / '**' / '*.trx'), recursive=True) @staticmethod - def parse_file(filename) -> Union[JUnitTree, BaseException]: + def parse_file(filename) -> JUnitTreeOrParseError: return list(parse_trx_files([filename]))[0][1] diff --git a/python/test/test_unittestresults.py b/python/test/test_unittestresults.py index c8301908..c4e44675 100644 --- a/python/test/test_unittestresults.py +++ b/python/test/test_unittestresults.py @@ -9,8 +9,8 @@ UnitTestRunResults, UnitTestRunDeltaResults, ParseError from test_utils import d, n -errors = [ParseError('file', 'error')] -errors_dict = [dataclasses.asdict(e) for e in errors] +errors = [ParseError('file', 'error', exception=ValueError("Invalid value"))] +errors_dict = [dataclasses.asdict(e.without_exception()) for e in errors] def create_unit_test_run_results(files=1, @@ -64,21 +64,28 @@ def test_parse_error_from_xml_parse_error(self): error.code = 123 error.position = (1, 2) actual = ParseError.from_exception('file', error) - expected = ParseError('file', 'xml parse error', 1, 2) + expected = ParseError('file', 'xml parse error', 1, 2, exception=error) self.assertEqual(expected, actual) def test_parse_error_from_file_not_found(self): error = FileNotFoundError(2, 'No such file or directory') error.filename = 'some file path' actual = ParseError.from_exception('file', error) - expected = ParseError('file', "[Errno 2] No such file or directory: 'some file path'") + expected = ParseError('file', "[Errno 2] No such file or directory: 'some file path'", exception=error) self.assertEqual(expected, actual) def test_parse_error_from_error(self): - actual = ParseError.from_exception('file', ValueError('error')) - expected = ParseError('file', 'error') + error = ValueError('error') + actual = ParseError.from_exception('file', error) + expected = ParseError('file', 'error', exception=error) self.assertEqual(expected, actual) + def test_parse_error_with_exception(self): + error = ValueError('error') + actual = ParseError.from_exception('file', error) + expected = ParseError('file', 'error', exception=None) + self.assertEqual(expected, actual.without_exception()) + def test_parsed_unit_test_results_with_commit(self): self.assertEqual( ParsedUnitTestResultsWithCommit( @@ -112,6 +119,16 @@ def test_parsed_unit_test_results_with_commit(self): ).with_commit('commit sha') ) + def test_unit_test_run_results_without_exception(self): + results = create_unit_test_run_results(errors=errors) + self.assertEqual(create_unit_test_run_results(errors=[error.without_exception() for error in errors]), + results.without_exceptions()) + + def test_unit_test_run_delta_results_without_exception(self): + results = create_unit_test_run_delta_results(errors=errors) + self.assertEqual(create_unit_test_run_delta_results(errors=[error.without_exception() for error in errors]), + results.without_exceptions()) + def test_unit_test_run_results_to_dict(self): actual = UnitTestRunResults( files=1, errors=errors, suites=2, duration=3, diff --git a/python/test/test_xunit.py b/python/test/test_xunit.py index 82047f07..62fefeb0 100644 --- a/python/test/test_xunit.py +++ b/python/test/test_xunit.py @@ -2,17 +2,17 @@ import sys import unittest from glob import glob -from typing import List, Union +from typing import List sys.path.append(str(pathlib.Path(__file__).resolve().parent.parent)) sys.path.append(str(pathlib.Path(__file__).resolve().parent.parent.parent)) -from publish.junit import JUnitTree +from publish.junit import JUnitTreeOrParseError from publish.xunit import parse_xunit_files from test_junit import JUnitXmlParseTest -test_files_path = pathlib.Path(__file__).parent / 'files' / 'xunit' +test_files_path = pathlib.Path(__file__).resolve().parent / 'files' / 'xunit' class TestXunit(unittest.TestCase, JUnitXmlParseTest): @@ -31,7 +31,7 @@ def get_test_files() -> List[str]: return glob(str(test_files_path / '**' / '*.xml'), recursive=True) @staticmethod - def parse_file(filename) -> Union[JUnitTree, BaseException]: + def parse_file(filename) -> JUnitTreeOrParseError: return list(parse_xunit_files([filename]))[0][1]