Skip to content

Commit

Permalink
Add xunit support
Browse files Browse the repository at this point in the history
  • Loading branch information
EnricoMi committed May 23, 2022
1 parent fa1d315 commit 9e00d5d
Show file tree
Hide file tree
Showing 15 changed files with 427 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions README.md
Expand Up @@ -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"
```

Expand All @@ -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"
```

Expand Down Expand Up @@ -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 `!`:
Expand All @@ -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.|
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions composite/action.yml
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion python/publish/junit.py
Expand Up @@ -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.')
Expand Down
1 change: 1 addition & 0 deletions python/publish/publisher.py
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion python/publish/trx.py
Expand Up @@ -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.')
Expand Down
66 changes: 66 additions & 0 deletions python/publish/xslt/xunit-to-junit.xslt
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- based on https://gist.github.com/cdroulers/e23eeb31d6c1c2cade6f680e321aed8d -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" cdata-section-elements="message stack-trace"/>
<xsl:template match="/">
<testsuites>
<xsl:for-each select="//assembly">
<testsuite>
<xsl:attribute name="name"><xsl:value-of select="@name"/></xsl:attribute>
<xsl:attribute name="tests"><xsl:value-of select="@total"/></xsl:attribute>
<xsl:attribute name="failures"><xsl:value-of select="@failed"/></xsl:attribute>
<xsl:if test="@errors">
<xsl:attribute name="errors"><xsl:value-of select="@errors"/></xsl:attribute>
</xsl:if>
<xsl:attribute name="time"><xsl:value-of select="@time"/></xsl:attribute>
<xsl:attribute name="skipped"><xsl:value-of select="@skipped"/></xsl:attribute>
<xsl:attribute name="timestamp"><xsl:value-of select="@run-date"/>T<xsl:value-of select="@run-time"/></xsl:attribute>

<xsl:for-each select="collection">
<xsl:sort select="@type" />
<testsuite>
<xsl:attribute name="name"><xsl:value-of select="@name"/></xsl:attribute>
<xsl:attribute name="tests"><xsl:value-of select="@total"/></xsl:attribute>
<xsl:attribute name="failures"><xsl:value-of select="@failed"/></xsl:attribute>
<xsl:if test="@errors">
<xsl:attribute name="errors"><xsl:value-of select="@errors"/></xsl:attribute>
</xsl:if>
<xsl:attribute name="time"><xsl:value-of select="@time"/></xsl:attribute>
<xsl:attribute name="skipped"><xsl:value-of select="@skipped"/></xsl:attribute>

<xsl:for-each select="test">
<xsl:sort select="@name"/>
<testcase>
<xsl:attribute name="name"><xsl:value-of select="@method"/></xsl:attribute>
<xsl:attribute name="time"><xsl:value-of select="@time"/></xsl:attribute>
<xsl:attribute name="classname"><xsl:value-of select="@type"/></xsl:attribute>
<xsl:if test="reason">
<skipped>
<xsl:attribute name="message"><xsl:value-of select="reason/text()"/></xsl:attribute>
</skipped>
</xsl:if>
<xsl:apply-templates select="failure"/>
</testcase>
</xsl:for-each>

</testsuite>
</xsl:for-each>

</testsuite>
</xsl:for-each>
</testsuites>
</xsl:template>

<xsl:template match="failure">
<failure>
<xsl:if test="@exception-type">
<xsl:attribute name="type"><xsl:value-of select="@exception-type"/></xsl:attribute>
</xsl:if>
<xsl:attribute name="message"><xsl:value-of select="message"/></xsl:attribute>
<xsl:text disable-output-escaping="yes">&lt;![CDATA[</xsl:text>
<xsl:value-of select="stack-trace"/>
<xsl:text disable-output-escaping="yes">]]&gt;</xsl:text>
</failure>
</xsl:template>

</xsl:stylesheet>
28 changes: 28 additions & 0 deletions 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]
7 changes: 6 additions & 1 deletion python/publish_unit_test_results.py
Expand Up @@ -75,13 +75,17 @@ 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 = []

# 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))
Expand Down Expand Up @@ -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}"')
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 9e00d5d

Please sign in to comment.