How to Speed Up Codecov Analysis for Xcode Projects, Revisited
In a
previous post, I outlined a method for converting Xcode's code coverage
format to a format that
Codecov can ingest. This
method relied on an open source tool called
xcresultparser
.
Since writing that post, I've found a new method that is slightly faster and — more importantly — removes the dependency on external tools, instead relying solely on tools included within Xcode's command line tools.
Codecov lists the code coverage formats it natively accepts
in its documentation here:
Supported Coverage Report Formats. Among the accepted formats is lcov
.
Xcode includes a tool called llvm-cov
, which
can be called from the command line using
xcrun
:
xcrun llvm-cov
llvm-cov
's has a number of subcommands, but the
one we're interested in is export
. This command
does the conversion we need from Xcode's native format to
the more interoperable lcov
format.
1. Run the Tests
In order to use the export
command, we'll need
to gather a couple dependencies that need to be passed to it
in order to do the conversion.
First, we need to know the location of the DerivedData
directory. In order to isolate your test results to just the
tests that you care about, I recommend using the
-derivedDataPath
flag to specify the location
of DerivedData explicitly in whatever
xcodebuild
command you use to run your tests.
(I usually just put it at the present working directory for
easy future reference.)
xcrun xcodebuild test -derivedDataPath ./DerivedData ...
2. Gather Information
Next, we need to locate the Profile data generated during
testing. This will be a file named
Coverage.profdata
, and will be located in a
subdirectory of DerivedData/Build/ProfileData
.
You can use a find
command to locate it like
so:
find "./DerivedData/Build/ProfileData" -name "Coverage.profdata"
# example result:
# ./DerivedData/Build/ProfileData/00006000-000420CA0CA3801E/Coverage.profdata
Finally, we need to get a list of all the test bundles
generated when running our tests. These will be files with a
.xctest
extension, and will be located in
various locations within
./DerivedData/Build/Products
. We can use a
find
command to locate them like so:
find "./DerivedData/Build/Products" -name "*.xctest"
# example result:
# ./DerivedData/Build/Products/Debug-iphoneos/MyAppUITests-Runner.app/PlugIns/MyAppUITests.xctest
# ./DerivedData/Build/Products/Debug-iphoneos/MyApp.app/PlugIns/MyAppTests.xctest
# ./DerivedData/Build/Products/Debug-iphoneos/.XCInstall/MyApp.app/Wrapper/MyApp.app/PlugIns/MyAppTests.xctest
What llvm-cov
expects is the path to the binary
within these test bundles. They will be located at the root
of each bundle, and be named the same as the bundle itself.
You can use the basename
function in bash to
get the name like so:
basename "./DerivedData/Build/Products/Debug-iphoneos/MyAppUITests-Runner.app/PlugIns/MyAppUITests.xctest" .xctest
# example result:
# MyAppUITests
3. Convert!
Now that we've located that information, we can pass it to
the llvm-cov export
command. The command will
need to be run separately for each test bundle you're
handling, so we'll run it multiple times within a loop.
Here's a simple bash script that puts all the pieces together:
#!/bin/bash
set -o errexit # Exit on error
set -o nounset # Exit on unset variable
set -o pipefail # Exit on pipe failure
function main() {
declare -r coverage_profdata_path="$(
find "./DerivedData/Build/ProfileData" -name "Coverage.profdata"
)"
declare -r xctest_bundles="$(
find "./DerivedData/Build/Products" -name "*.xctest"
)"
# Loop through each test bundle and convert the coverage data to lcov format
while IFS= read -r test_bundle; do
# Get the test bundle name
test_bundle_name="$(basename "$test_bundle" .xctest)"
# Get the test bundle binary path
test_bundle_binary_path="${test_bundle}/${test_bundle_name}"
# Define where the converted coverage data will be output
output_path="./artifacts/${test_bundle_name}.coverage.info"
# Convert the coverage data to lcov format
xcrun llvm-cov export --format lcov \
--instr-profile "${coverage_profdata_path}" \
"${test_bundle_binary_path}" >"${output_path}"
done <<<"${xctest_bundles}"
}
main "$@"
This script will produce a number of text files with the
extension .coverage.info
within a directory
named artifacts
. From there, you can upload
these text files to Codecov and let it work its magic!
As I said before, this does run slightly faster than the
xcresultparser
method, but the biggest win is
the fact that it removes a dependency on a tool, which is
always welcome.