#!/usr/bin/env bash
set -euo pipefail
# Convert xccov output to SonarQube Generic Coverage format
# Supports both line coverage and branch coverage
#
# xccov output format:
# /path/to/file.swift:
# 10: * <- non-executable line
# 11: 0 <- uncovered line
# 12: 5 <- covered line (executed 5 times)
# 13: 2 [ <- covered line with branch info
# (33, 9, 0) <- branch at column 33: 9 hits on one path, 0 on other
# ]
#
# Branch tuple format: (column, count1, count2)
# - count1 > 0 means first branch path was executed
# - count2 > 0 means second branch path was executed
function convert_xccov_to_xml {
awk '
BEGIN {
current_file = ""
in_branch_block = 0
}
# Match file path line (ends with :)
/:$/ && !/^ *[0-9]+:/ {
# Close previous file if any
if (current_file != "") {
print " "
}
# Extract path (remove trailing colon) and escape ampersands for XML
path = substr($0, 1, length($0) - 1)
gsub(/&/, "\\&", path)
print " "
current_file = path
in_branch_block = 0
next
}
# Match branch info opening: " NN: X ["
/^ *[0-9]+: [0-9]+ \[$/ {
# Parse line number (field 1 without colon)
line_num = $1
sub(/:$/, "", line_num)
# Parse execution count (field 2)
exec_count = $2 + 0
current_line = line_num
current_exec = exec_count
total_branches = 0
covered_branches = 0
in_branch_block = 1
next
}
# Match branch tuple: "(column, count1, count2)"
/^\([0-9]+, [0-9]+, [0-9]+\)$/ {
if (in_branch_block) {
# Remove parentheses
line = $0
gsub(/[()]/, "", line)
# Split by ", "
n = split(line, vals, ", ")
if (n >= 3) {
count1 = vals[2] + 0
count2 = vals[3] + 0
# Each tuple represents 2 branches
total_branches += 2
if (count1 > 0) covered_branches++
if (count2 > 0) covered_branches++
}
}
next
}
# Match branch block closing: "]"
/^\]$/ {
if (in_branch_block) {
covered = (current_exec > 0) ? "true" : "false"
if (total_branches > 0) {
printf " \n", current_line, covered, total_branches, covered_branches
} else {
printf " \n", current_line, covered
}
in_branch_block = 0
}
next
}
# Match uncovered line: " NN: 0" (no branch info)
/^ *[0-9]+: 0$/ {
line_num = $1
sub(/:$/, "", line_num)
printf " \n", line_num
next
}
# Match covered line without branch info: " NN: X" where X > 0
/^ *[0-9]+: [1-9][0-9]*$/ {
line_num = $1
sub(/:$/, "", line_num)
printf " \n", line_num
next
}
# Empty line - end of file section
/^$/ {
if (current_file != "") {
print " "
current_file = ""
}
next
}
END {
# Close last file if needed
if (current_file != "") {
print " "
}
}
'
}
function convert_xccov_to_coverage_xml {
local xcresult="$1"
echo ''
xcrun xccov view --archive "$xcresult" | convert_xccov_to_xml
echo ''
}
function is_xcode_version_supported() {
local major=${1:-0} minor=${2:-0}
# Return 0 (success) if version is supported, 1 (failure) if not
if (( (major >= 14) || (major == 13 && minor >= 3) )); then
return 0 # supported
else
return 1 # not supported
fi
}
# Ensure required tools are available
command -v xcrun >/dev/null 2>&1 || { echo >&2 "xcrun is required but not installed."; exit 1; }
command -v xcodebuild >/dev/null 2>&1 || { echo >&2 "xcodebuild is required but not installed."; exit 1; }
# Get Xcode version
if ! xcode_version="$(xcodebuild -version | sed -n '1s/^Xcode \([0-9.]*\)$/\1/p')"; then
echo 'Failed to get Xcode version' 1>&2
exit 1
elif ! is_xcode_version_supported ${xcode_version//./ }; then
echo "Xcode version '$xcode_version' not supported, version 13.3 or above is required" 1>&2;
exit 1
fi
# Validate input
if [[ $# -ne 1 ]]; then
echo "Invalid number of arguments. Expecting 1 path matching '*.xcresult'"
exit 1
fi
xcresult="$1"
if [[ ! -d "$xcresult" ]]; then
echo "Path not found: $xcresult" 1>&2;
exit 1
elif [[ $xcresult != *".xcresult"* ]]; then
echo "Expecting input to match '*.xcresult', got: $xcresult" 1>&2;
exit 1
fi
# Perform conversion
convert_xccov_to_coverage_xml "$xcresult"