-
Notifications
You must be signed in to change notification settings - Fork 137
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
Merge junit reports into one file #12064
Conversation
To speed up test collection time
📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.
|
@@ -0,0 +1,41 @@ | |||
#!/bin/bash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AliSoftware 👋 can I have your review on this shell script? 🙂
merge_junit.sh
Outdated
# Iterate over the XML files in the directory, merging their content | ||
for report in "$reports_dir"/*.xml; do | ||
# Strip the outer <testsuites> tags and append the content to the merged file | ||
sed 's/^<.*testsuites>//; s/<.*\/testsuites>$//' "$report" >> "$output_file" | ||
done |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Why the
.*
in front of thetestsuites
tag name? - This doesn't account for the
<testsuites …>
tag oftentimes having attributes, e.g.<testsuites name='WordPressTest.xctest' tests='1885' failures='2'>
(granted, this example was taken from a.junit
file generated for an iOS project, while the JUnit file generated by Gradle and Android tools might (?) not add attributes to the<testsuites>
tag, but better cover all our bases) - This also doesn't remove the
<?xml version="1.0" …>
from the input files
I'd suggest instead to use the following sed
command, which prints only lines that are between 2 patterns, excluding said patterns:
sed -n '/^\<testsuites.*\>/,/\<\/testsuites\>$/{//!p;}' "$report" >> "$output_file"
Actually, you can also pass a list of input files to sed
directly instead of doing a for
loop, so I think something like this would work too:
# Iterate over the XML files in the directory, merging their content | |
for report in "$reports_dir"/*.xml; do | |
# Strip the outer <testsuites> tags and append the content to the merged file | |
sed 's/^<.*testsuites>//; s/<.*\/testsuites>$//' "$report" >> "$output_file" | |
done | |
# Strip the outer <testsuites> tags of each input file, and append the content to the merged file | |
sed -n '/^\<testsuites.*\>/,/\<\/testsuites\>$/{//!p;}' "$reports_dir"/*.xml >> "$output_file" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested with the following dummy files:
tests1.junit
<?xml version='1.0' encoding='UTF-8'?>
<testsuites name='WordPressTest.xctest' tests='1885' failures='2'>
<testsuite name='WordPressFluxTests.WordPressFluxTests' tests='6' failures='0'>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testQueryStore' time='0.011'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStatefulStoreEmitsChanges' time='0.002'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStatefulStoreWithoutTransaction' time='0.002'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStatefulStoreWithTransaction' time='0.003'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStoreEmitsChanges' time='0.002'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStoreReceivesActions' time='0.001'/>
</testsuite>
</testsuites>
tests2.junit
<?xml version='1.0' encoding='UTF-8'?>
<testsuites name='WordPressTest.xctest' tests='1885' failures='2'>
<testsuite name='WordPressTest.AbstractPostFixLocalMediaURLsTests' tests='4' failures='0'>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateAllLocalMediaPathsButDoesNotChangeRemotePaths' time='0.075'/>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateLocalMediaPathsInCachesDirectory' time='0.062'/>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateLocalMediaPathsInDocumentDirectory' time='0.059'/>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateMultipleLocalMediaPaths' time='0.060'/>
</testsuite>
<testsuite name='WordPressTest.AbstractPostTest' tests='2' failures='0'>
<testcase classname='WordPressTest.AbstractPostTest' name='testFeaturedImageURLForDisplay' time='0.030'/>
<testcase classname='WordPressTest.AbstractPostTest' name='testTitleForStatus' time='0.002'/>
</testsuite>
</testsuites>
tests3.junit
<?xml version='1.0' encoding='UTF-8'?>
<testsuites name='WordPressTest.xctest' tests='1885' failures='2'>
<testsuite name='WordPressTest.ZendeskUtilsPlans' tests='7' failures='0'>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testAddOnPlanSelected' time='0.031'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testBloggerPlanSelected' time='0.022'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testBusinessPlanSelected' time='0.027'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testEcommercePlanSelected' time='0.034'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testFreePlanSelected' time='0.021'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testPremiumPlanSelected' time='0.040'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testPresonalPlanSelected' time='0.018'/>
</testsuite>
</testsuites>
sed command output
$ sed -n '/^\<testsuites.*\>/,/\<\/testsuites\>$/{//!p;}' tests*.junit
<testsuite name='WordPressFluxTests.WordPressFluxTests' tests='6' failures='0'>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testQueryStore' time='0.011'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStatefulStoreEmitsChanges' time='0.002'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStatefulStoreWithoutTransaction' time='0.002'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStatefulStoreWithTransaction' time='0.003'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStoreEmitsChanges' time='0.002'/>
<testcase classname='WordPressFluxTests.WordPressFluxTests' name='testStoreReceivesActions' time='0.001'/>
</testsuite>
<testsuite name='WordPressTest.AbstractPostFixLocalMediaURLsTests' tests='4' failures='0'>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateAllLocalMediaPathsButDoesNotChangeRemotePaths' time='0.075'/>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateLocalMediaPathsInCachesDirectory' time='0.062'/>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateLocalMediaPathsInDocumentDirectory' time='0.059'/>
<testcase classname='WordPressTest.AbstractPostFixLocalMediaURLsTests' name='testUpdateMultipleLocalMediaPaths' time='0.060'/>
</testsuite>
<testsuite name='WordPressTest.AbstractPostTest' tests='2' failures='0'>
<testcase classname='WordPressTest.AbstractPostTest' name='testFeaturedImageURLForDisplay' time='0.030'/>
<testcase classname='WordPressTest.AbstractPostTest' name='testTitleForStatus' time='0.002'/>
</testsuite>
<testsuite name='WordPressTest.ZendeskUtilsPlans' tests='7' failures='0'>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testAddOnPlanSelected' time='0.031'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testBloggerPlanSelected' time='0.022'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testBusinessPlanSelected' time='0.027'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testEcommercePlanSelected' time='0.034'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testFreePlanSelected' time='0.021'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testPremiumPlanSelected' time='0.040'/>
<testcase classname='WordPressTest.ZendeskUtilsPlans' name='testPresonalPlanSelected' time='0.018'/>
</testsuite>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @AliSoftware !
This doesn't account for the <testsuites …> tag oftentimes having attributes, e.g. (granted, this example was taken from a .junit file generated for an iOS project, while the JUnit file generated by Gradle and Android tools might (?) not add attributes to the tag, but better cover all our bases)
For unit tests, there was no <testsuites>
tag before, it's artificially created only in this PR. I'm not sure if we should add attributes to this tag, as we'd have to compute their values, and this is getting complex (and probably not needed, as it wasn't here before).
This also doesn't remove the <?xml version="1.0" …> from the input files
Good point, thanks!
I'd suggest instead to use the following sed command, which prints only lines that are between 2 patterns, excluding said patterns:
This, unfortunately, won't work as there are no testsuites
tag in generated artifacts. I'm trying to figure out why the end result in merged-test-results.xml
is as expected (beside additional <?xml(...)
tags> 😬 .
Example singular report file for AddressTest
test suite.
Locally in WCAndroid you can run sh .buildkite/commands/run-unit-tests.sh
and check WooCommerce/build/test-results/merged-test-results.xml
file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, after investigating more, I think we can simplify logic to this: 1b5891a WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For unit tests, there was no tag before, it's artificially created only in this PR. I'm not sure if we should add attributes to this tag, as we'd have to compute their values, and this is getting complex (and probably not needed, as it wasn't here before).
Sorry if I was unclear, what I meant is that your regex wouldn't match <testsuites>
tag that might have attributes in the input files. Because the RegEx /^<.*testsuites>/
would not match the cases of <testsuites attribute="value">
. So that would fail to remove them from the input file
I was NOT suggesting that we'd try to report the <testsuites>
attributes of the input files in the output files. I don't think that's worth it nor that it would be easy nor useful anyway. So I'm totally ok with echo '<testsuites>' >>"$output_file"
on line 29 🙂
This, unfortunately, won't work as there are no testsuites tag in generated artifacts.
This is very suspicious, as according the the specs I found of the JUnit file format, I'd expect <testsuites>
to be the root tag of any JUnit file 🤔 But I guess as the README of this junitxml
project says, there seems to be no official spec for the JUnit file format, only a common(-ish) format convention… so maybe some tools wrap the test suites within <testsuites>
while some other tools (like Gradle?) don't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so maybe some tools wrap the test suites within while some other tools (like Gradle?) don't?
Yes, it looks like this. I want to highlight only that lack of <testsuites>
tag is present only for unit tests. The instrumented tests are reported in a single file with <testsuites>
tag 🤷♂️ .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, after investigating more, I think we can simplify logic to this: 1b5891a WDYT?
If we are sure / guaranteed that, in the context of the JUnit files generated by Android Unit Tests tooling:
- The
<testsuite>
tags of the input files will never be wrapped in a parent<testsuites>
tag (like I've seen in other JUnit files generated by other projects) - The XML processor directive will always look like
<?xml version="1.0" encoding="UTF-8"?>
(and never e.g.<?xml version="1.0" encoding="utf-8"?>
or<?xml version="1.0"?>
or<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
Then yes, this commit could work (and could even be simplified by avoiding the for
loop and passing the list of input files directly to sed
as I suggested above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we can loosen this constraint! Added all improvements 👍
Remove only <?xml header from the single-test report
Co-authored-by: Olivier Halligon <[email protected]>
So we don't have to iterate with `for` loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To assert Buildkite annotation logic works as expected
This reverts commit 46c216c.
To improve performance of Android unit tests collection. Originated it woocommerce/woocommerce-android#12064
To improve performance of Android unit tests collection. Originated it woocommerce/woocommerce-android#12064
* Introduce `merge_junit` script To improve performance of Android unit tests collection. Originated it woocommerce/woocommerce-android#12064 * Add changelog entry * Address SC2129 Consider using { cmd1; cmd2; } >> file instead of individual redirects. https://www.shellcheck.net/wiki/SC2129 * Remove `sh` suffix To align to other commands * Create output file before saving data there * Rename script to `merge_junit_reports` * Remove `<testsuites>` tag if it's present in any report that is about to be merged * Update CHANGELOG.md Co-authored-by: Olivier Halligon <[email protected]> --------- Co-authored-by: Olivier Halligon <[email protected]>
To speed up test collection time
Description
Problem to address
Collecting JUnit results for unit tests takes significant part of the unit tests job (3m for Buildkite annotations, 4m for Buildkite test collector, internal source: paaHJt-6MN-p2 )
Possible solution
I've noticed that the problem is visible in unit tests, which produces a separate
.xml
report file per tests-suite, rather than in instrumented tests, where there's a single.xml
report.Why unit tests produces multiple
.xml
reports and instrumented tests produce single?It seems it's the way AGP is organizing running instrumented tests. According to my research, there's no way to communicate to JUnit to produce a single report file, hence a simple merge logic proposed in this PR.
I think it's better to use this custom, small piece of logic than e.g. intrudcing a 3rd party npm package like https://www.npmjs.com/package/junit-report-merger
Testing information
trunk
commit that this PR is based on: 2a8a3d1unit-tests
jobView in test analytics
Test execution count
(it's 3744)Future actions
I'd like to merge this PR as it is and see how impactful this change is. After asserting the impact and lack of problems, I'd like to move
merge_junit.sh
script to our CI toolkit, to make it available for both WC and WP/JP Android projects.RELEASE-NOTES.txt
if necessary. Use the "[Internal]" label for non-user-facing changes.