Skip to content

Commit

Permalink
Add support for TRX files (#287)
Browse files Browse the repository at this point in the history
  • Loading branch information
EnricoMi committed Jun 21, 2022
1 parent b776d2f commit e633876
Show file tree
Hide file tree
Showing 121 changed files with 25,257 additions and 624 deletions.
17 changes: 7 additions & 10 deletions .github/workflows/ci-cd.yml
Expand Up @@ -202,7 +202,7 @@ jobs:
id: test-results
if: always()
run: |
docker run --workdir $GITHUB_WORKSPACE --rm -e INPUT_CHECK_NAME -e INPUT_JUNIT_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_JSON_THOUSANDS_SEPARATOR -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_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_JSON_THOUSANDS_SEPARATOR -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 Expand Up @@ -409,10 +409,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v3

- name: Copy test junit xml files
run: |
mkdir -p test-files
cp -v python/test/files/*.xml test-files/
- name: Copy test files
run: cp -rv python/test/files test-files

- name: Prepare publish action from this branch
run: |
Expand All @@ -424,7 +422,8 @@ jobs:
with:
check_name: Test Results (Test Files)
fail_on: nothing
junit_files: "test-files/**.xml"
junit_files: "test-files/junit-xml/**/*.xml"
trx_files: "test-files/trx/**/*.trx"
json_file: "tests.json"
log_level: DEBUG

Expand Down Expand Up @@ -453,9 +452,7 @@ jobs:
uses: actions/checkout@v3

- name: Copy test junit xml files
run: |
mkdir -p test-files
cp -v python/test/files/*.xml test-files/
run: cp -rv python/test/files/junit-xml test-files

- name: Prepare publish action from this branch
run: |
Expand All @@ -467,7 +464,7 @@ jobs:
with:
check_name: Test Results (Test File)
fail_on: nothing
files: "test-files/junit.gloo.standalone.xml"
files: "test-files/pytest/junit.gloo.standalone.xml"
json_file: "tests.json"
log_level: DEBUG

Expand Down
16 changes: 10 additions & 6 deletions README.md
Expand Up @@ -13,7 +13,7 @@
[![Test Results](https://gist.githubusercontent.com/EnricoMi/612cb538c14731f1a8fefe504f519395/raw/badge.svg)](https://gist.githubusercontent.com/EnricoMi/612cb538c14731f1a8fefe504f519395/raw/badge.svg)

This [GitHub Action](https://github.com/actions) analyses test result files and
publishes the results on GitHub. It supports the JUnit XML file format and runs on Linux, macOS and Windows.
publishes the results on GitHub. It supports the JUnit XML and TRX file formats, and runs on Linux, macOS and Windows.

You can add this action to your GitHub workflow for ![Ubuntu Linux](https://badgen.net/badge/icon/Ubuntu?icon=terminal&label) (e.g. `runs-on: ubuntu-latest`) runners:

Expand All @@ -23,6 +23,7 @@ You can add this action to your GitHub workflow for ![Ubuntu Linux](https://badg
if: always()
with:
junit_files: "test-results/**/*.xml"
trx_files: "test-results/**/*.trx"
```

Use this for ![macOS](https://badgen.net/badge/icon/macOS?icon=apple&label) (e.g. `runs-on: macos-latest`)
Expand All @@ -34,6 +35,7 @@ and ![Windows](https://badgen.net/badge/icon/Windows?icon=windows&label) (e.g. `
if: always()
with:
junit_files: "test-results/**/*.xml"
trx_files: "test-results/**/*.trx"
```

See the [notes on running this action as a composite action](#running-as-a-composite-action) if you run it on Windows or macOS.
Expand Down Expand Up @@ -180,10 +182,12 @@ With `comment_mode: off`, the `pull-requests: write` permission is not needed.

## Configuration

Files can be selected via the `files` option, which is optional and defaults to `*.xml` in the current working directory.
[It supports wildcards](https://docs.python.org/3/library/glob.html#glob.glob) like `*`, `**`, `?` and `[]`.
Files can be selected via the `junit_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.

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 @@ -198,11 +202,11 @@ The list of most notable options:

|Option|Default Value|Description|
|:-----|:-----:|:----------|
|`junit_files`|`*.xml`|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 `!`.|
|`junit_files`<br/>`trx_files`|At least one of these `*_files` must be set.|File patterns of JUnit XML and TRX test result files, respectively. 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 `!`.|
|`check_name`|`"Test Results"`|An alternative name for the check result.|
|`comment_title`|same as `check_name`|An alternative name for the pull request comment.|
|`comment_mode`|`always`|The action posts comments to pull requests that are associated with the commit. Set to:<br/>`always` - always comment<br/>`changes` - comment when changes w.r.t. the target branch exist<br/>`changes in failures` - when changes in the number of failures and errors exist<br/>`changes in errors` - when changes in the number of (only) errors exist<br/>`failures` - when failures or errors exist<br/>`errors` - when (only) errors exist<br/>`off` - to not create pull request comments.|
|`ignore_runs`|`false`|Does not process test run information by ignoring `<testcase>` elements in the XML files, which is useful for very large XML files. This disables any check run annotations.|
|`ignore_runs`|`false`|Does not collect test run information from the test result files, which is useful for very large files. This disables any check run annotations.|

<details>
<summary>Options related to Git and GitHub</summary>
Expand Down Expand Up @@ -651,7 +655,7 @@ If this conflicts with actions that later run Python in the same workflow (which
it is recommended to run this action as the last step in your workflow, or to run it in an isolated workflow.
Running it in an isolated workflow is similar to the workflows shown in [Use with matrix strategy](#use-with-matrix-strategy).

To run the composite action in an isolated workflow, your CI workflow should upload all test result XML files:
To run the composite action in an isolated workflow, your CI workflow should upload all test result files:

```yaml
build-and-test:
Expand Down
7 changes: 5 additions & 2 deletions action.yml
Expand Up @@ -32,8 +32,11 @@ 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
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
time_unit:
description: 'Time values in the XML files have this unit. Supports "seconds" and "milliseconds".'
description: 'Time values in the test result files have this unit. Supports "seconds" and "milliseconds".'
default: 'seconds'
required: false
report_individual_runs:
Expand All @@ -43,7 +46,7 @@ inputs:
description: 'De-duplicates classes with same name by their file name when set "true", combines test results for those classes otherwise'
required: false
ignore_runs:
description: 'Does not process test run information by ignoring <testcase> elements in the XML files, which is useful for very large XML files. This disables any check run annotations'
description: 'Does not collect test run information from the test result files, which is useful for very large files. This disables any check run annotations'
default: 'false'
required: false
hide_comments:
Expand Down
8 changes: 6 additions & 2 deletions composite/action.yml
Expand Up @@ -32,8 +32,11 @@ 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
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
time_unit:
description: 'Time values in the XML files have this unit. Supports "seconds" and "milliseconds".'
description: 'Time values in the test result files have this unit. Supports "seconds" and "milliseconds".'
default: 'seconds'
required: false
report_individual_runs:
Expand All @@ -43,7 +46,7 @@ inputs:
description: 'De-duplicates classes with same name by their file name when set "true", combines test results for those classes otherwise'
required: false
ignore_runs:
description: 'Does not process test run information by ignoring <testcase> elements in the XML files, which is useful for very large XML files. This disables any check run annotations'
description: 'Does not collect test run information from the test result files, which is useful for very large files. This disables any check run annotations'
default: 'false'
required: false
hide_comments:
Expand Down Expand Up @@ -154,6 +157,7 @@ runs:
# deprecated
FILES: ${{ inputs.files }}
JUNIT_FILES: ${{ inputs.junit_files }}
TRX_FILES: ${{ inputs.trx_files }}
TIME_UNIT: ${{ inputs.time_unit }}
REPORT_INDIVIDUAL_RUNS: ${{ inputs.report_individual_runs }}
DEDUPLICATE_CLASSES_BY_FILE_NAME: ${{ inputs.deduplicate_classes_by_file_name }}
Expand Down
50 changes: 37 additions & 13 deletions python/publish/junit.py
@@ -1,9 +1,10 @@
import math
import os
from collections import defaultdict
from typing import Optional, Iterable, Union, Any, List, Dict, Callable, Tuple
from typing import Optional, Iterable, Union, List, Dict, Callable, Tuple

import junitparser
from junitparser import Element, JUnitXml, TestCase, TestSuite, Skipped
from junitparser import Element, JUnitXml, JUnitXmlError, TestCase, TestSuite, Skipped
from junitparser.junitparser import etree

from publish.unittestresults import ParsedUnitTestResults, UnitTestCase, ParseError
Expand Down Expand Up @@ -118,12 +119,16 @@ def close(self) -> Element:
return super().close()


JUnitTree = etree.ElementTree
JUnitTreeOrException = Union[JUnitTree, BaseException]
ParsedJUnitFile = Tuple[str, JUnitTreeOrException]


def parse_junit_xml_files(files: Iterable[str],
time_factor: float = 1.0,
drop_testcases: bool = False,
progress: Callable[[Tuple[str, Union[JUnitXml, BaseException]]], Tuple[str, Union[JUnitXml, BaseException]]] = lambda x: x) -> ParsedUnitTestResults:
"""Parses junit xml files and returns aggregated statistics as a ParsedUnitTestResults."""
def parse(path: str) -> Union[JUnitXml, BaseException]:
progress: Callable[[ParsedJUnitFile], ParsedJUnitFile] = lambda x: x) -> Iterable[ParsedJUnitFile]:
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:
Expand All @@ -132,27 +137,46 @@ def parse(path: str) -> Union[JUnitXml, BaseException]:
try:
if drop_testcases:
builder = DropTestCaseBuilder()
return JUnitXml.fromfile(path, parse_func=builder.parse)
return JUnitXml.fromfile(path)
return etree.parse(path, parser=etree.XMLParser(target=builder, encoding='utf-8'))
return etree.parse(path)
except BaseException as e:
return e

parsed_files = [progress((result_file, parse(result_file))) for result_file in files]
return [progress((result_file, parse(result_file))) for result_file in files]


def process_junit_xml_elems(trees: Iterable[ParsedJUnitFile], time_factor: float = 1.0) -> ParsedUnitTestResults:
# TODO: move upstream into JUnitTree
def create_junitxml(filepath: str, tree: JUnitTree) -> Union[JUnitXml, JUnitXmlError]:
root_elem = tree.getroot()
if root_elem.tag == "testsuites":
instance = JUnitXml()
elif root_elem.tag == "testsuite":
instance = TestSuite()
else:
return JUnitXmlError("Invalid format.")
instance._elem = root_elem
instance.filepath = filepath
return instance

processed = [(result_file, create_junitxml(result_file, tree) if not isinstance(tree, BaseException) else tree)
for result_file, tree in trees]
junits = [(result_file, junit)
for result_file, junit in parsed_files
for result_file, junit in processed
if not isinstance(junit, BaseException)]
errors = [ParseError.from_exception(result_file, exception)
for result_file, exception in parsed_files
for result_file, exception in processed
if isinstance(exception, BaseException)]

suites = [(result_file, suite)
for result_file, junit in junits
for suite in (junit if junit._tag == "testsuites" else [junit])]

suite_tests = sum([suite.tests for result_file, suite in suites])
suite_skipped = sum([suite.skipped + suite.disabled for result_file, suite in suites])
suite_failures = sum([suite.failures for result_file, suite in suites])
suite_errors = sum([suite.errors for result_file, suite in suites])
suite_time = int(sum([suite.time for result_file, suite in suites]) * time_factor)
suite_time = int(sum([suite.time for result_file, suite in suites if not math.isnan(suite.time)]) * time_factor)

def int_opt(string: Optional[str]) -> Optional[int]:
try:
Expand Down Expand Up @@ -192,7 +216,7 @@ def get_cases(suite: TestSuite) -> List[TestCase]:
]

return ParsedUnitTestResults(
files=len(parsed_files),
files=len(list(trees)),
errors=errors,
# test state counts from suites
suites=len(suites),
Expand Down
4 changes: 3 additions & 1 deletion python/publish/publisher.py
Expand Up @@ -43,7 +43,9 @@ class Settings:
json_thousands_separator: str
fail_on_errors: bool
fail_on_failures: bool
junit_files_glob: str
# one of these *_files_glob must be set
junit_files_glob: Optional[str]
trx_files_glob: Optional[str]
time_factor: float
check_name: str
comment_title: str
Expand Down
28 changes: 28 additions & 0 deletions python/publish/trx.py
@@ -0,0 +1,28 @@
import os
import pathlib
from typing import Iterable, Callable

from lxml import etree

from publish.junit import JUnitTreeOrException, ParsedJUnitFile

with (pathlib.Path(__file__).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]:
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.')

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]

0 comments on commit e633876

Please sign in to comment.