Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for XUnit files #288

Merged
merged 10 commits into from Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .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_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
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_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 @@ -423,6 +423,7 @@ jobs:
check_name: Test Results (Test Files)
fail_on: nothing
junit_files: "test-files/junit-xml/**/*.xml"
xunit_files: "test-files/xunit/**/*.xml"
trx_files: "test-files/trx/**/*.trx"
json_file: "tests.json"
log_level: DEBUG
Expand Down
12 changes: 7 additions & 5 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@v2
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@v2
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 @@ -182,11 +184,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 @@ -202,7 +204,7 @@ The list of most notable options:

|Option|Default Value|Description|
|:-----|:-----:|:----------|
|`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 `!`.|
|`junit_files`<br/>`xunit_files`<br/>`trx_files`|At least one of these `*_files` must be set.|File patterns of JUnit XML, XUnit 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.|
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Expand Up @@ -32,6 +32,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 @@ -32,6 +32,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 @@ -157,6 +160,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
12 changes: 7 additions & 5 deletions python/publish/junit.py
Expand Up @@ -127,6 +127,7 @@ def close(self) -> Element:
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):
Expand Down Expand Up @@ -172,11 +173,12 @@ def create_junitxml(filepath: str, tree: JUnitTree) -> Union[JUnitXml, JUnitXmlE
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 if not math.isnan(suite.time)]) * time_factor)
suite_tests = sum([suite.tests for result_file, suite in suites if suite.tests])
suite_skipped = sum([suite.skipped + suite.disabled for result_file, suite in suites if suite.skipped])
suite_failures = sum([suite.failures for result_file, suite in suites if suite.failures])
suite_errors = sum([suite.errors for result_file, suite in suites if suite.errors])
suite_time = int(sum([suite.time for result_file, suite in suites
if suite.time and not math.isnan(suite.time)]) * time_factor)

def int_opt(string: Optional[str]) -> Optional[int]:
try:
Expand Down
1 change: 1 addition & 0 deletions python/publish/publisher.py
Expand Up @@ -45,6 +45,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
1 change: 1 addition & 0 deletions python/publish/trx.py
Expand Up @@ -12,6 +12,7 @@

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):
Expand Down
63 changes: 63 additions & 0 deletions python/publish/xslt/xunit-to-junit.xslt
@@ -0,0 +1,63 @@
<?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"/>
<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 | class">
<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">
<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:value-of select="message"/>
<xsl:value-of select="stack-trace"/>
</failure>
</xsl:template>

</xsl:stylesheet>
29 changes: 29 additions & 0 deletions python/publish/xunit.py
@@ -0,0 +1,29 @@
import os
import pathlib
from typing import Iterable, Callable

from lxml import etree
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blacklist: Using etree to parse untrusted XML data is known to be vulnerable to XML attacks. Replace etree with the equivalent defusedxml package.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opt.semgrep.python.lang.security.use-defused-xml.use-defused-xml: Found use of the native Python XML libraries, which is vulnerable to XML external entity (XXE)
attacks. The Python documentation recommends the 'defusedxml' library instead if the XML being
loaded is untrusted.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]


from publish.junit import JUnitTreeOrException, ParsedJUnitFile

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))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blacklist: Using lxml.etree.parse to parse untrusted XML data is known to be vulnerable to XML attacks. Replace lxml.etree.parse with its defusedxml equivalent function.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]



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.')

try:
trx = etree.parse(path)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blacklist: Using lxml.etree.parse to parse untrusted XML data is known to be vulnerable to XML attacks. Replace lxml.etree.parse with its defusedxml equivalent function.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

return transform_xunit_to_junit(trx)
except BaseException as e:
return e

return [progress((result_file, parse(result_file))) for result_file in files]
9 changes: 7 additions & 2 deletions python/publish_unit_test_results.py
Expand Up @@ -93,20 +93,24 @@ def get_number_of_files(files: List[str]) -> 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, log the progress
# https://github.com/EnricoMi/publish-unit-test-result-action/issues/304
with progress_logger(items=len(junit_files + trx_files),
with progress_logger(items=len(junit_files + xunit_files + trx_files),
interval_seconds=10,
progress_template='Read {progress} files in {time}',
finish_template='Finished reading {observations} files in {duration}',
progress_item_type=Tuple[str, Any],
logger=logger) as progress:
if junit_files:
elems.extend(parse_junit_xml_files(junit_files, settings.ignore_runs, progress))
if xunit_files:
from publish.xunit import parse_xunit_files
elems.extend(parse_xunit_files(xunit_files, progress))
if trx_files:
from publish.trx import parse_trx_files
elems.extend(parse_trx_files(trx_files, progress))
Expand Down Expand Up @@ -325,7 +329,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 @@ -369,6 +373,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
2 changes: 1 addition & 1 deletion python/test/files/junit-xml/tst/disabled.results
Expand Up @@ -3,7 +3,7 @@ publish.unittestresults.ParsedUnitTestResults(
errors=[],
suites=2,
suite_tests=31,
suite_skipped=5,
suite_skipped=0,
suite_failures=19,
suite_errors=1,
suite_time=0,
Expand Down
1 change: 1 addition & 0 deletions python/test/files/update_expectations.sh
Expand Up @@ -3,5 +3,6 @@
base=$(dirname "$0")

python $base/../test_junit.py
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SC2086: Double quote to prevent globbing and word splitting.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

python $base/../test_xunit.py
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SC2086: Double quote to prevent globbing and word splitting.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

python $base/../test_trx.py
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SC2086: Double quote to prevent globbing and word splitting.

(at-me in a reply with help or ignore)


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]


1 change: 1 addition & 0 deletions python/test/files/xunit/README.md
@@ -0,0 +1 @@
[mstest/fixie.xml](https://raw.githubusercontent.com/fixie/fixie/42b43dc6cc57476958eea8b507aa9d0d72cedae6/src/Fixie.Tests/Reports/XUnitXmlReport.xml)
26 changes: 26 additions & 0 deletions python/test/files/xunit/mstest/fixie.junit-xml
@@ -0,0 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites>
<testsuite name="[assemblyLocation]" tests="7" failures="3" time="1.234" skipped="1" timestamp="YYYY-MM-DDTHH:MM:SS">
<testsuite name="[genericTestClass]" tests="3" failures="1" time="1.234" skipped="0">
<testcase name="ShouldBeString" time="1.234" classname="[genericTestClass]"/>
<testcase name="ShouldBeString" time="1.234" classname="[genericTestClass]"/>
<testcase name="ShouldBeString" time="1.234" classname="[genericTestClass]">
<failure type="Fixie.Tests.Assertions.AssertException" message="Expected: System.String&#10;Actual: System.Int32">Expected: System.String
Actual: System.Int32 at [genericTestClassForStackTrace].ShouldBeString[T](T genericArgument) in [fileLocation]:line #</failure>
</testcase>
</testsuite>
<testsuite name="[testClass]" tests="4" failures="2" time="1.234" skipped="1">
<testcase name="Fail" time="1.234" classname="[testClass]">
<failure type="Fixie.Tests.FailureException" message="'Fail' failed!">'Fail' failed! at [testClassForStackTrace].Fail() in [fileLocation]:line #</failure>
</testcase>
<testcase name="FailByAssertion" time="1.234" classname="[testClass]">
<failure type="Fixie.Tests.Assertions.AssertException" message="Expected: 2&#10;Actual: 1">Expected: 2
Actual: 1 at [testClassForStackTrace].FailByAssertion() in [fileLocation]:line #</failure>
</testcase>
<testcase name="Pass" time="1.234" classname="[testClass]"/>
<testcase name="Skip" time="1.234" classname="[testClass]">
<skipped message="&#x26A0; Skipped with attribute."/>
</testcase>
</testsuite>
</testsuite>
</testsuites>