diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b75b31b8..f097f004 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -199,7 +199,7 @@ jobs: id: test-results if: always() run: | - docker run --workdir $GITHUB_WORKSPACE --rm -e INPUT_CHECK_NAME -e INPUT_JUNIT_FILES -e INPUT_TRX_FILES -e INPUT_TIME_UNIT -e INPUT_GITHUB_TOKEN -e INPUT_GITHUB_RETRIES -e INPUT_COMMIT -e INPUT_COMMENT_TITLE -e INPUT_FAIL_ON -e INPUT_REPORT_INDIVIDUAL_RUNS -e INPUT_DEDUPLICATE_CLASSES_BY_FILE_NAME -e INPUT_IGNORE_RUNS -e INPUT_HIDE_COMMENTS -e INPUT_COMMENT_ON_PR -e INPUT_COMMENT_MODE -e INPUT_COMPARE_TO_EARLIER_COMMIT -e INPUT_PULL_REQUEST_BUILD -e INPUT_EVENT_FILE -e INPUT_EVENT_NAME -e INPUT_TEST_CHANGES_LIMIT -e INPUT_CHECK_RUN_ANNOTATIONS -e INPUT_CHECK_RUN_ANNOTATIONS_BRANCH -e INPUT_SECONDS_BETWEEN_GITHUB_READS -e INPUT_SECONDS_BETWEEN_GITHUB_WRITES -e INPUT_JSON_FILE -e INPUT_JOB_SUMMARY -e HOME -e GITHUB_JOB -e GITHUB_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_REPOSITORY_OWNER -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RETENTION_DAYS -e GITHUB_ACTOR -e GITHUB_WORKFLOW -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GITHUB_EVENT_NAME -e GITHUB_SERVER_URL -e GITHUB_API_URL -e GITHUB_GRAPHQL_URL -e GITHUB_WORKSPACE -e GITHUB_ACTION -e GITHUB_EVENT_PATH -e GITHUB_ACTION_REPOSITORY -e GITHUB_ACTION_REF -e GITHUB_PATH -e GITHUB_ENV -e GITHUB_STEP_SUMMARY -e RUNNER_OS -e RUNNER_TOOL_CACHE -e RUNNER_TEMP -e RUNNER_WORKSPACE -e ACTIONS_RUNTIME_URL -e ACTIONS_RUNTIME_TOKEN -e ACTIONS_CACHE_URL -e GITHUB_ACTIONS=true -e CI=true -v "/var/run/docker.sock":"/var/run/docker.sock" -v "$RUNNER_TEMP":"$RUNNER_TEMP" -v "$GITHUB_WORKSPACE":"$GITHUB_WORKSPACE" enricomi/publish-unit-test-result-action:latest + docker run --workdir $GITHUB_WORKSPACE --rm -e INPUT_CHECK_NAME -e INPUT_JUNIT_FILES -e INPUT_XUNIT_FILES -e INPUT_TRX_FILES -e INPUT_TIME_UNIT -e INPUT_GITHUB_TOKEN -e INPUT_GITHUB_RETRIES -e INPUT_COMMIT -e INPUT_COMMENT_TITLE -e INPUT_FAIL_ON -e INPUT_REPORT_INDIVIDUAL_RUNS -e INPUT_DEDUPLICATE_CLASSES_BY_FILE_NAME -e INPUT_IGNORE_RUNS -e INPUT_HIDE_COMMENTS -e INPUT_COMMENT_ON_PR -e INPUT_COMMENT_MODE -e INPUT_COMPARE_TO_EARLIER_COMMIT -e INPUT_PULL_REQUEST_BUILD -e INPUT_EVENT_FILE -e INPUT_EVENT_NAME -e INPUT_TEST_CHANGES_LIMIT -e INPUT_CHECK_RUN_ANNOTATIONS -e INPUT_CHECK_RUN_ANNOTATIONS_BRANCH -e INPUT_SECONDS_BETWEEN_GITHUB_READS -e INPUT_SECONDS_BETWEEN_GITHUB_WRITES -e INPUT_JSON_FILE -e INPUT_JOB_SUMMARY -e HOME -e GITHUB_JOB -e GITHUB_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_REPOSITORY_OWNER -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RETENTION_DAYS -e GITHUB_ACTOR -e GITHUB_WORKFLOW -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GITHUB_EVENT_NAME -e GITHUB_SERVER_URL -e GITHUB_API_URL -e GITHUB_GRAPHQL_URL -e GITHUB_WORKSPACE -e GITHUB_ACTION -e GITHUB_EVENT_PATH -e GITHUB_ACTION_REPOSITORY -e GITHUB_ACTION_REF -e GITHUB_PATH -e GITHUB_ENV -e GITHUB_STEP_SUMMARY -e RUNNER_OS -e RUNNER_TOOL_CACHE -e RUNNER_TEMP -e RUNNER_WORKSPACE -e ACTIONS_RUNTIME_URL -e ACTIONS_RUNTIME_TOKEN -e ACTIONS_CACHE_URL -e GITHUB_ACTIONS=true -e CI=true -v "/var/run/docker.sock":"/var/run/docker.sock" -v "$RUNNER_TEMP":"$RUNNER_TEMP" -v "$GITHUB_WORKSPACE":"$GITHUB_WORKSPACE" enricomi/publish-unit-test-result-action:latest env: INPUT_GITHUB_TOKEN: ${{ github.token }} INPUT_CHECK_NAME: Test Results (Docker Image) diff --git a/README.md b/README.md index f1e49a37..c93ee022 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ You can add this action to your GitHub workflow for ![Ubuntu Linux](https://badg uses: EnricoMi/publish-unit-test-result-action@v1 if: always() with: - junit_files: "test-results/**/*.xml" + junit_files: "test-results/junit/**/*.xml" + xunit_files: "test-results/xunit/**/*.xml" trx_files: "test-results/**/*.trx" ``` @@ -34,7 +35,8 @@ and ![Windows](https://badgen.net/badge/icon/Windows?icon=windows&label) (e.g. ` uses: EnricoMi/publish-unit-test-result-action/composite@v1 if: always() with: - junit_files: "test-results/**/*.xml" + junit_files: "test-results/junit/**/*.xml" + xunit_files: "test-results/xunit/**/*.xml" trx_files: "test-results/**/*.trx" ``` @@ -153,11 +155,11 @@ With `comment_mode: off`, the `pull-requests: write` permission is not needed. ## Configuration -Files can be selected via the `junit_files` and `trx_files` options. +Files can be selected via the `junit_files`, `xunit_files`, and `trx_files` options. They support [glob wildcards](https://docs.python.org/3/library/glob.html#glob.glob) like `*`, `**`, `?` and `[]`. The `**` wildcard matches all files and directories recursively: `./`, `./*/`, `./*/*/`, etc. -At least one of `junit_files` and `trx_files` options have to be set. +At least one of `junit_files`, `xunit_files`, and `trx_files` options have to be set. You can provide multiple file patterns, one pattern per line. Patterns starting with `!` exclude the matching files. There have to be at least one pattern starting without a `!`: @@ -174,6 +176,7 @@ See the complete list of options below. |Option|Default Value|Description| |:-----|:-----:|:----------| |`junit_files`|One of `*_files` must be set|File patterns of JUnit XML test result files. Supports `*`, `**`, `?`, and `[]`. Use multiline string for multiple patterns. Patterns starting with `!` exclude the matching files. There have to be at least one pattern starting without a `!`.| +|`xunit_files`|One of `*_files` must be set|File patterns of XUnit XML test result files. Supports `*`, `**`, `?`, and `[]`. Use multiline string for multiple patterns. Patterns starting with `!` exclude the matching files. There have to be at least one pattern starting without a `!`.| |`trx_files`|One of `*_files` must be set|File patterns of TRX test result files. Supports `*`, `**`, `?`, and `[]`. Use multiline string for multiple patterns. Patterns starting with `!` exclude the matching files. There have to be at least one pattern starting without a `!`.| |`time_unit`|`seconds`|Time values in the XML files have this unit. Supports `seconds` and `milliseconds`.| |`check_name`|`"Unit Test Results"`|An alternative name for the check result.| diff --git a/action.yml b/action.yml index 086246d6..943f40f0 100644 --- a/action.yml +++ b/action.yml @@ -28,6 +28,9 @@ inputs: junit_files: description: 'File patterns of JUnit XML test result files. Supports *, **, ?, and []. Use multiline string for multiple patterns. Patterns starting with ! exclude the matching files. There have to be at least one pattern starting without a `!`.' required: false + xunit_files: + description: 'File patterns of XUnit XML test result files. Supports *, **, ?, and []. Use multiline string for multiple patterns. Patterns starting with ! exclude the matching files. There have to be at least one pattern starting without a `!`.' + required: false trx_files: description: 'File patterns of TRX test result files. Supports *, **, ?, and []. Use multiline string for multiple patterns. Patterns starting with ! exclude the matching files. There have to be at least one pattern starting without a `!`.' required: false diff --git a/composite/action.yml b/composite/action.yml index b5bb117d..00472f40 100644 --- a/composite/action.yml +++ b/composite/action.yml @@ -28,6 +28,9 @@ inputs: junit_files: description: 'File patterns of JUnit XML test result files. Supports *, **, ?, and []. Use multiline string for multiple patterns. Patterns starting with ! exclude the matching files. There have to be at least one pattern starting without a `!`.' required: false + xunit_files: + description: 'File patterns of XUnit XML test result files. Supports *, **, ?, and []. Use multiline string for multiple patterns. Patterns starting with ! exclude the matching files. There have to be at least one pattern starting without a `!`.' + required: false trx_files: description: 'File patterns of TRX test result files. Supports *, **, ?, and []. Use multiline string for multiple patterns. Patterns starting with ! exclude the matching files. There have to be at least one pattern starting without a `!`.' required: false @@ -152,6 +155,7 @@ runs: # deprecated FILES: ${{ inputs.files }} JUNIT_FILES: ${{ inputs.junit_files }} + XUNIT_FILES: ${{ inputs.xunit_files }} TRX_FILES: ${{ inputs.trx_files }} TIME_UNIT: ${{ inputs.time_unit }} REPORT_INDIVIDUAL_RUNS: ${{ inputs.report_individual_runs }} diff --git a/python/publish/junit.py b/python/publish/junit.py index e3745d6e..253c3ab2 100644 --- a/python/publish/junit.py +++ b/python/publish/junit.py @@ -119,7 +119,7 @@ def close(self) -> Element: def parse_junit_xml_files(files: Iterable[str], drop_testcases: bool = False) -> Iterable[Tuple[str, Union[JUnitXml, BaseException]]]: - """Parses junit xml files and returns aggregated statistics as a ParsedUnitTestResults.""" + """Parses junit xml files.""" def parse(path: str) -> Union[JUnitXml, BaseException]: if not os.path.exists(path): return FileNotFoundError(f'File does not exist.') diff --git a/python/publish/publisher.py b/python/publish/publisher.py index a05caea9..74cb02cd 100644 --- a/python/publish/publisher.py +++ b/python/publish/publisher.py @@ -39,6 +39,7 @@ class Settings: fail_on_failures: bool # one of these *_files_glob must be set junit_files_glob: Optional[str] + xunit_files_glob: Optional[str] trx_files_glob: Optional[str] time_factor: float check_name: str diff --git a/python/publish/trx.py b/python/publish/trx.py index 035dad3f..147892bd 100644 --- a/python/publish/trx.py +++ b/python/publish/trx.py @@ -11,7 +11,7 @@ def parse_trx_files(files: Iterable[str]) -> Iterable[Tuple[str, Union[JUnitXml, BaseException]]]: - """Parses trx files and returns aggregated statistics as a ParsedUnitTestResults.""" + """Parses trx files.""" def parse(path: str) -> Union[JUnitXml, BaseException]: if not os.path.exists(path): return FileNotFoundError(f'File does not exist.') diff --git a/python/publish/xslt/xunit-to-junit.xslt b/python/publish/xslt/xunit-to-junit.xslt new file mode 100644 index 00000000..71fd5109 --- /dev/null +++ b/python/publish/xslt/xunit-to-junit.xslt @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + T + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <![CDATA[ + + ]]> + + + + diff --git a/python/publish/xunit.py b/python/publish/xunit.py new file mode 100644 index 00000000..080b44db --- /dev/null +++ b/python/publish/xunit.py @@ -0,0 +1,28 @@ +import os +import pathlib +from typing import Iterable, Tuple, Union + +from junitparser import JUnitXml +from lxml import etree + + +with (pathlib.Path(__file__).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]) -> Iterable[Tuple[str, Union[JUnitXml, BaseException]]]: + """Parses xunit files.""" + def parse(path: str) -> Union[JUnitXml, BaseException]: + 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: + trx = etree.parse(path) + junit = transform_xunit_to_junit(trx) + return JUnitXml.fromelem(junit.getroot()) + except BaseException as e: + return e + + return [(result_file, parse(result_file)) for result_file in files] diff --git a/python/publish_unit_test_results.py b/python/publish_unit_test_results.py index 2f48c975..04204fa7 100644 --- a/python/publish_unit_test_results.py +++ b/python/publish_unit_test_results.py @@ -75,6 +75,7 @@ def expand_glob(pattern: Optional[str], gha: GithubAction) -> List[str]: def parse_files(settings: Settings, gha: GithubAction) -> ParsedUnitTestResultsWithCommit: # expand file globs junit_files = expand_glob(settings.junit_files_glob, gha) + xunit_files = expand_glob(settings.xunit_files_glob, gha) trx_files = expand_glob(settings.trx_files_glob, gha) elems = [] @@ -82,6 +83,9 @@ def parse_files(settings: Settings, gha: GithubAction) -> ParsedUnitTestResultsW # parse files if junit_files: elems.extend(parse_junit_xml_files(junit_files, settings.ignore_runs)) + if xunit_files: + from publish.xunit import parse_xunit_files + elems.extend(parse_xunit_files(xunit_files)) if trx_files: from publish.trx import parse_trx_files elems.extend(parse_trx_files(trx_files)) @@ -275,7 +279,7 @@ def get_settings(options: dict, gha: Optional[GithubAction] = None) -> Settings: # replace with error when deprecated FILES is removed default_junit_files_glob = None if not any([get_var(f'{flavour}_FILES', options) - for flavour in ['JUNIT', 'TRX']]): + for flavour in ['JUNIT', 'XUNIT', 'TRX']]): default_junit_files_glob = '*.xml' gha.warning(f'At least one of the *_FILES options has to be set! ' f'Falling back to deprecated default "{default_junit_files_glob}"') @@ -318,6 +322,7 @@ def get_settings(options: dict, gha: Optional[GithubAction] = None) -> Settings: fail_on_errors=fail_on_errors, fail_on_failures=fail_on_failures, junit_files_glob=get_var('JUNIT_FILES', options) or get_var('FILES', options) or default_junit_files_glob, + xunit_files_glob=get_var('XUNIT_FILES', options), trx_files_glob=get_var('TRX_FILES', options), time_factor=time_factor, check_name=check_name, diff --git a/python/test/files/xunit/xunit.xml b/python/test/files/xunit/xunit.xml new file mode 100644 index 00000000..a90f339a --- /dev/null +++ b/python/test/files/xunit/xunit.xml @@ -0,0 +1,92 @@ + + + + + + + + + + Given I have entered 50 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(50) (0.0s) + And I have entered 70 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(70) (0.0s) + When I press add + -> done: Steps.WhenIPressAdd() (0.0s) + Then the result should be 120 on the screen + -> done: Steps.ThenTheResultShouldBePass(120) (0.0s) + + + + + + + + + Given I have entered 40 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(40) (0.0s) + And I have entered 50 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(50) (0.0s) + When I press add + -> done: Steps.WhenIPressAdd() (0.0s) + Then the result should be 90 on the screen + -> done: Steps.ThenTheResultShouldBePass(90) (0.0s) + + + + + + + + + Given I have entered 60 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(60) (0.0s) + And I have entered 70 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(70) (0.0s) + When I press add + -> done: Steps.WhenIPressAdd() (0.0s) + Then the result should be 130 on the screen + -> done: Steps.ThenTheResultShouldBePass(130) (0.0s) + + + + + + + + + Given I have entered 50 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(50) (0.0s) + And I have entered -1 into the calculator + -> done: Steps.GivenIHaveEnteredSomethingIntoTheCalculator(-1) (0.0s) + When I press add + -> done: Steps.WhenIPressAdd() (0.0s) + Then the result should be -50 on the screen + -> error: This is a fake failure message + + + + System.InvalidOperationException : This is a fake failure message + + + at Pickles.TestHarness.xUnit.Steps.ThenTheResultShouldBePass(Int32 result) in C:\dev\pickles-results-harness\Pickles.TestHarness\Pickles.TestHarness.xUnit\Steps.cs:line 26 + at lambda_method(Closure , IContextManager , Int32 ) + at TechTalk.SpecFlow.Bindings.MethodBinding.InvokeAction(IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration) + at TechTalk.SpecFlow.Bindings.StepDefinitionBinding.Invoke(IContextManager contextManager, ITestTracer testTracer, Object[] arguments, TimeSpan& duration) + at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments) + at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(StepArgs stepArgs) + at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.OnAfterLastStep() + at TechTalk.SpecFlow.TestRunner.CollectScenarioErrors() + at Pickles.TestHarness.xUnit.AdditionFeature.ScenarioCleanup() in C:\dev\pickles-results-harness\Pickles.TestHarness\Pickles.TestHarness.xUnit\Addition.feature.cs:line 0 + at Pickles.TestHarness.xUnit.AdditionFeature.FailToAddTwoNumbers() in c:\dev\pickles-results-harness\Pickles.TestHarness\Pickles.TestHarness.xUnit\Addition.feature:line 18 + + + + + diff --git a/python/test/test_action_script.py b/python/test/test_action_script.py index 62922a05..7ef58876 100644 --- a/python/test/test_action_script.py +++ b/python/test/test_action_script.py @@ -135,8 +135,10 @@ def test_get_var(self): @classmethod def get_settings_no_default_files(cls, junit_files_glob=None, + xunit_files_glob=None, trx_files_glob=None) -> Settings: return cls.get_settings(junit_files_glob=junit_files_glob, + xunit_files_glob=xunit_files_glob, trx_files_glob=trx_files_glob) @staticmethod @@ -152,6 +154,7 @@ def get_settings(token='token', fail_on_errors=True, fail_on_failures=True, junit_files_glob='junit-files', + xunit_files_glob='xunit-files', trx_files_glob='trx-files', time_factor=1.0, check_name='check name', @@ -183,6 +186,7 @@ def get_settings(token='token', fail_on_errors=fail_on_errors, fail_on_failures=fail_on_failures, junit_files_glob=junit_files_glob, + xunit_files_glob=xunit_files_glob, trx_files_glob=trx_files_glob, time_factor=time_factor, check_name=check_name, @@ -247,15 +251,18 @@ def test_get_settings_github_retries(self): def test_get_settings_any_files(self): for junit in [None, 'junit-file']: - for trx in [None, 'trx-file']: - with self.subTest(junit=junit, trx=trx): - any_flavour_set = any([flavour is not None for flavour in [junit, trx]]) - expected = self.get_settings(junit_files_glob=junit if any_flavour_set else '*.xml', - trx_files_glob=trx) - warnings = None if any_flavour_set else 'At least one of the *_FILES options has to be set! ' \ - 'Falling back to deprecated default "*.xml"' - - self.do_test_get_settings(JUNIT_FILES=junit, TRX_FILES=trx, expected=expected, warning=warnings) + for xunit in [None, 'xunit-file']: + for trx in [None, 'trx-file']: + with self.subTest(junit=junit, xunit=xunit, trx=trx): + any_flavour_set = any([flavour is not None for flavour in [junit, xunit, trx]]) + expected = self.get_settings(junit_files_glob=junit if any_flavour_set else '*.xml', + xunit_files_glob=xunit, + trx_files_glob=trx) + warnings = None if any_flavour_set else 'At least one of the *_FILES options has to be set! ' \ + 'Falling back to deprecated default "*.xml"' + + self.do_test_get_settings(JUNIT_FILES=junit, XUNIT_FILES=xunit, TRX_FILES=trx, + expected=expected, warning=warnings) def test_get_settings_junit_files(self): self.do_test_get_settings_no_default_files(JUNIT_FILES='file', expected=self.get_settings_no_default_files(junit_files_glob='file')) @@ -268,6 +275,16 @@ def test_get_settings_junit_files(self): self.do_test_get_settings_no_default_files(JUNIT_FILES=None, FILES='file\nfile2', expected=self.get_settings_no_default_files(junit_files_glob='file\nfile2'), warning=['Option FILES is deprecated, please use JUNIT_FILES instead!', 'At least one of the *_FILES options has to be set! Falling back to deprecated default "*.xml"']) self.do_test_get_settings_no_default_files(JUNIT_FILES=None, FILES=None, expected=self.get_settings_no_default_files(junit_files_glob='*.xml'), warning='At least one of the *_FILES options has to be set! Falling back to deprecated default "*.xml"') + def test_get_settings_xunit_files(self): + self.do_test_get_settings_no_default_files(XUNIT_FILES='file', expected=self.get_settings_no_default_files(xunit_files_glob='file')) + self.do_test_get_settings_no_default_files(XUNIT_FILES='file\nfile2', expected=self.get_settings_no_default_files(xunit_files_glob='file\nfile2')) + self.do_test_get_settings_no_default_files(XUNIT_FILES=None, expected=self.get_settings_no_default_files(xunit_files_glob=None, junit_files_glob='*.xml'), warning='At least one of the *_FILES options has to be set! Falling back to deprecated default "*.xml"') + + def test_get_settings_trx_files(self): + self.do_test_get_settings_no_default_files(TRX_FILES='file', expected=self.get_settings_no_default_files(trx_files_glob='file')) + self.do_test_get_settings_no_default_files(TRX_FILES='file\nfile2', expected=self.get_settings_no_default_files(trx_files_glob='file\nfile2')) + self.do_test_get_settings_no_default_files(TRX_FILES=None, expected=self.get_settings_no_default_files(trx_files_glob=None, junit_files_glob='*.xml'), warning='At least one of the *_FILES options has to be set! Falling back to deprecated default "*.xml"') + def test_get_settings_time_unit(self): self.do_test_get_settings(TIME_UNIT=None, expected=self.get_settings(time_factor=1.0)) self.do_test_get_settings(TIME_UNIT='milliseconds', expected=self.get_settings(time_factor=0.001)) @@ -462,7 +479,7 @@ def do_test_get_settings_no_default_files(self, expected: Settings = get_settings.__func__(), **kwargs): options = dict(**kwargs) - for flavour in ['JUNIT', 'TRX']: + for flavour in ['JUNIT', 'XUNIT', 'TRX']: if f'{flavour}_FILES' not in kwargs: options[f'{flavour}_FILES'] = None @@ -496,6 +513,7 @@ def do_test_get_settings(self, GITHUB_REPOSITORY='repo', COMMIT='commit', # defaults to get_commit_sha(event, event_name) JUNIT_FILES='junit-files', + XUNIT_FILES='xunit-files', TRX_FILES='trx-files', COMMENT_TITLE='title', # defaults to check name COMMENT_MODE='create new', # true unless 'false' @@ -780,18 +798,19 @@ def test_get_files_with_mock(self): def test_parse_files(self): gha = mock.MagicMock() settings = self.get_settings(junit_files_glob=str(test_files_path / 'junit' / '*.xml'), + xunit_files_glob=str(test_files_path / 'xunit' / '*.xml'), trx_files_glob=str(test_files_path / 'mstest' / '*.trx')) actual = parse_files(settings, gha) gha.warning.assert_not_called() gha.error.assert_not_called() - self.assertEqual(27, actual.files) + self.assertEqual(28, actual.files) self.assertEqual(4, len(actual.errors)) - self.assertEqual(24, actual.suites) - self.assertEqual(454, actual.suite_tests) + self.assertEqual(25, actual.suites) + self.assertEqual(458, actual.suite_tests) self.assertEqual(57, actual.suite_skipped) - self.assertEqual(29, actual.suite_failures) + self.assertEqual(30, actual.suite_failures) self.assertEqual(7, actual.suite_errors) self.assertEqual(2361, actual.suite_time) self.assertEqual(444, len(actual.cases)) @@ -801,13 +820,16 @@ def test_parse_files_no_matches(self): gha = mock.MagicMock() with tempfile.TemporaryDirectory() as path: missing_junit = str(pathlib.Path(path) / 'junit-not-there') + missing_xunit = str(pathlib.Path(path) / 'xunit-not-there') missing_trx = str(pathlib.Path(path) / 'trx-not-there') settings = self.get_settings(junit_files_glob=missing_junit, + xunit_files_glob=missing_xunit, trx_files_glob=missing_trx) actual = parse_files(settings, gha) gha.warning.assert_has_calls([ mock.call(f'Could not find any files for {missing_junit}'), + mock.call(f'Could not find any files for {missing_xunit}'), mock.call(f'Could not find any files for {missing_trx}') ]) gha.error.assert_not_called() diff --git a/python/test/test_publisher.py b/python/test/test_publisher.py index 6e0c85ed..d6f193ab 100644 --- a/python/test/test_publisher.py +++ b/python/test/test_publisher.py @@ -69,6 +69,7 @@ def create_settings(comment_mode=comment_mode_create, fail_on_errors=True, fail_on_failures=True, junit_files_glob='*.xml', + xunit_files_glob=None, trx_files_glob=None, time_factor=1.0, check_name='Check Name', diff --git a/python/test/test_trx.py b/python/test/test_trx.py new file mode 100644 index 00000000..931e3172 --- /dev/null +++ b/python/test/test_trx.py @@ -0,0 +1,91 @@ +import pathlib +import unittest + +from publish.junit import process_junit_xml_elems, ParsedUnitTestResults, UnitTestCase +from publish.trx import parse_trx_files + +test_files_path = pathlib.Path(__file__).parent / 'files' / 'mstest' + + +class TestTrx(unittest.TestCase): + def test_process_parse_trx_files_with_time_factor(self): + result_file = str(test_files_path / 'mstest.trx') + for time_factor in [1.0, 10.0, 60.0, 0.1, 0.001]: + with self.subTest(time_factor=time_factor): + actual = process_junit_xml_elems(parse_trx_files([result_file]), time_factor=time_factor) + self.assertEqual(actual, + ParsedUnitTestResults( + files=1, + errors=[], + suites=1, + suite_tests=4, + suite_skipped=0, + suite_failures=1, + suite_errors=0, + suite_time=int(0.1395124 * time_factor), + cases=[ + UnitTestCase( + class_name='Pickles.TestHarness.MSTest.AdditionFeature', + result_file=result_file, + test_file=None, + line=None, + test_name='AddingSeveralNumbers_40', + result='success', + content=None, + message=None, + time=0.076891 * time_factor + ), + UnitTestCase( + class_name='Pickles.TestHarness.MSTest.AdditionFeature', + result_file=result_file, + test_file=None, + line=None, + test_name='AddingSeveralNumbers_60', + result='success', + content=None, + message=None, + time=0.0111534 * time_factor + ), + UnitTestCase( + class_name='Pickles.TestHarness.MSTest.AdditionFeature', + result_file=result_file, + test_file=None, + line=None, + test_name='AddTwoNumbers', + result='success', + content=None, + message=None, + time=0.0055623 * time_factor + ), + UnitTestCase( + class_name='Pickles.TestHarness.MSTest.AdditionFeature', + result_file=result_file, + test_file=None, + line=None, + test_name='FailToAddTwoNumbers', + result='failure', + content='\n' + ' MESSAGE:\n' + ' \n' + ' Test method Pickles.TestHarness.MSTest.AdditionFeature.FailToAddTwoNumbers threw exception:\n' + ' Should.Core.Exceptions.NotEqualException: Assert.NotEqual() Failure\n' + ' \n' + ' +++++++++++++++++++\n' + ' STACK TRACE:\n' + ' \n' + ' at Pickles.TestHarness.MSTest.Steps.ThenTheResultShouldBePass(Int32 result) in C:\\dev\\pickles-results-harness\\Pickles.TestHarness\\Pickles.TestHarness.MSTest\\Steps.cs:line 28\n' + ' at lambda_method(Closure , IContextManager , Int32 )\n' + ' at TechTalk.SpecFlow.Bindings.MethodBinding.InvokeAction(IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration)\n' + ' at TechTalk.SpecFlow.Bindings.StepDefinitionBinding.Invoke(IContextManager contextManager, ITestTracer testTracer, Object[] arguments, TimeSpan& duration)\n' + ' at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments)\n' + ' at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(StepArgs stepArgs)\n' + ' at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.OnAfterLastStep()\n' + ' at TechTalk.SpecFlow.TestRunner.CollectScenarioErrors()\n' + ' at Pickles.TestHarness.MSTest.AdditionFeature.ScenarioCleanup() in C:\\dev\\pickles-results-harness\\Pickles.TestHarness\\Pickles.TestHarness.MSTest\\Addition.feature.cs:line 0\n' + ' at Pickles.TestHarness.MSTest.AdditionFeature.FailToAddTwoNumbers() in c:\\dev\\pickles-results-harness\\Pickles.TestHarness\\Pickles.TestHarness.MSTest\\Addition.feature:line 18\n' + ' ', + message=None, + time=0.0459057 * time_factor + ) + ] + )) diff --git a/python/test/test_xunit.py b/python/test/test_xunit.py new file mode 100644 index 00000000..d12c7814 --- /dev/null +++ b/python/test/test_xunit.py @@ -0,0 +1,89 @@ +import pathlib +import unittest + +from publish.junit import process_junit_xml_elems, ParsedUnitTestResults, UnitTestCase +from publish.xunit import parse_xunit_files + +test_files_path = pathlib.Path(__file__).parent / 'files' / 'xunit' + + +class TestXunit(unittest.TestCase): + def test_process_parse_xunit_files_with_time_factor(self): + result_file = str(test_files_path / 'xunit.xml') + for time_factor in [1.0, 10.0, 60.0, 0.1, 0.001]: + with self.subTest(time_factor=time_factor): + actual = process_junit_xml_elems(parse_xunit_files([result_file]), time_factor=time_factor) + print(actual) + self.assertEqual(actual, + ParsedUnitTestResults( + files=1, + errors=[], + suites=1, + suite_tests=4, + suite_skipped=0, + suite_failures=1, + suite_errors=0, + suite_time=int(0.867 * time_factor), + cases=[ + UnitTestCase( + class_name='Pickles.TestHarness.AdditionFeature', + result_file=result_file, + test_file=None, + line=None, + test_name='AddingSeveralNumbers("60","70","130",System.String[])', + result='success', + content=None, + message=None, + time=0.137 * time_factor + ), + UnitTestCase( + class_name='Pickles.TestHarness.AdditionFeature', + result_file=result_file, + test_file=None, + line=None, + test_name='AddingSeveralNumbers("40","50","90",System.String[])', + result='success', + content=None, + message=None, + time=0.009 * time_factor + ), + UnitTestCase( + class_name='Pickles.TestHarness', + result_file=result_file, + test_file=None, + line=None, + test_name='AdditionFeature.AddTwoNumbers', + result='success', + content=None, + message=None, + time=0.004 * time_factor + ), + UnitTestCase( + class_name='Pickles.TestHarness', + result_file=result_file, + test_file=None, + line=None, + test_name='AdditionFeature.FailToAddTwoNumbers', + result='failure', + content='\n' + 'MESSAGE:\n' + '\n' + '+++++++++++++++++++\n' + 'STACK TRACE:\n' + '\n' + ' at Pickles.TestHarness.xUnit.Steps.ThenTheResultShouldBePass(Int32 result) in C:\\dev\\pickles-results-harness\\Pickles.TestHarness\\Pickles.TestHarness.NUnit\\Steps.cs:line 26\nat lambda_method(Closure , IContextManager , Int32 )\n' + 'at TechTalk.SpecFlow.Bindings.MethodBinding.InvokeAction(IContextManager contextManager, Object[] arguments, ITestTracer testTracer, TimeSpan& duration)\n' + 'at TechTalk.SpecFlow.Bindings.StepDefinitionBinding.Invoke(IContextManager contextManager, ITestTracer testTracer, Object[] arguments, TimeSpan& duration)\n' + 'at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStepMatch(BindingMatch match, Object[] arguments)\n' + 'at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.ExecuteStep(StepArgs stepArgs)\n' + 'at TechTalk.SpecFlow.Infrastructure.TestExecutionEngine.OnAfterLastStep()\n' + 'at TechTalk.SpecFlow.TestRunner.CollectScenarioErrors()\n' + 'at Pickles.TestHarness.AdditionFeature.ScenarioCleanup() in C:\\dev\\pickles-results-harness\\Pickles.TestHarness\\Pickles.TestHarness.NUnit\\Addition.feature.cs:line 0\n' + 'at Pickles.TestHarness.AdditionFeature.FailToAddTwoNumbers() in c:\\dev\\pickles-results-harness\\Pickles.TestHarness\\Pickles.TestHarness.NUnit\\Addition.feature:line 18\n' + '\n' + ' ', + message=None, + time=0.028 * time_factor + ) + ] + ))