From 4d22c4cd9b150b30f6068d59e4af64b6d02887b7 Mon Sep 17 00:00:00 2001 From: Alex da Franca Date: Mon, 13 Jan 2025 10:59:27 +0100 Subject: [PATCH] 48 - Add 'configuration' property to test export Add support for sonarqube on sonarcloud.io (in order to verify, that this change doesn't break the sonarqube scan) --- .gitignore | 5 + CHANGELOG.md | 5 + CommandlineTool/main.swift | 2 +- README.md | 2 +- Sources/xcresultparser/JunitXML.swift | 47 ++++-- .../TestAssets/sonarTestExecution.xml | 2 +- ...arTestExecutionWithProjectRootAbsolute.xml | 2 +- ...arTestExecutionWithProjectRootRelative.xml | 2 +- runSonar.sh | 156 ++++++++++++++++++ testAndRunSonar.sh | 75 +++++++++ 10 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 runSonar.sh create mode 100644 testAndRunSonar.sh diff --git a/.gitignore b/.gitignore index 5824247..5870122 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,8 @@ iOSInjectionProject/ # Compiled app, ready for notarization product/ + + +.scannerwork/ +sonarqube-coverage.xml +sonarqube-testresults.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 42adf6d..bd1fea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Version 1.8.4 - 2025-01-12 +### CHANGES: +- Add 'configuration' property to test export +- Add support for sonarqube on sonarcloud.io (in order to verify, that this change doesn't break the sonarqube scan) + ## Version 1.8.3 - 2024-12-15 ### CHANGES: - Add .mm files to grep filter to resolve junit class names to files diff --git a/CommandlineTool/main.swift b/CommandlineTool/main.swift index 06ca1f9..15d591b 100644 --- a/CommandlineTool/main.swift +++ b/CommandlineTool/main.swift @@ -9,7 +9,7 @@ import ArgumentParser import Foundation import XcresultparserLib -private let marketingVersion = "1.8.3" +private let marketingVersion = "1.8.4" struct xcresultparser: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/README.md b/README.md index 4d6fd9d..6e41c55 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ You should see the tool respond like this: ``` Error: Missing expected argument '' -OVERVIEW: xcresultparser 1.8.3 +OVERVIEW: xcresultparser 1.8.4 Interpret binary .xcresult files and print summary in different formats: txt, xml, html or colored cli output. diff --git a/Sources/xcresultparser/JunitXML.swift b/Sources/xcresultparser/JunitXML.swift index b239bc9..490dd70 100644 --- a/Sources/xcresultparser/JunitXML.swift +++ b/Sources/xcresultparser/JunitXML.swift @@ -140,7 +140,12 @@ public struct JunitXML: XmlSerializable { for thisSummary in testPlanRunSummaries { for thisTestableSummary in thisSummary.testableSummaries { for group in thisTestableSummary.tests { - for testsuite in createTestSuite(group, failureSummaries: failureSummaries) { + let groupTestsuites = createTestSuite( + group, + failureSummaries: failureSummaries, + configurationName: thisSummary.name ?? "Unnamed configuration" + ) + for testsuite in groupTestsuites { testsuites.addChild(testsuite) } } @@ -184,26 +189,40 @@ public struct JunitXML: XmlSerializable { private func createTestSuite( _ group: ActionTestSummaryGroup, failureSummaries: [TestFailureIssueSummary], + configurationName: String, testDirectory: String = "" ) -> [XMLElement] { guard group.identifierString.hasSuffix(".xctest") || group.subtestGroups.isEmpty else { - return group.subtestGroups.reduce([XMLElement]()) { rslt, subGroup in + return group.subtestGroups.reduce([XMLElement]()) { + rslt, + subGroup in return rslt + createTestSuite( - subGroup, failureSummaries: failureSummaries, testDirectory: subGroup.identifierString + subGroup, + failureSummaries: failureSummaries, + configurationName: configurationName, + testDirectory: subGroup.identifierString ) } } if group.subtestGroups.isEmpty { return [ createTestSuiteFinally( - group, tests: group.subtests, failureSummaries: failureSummaries, testDirectory: testDirectory + group, + tests: group.subtests, + failureSummaries: failureSummaries, + testDirectory: testDirectory, + configurationName: configurationName ) ] } else { if testReportFormat == .sonar { var nodes = [XMLElement]() for subGroup in group.subtestGroups { - let node = subGroup.sonarFileXML(projectRoot: projectRoot, relativePathNames: relativePathNames) + let node = subGroup.sonarFileXML( + projectRoot: projectRoot, + configurationName: configurationName, + relativePathNames: relativePathNames + ) let testcases = createTestCases( for: subGroup.nameString, tests: subGroup.subtests, failureSummaries: failureSummaries ) @@ -228,11 +247,15 @@ public struct JunitXML: XmlSerializable { _ group: ActionTestSummaryGroup, tests: [ActionTestMetadata], failureSummaries: [TestFailureIssueSummary], - testDirectory: String = "" + testDirectory: String = "", + configurationName: String ) -> XMLElement { let node = testReportFormat == .sonar ? - group.sonarFileXML(projectRoot: projectRoot, relativePathNames: relativePathNames) : - group.testSuiteXML(numFormatter: numFormatter) + group.sonarFileXML( + projectRoot: projectRoot, + configurationName: configurationName, + relativePathNames: relativePathNames + ) : group.testSuiteXML(numFormatter: numFormatter) for thisTest in tests { let testcase = thisTest.xmlNode( @@ -353,9 +376,13 @@ private extension ActionTestSummaryGroup { return testsuite } - func sonarFileXML(projectRoot: URL?, relativePathNames: Bool = true) -> XMLElement { + func sonarFileXML(projectRoot: URL?, configurationName: String, relativePathNames: Bool = true) -> XMLElement { let testsuite = XMLElement(name: "file") - testsuite.addAttribute(name: "path", stringValue: classPath(in: projectRoot, relativePathNames: relativePathNames)) + testsuite.addAttribute( + name: "path", + stringValue: classPath(in: projectRoot, relativePathNames: relativePathNames) + ) + testsuite.addAttribute(name: "configuration", stringValue: configurationName) return testsuite } diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml index 4a9519f..b2d1878 100644 --- a/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecution.xml @@ -1,6 +1,6 @@ - + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml index 541d7ce..5ed5d23 100644 --- a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootAbsolute.xml @@ -1,6 +1,6 @@ - + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) diff --git a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml index 1ce006c..b990fd8 100644 --- a/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml +++ b/Tests/XcresultparserTests/TestAssets/sonarTestExecutionWithProjectRootRelative.xml @@ -1,6 +1,6 @@ - + failed - Unable to create CoverageConverter from /Users/fhaeser/Library/Developer/Xcode/DerivedData/xcresultparser-ebyquorsyljyyuchjpndxzpxmxvo/Build/Products/Debug/XcresultparserTests.xctest/Contents/Resources/Xcresultparser_XcresultparserTests.bundle/Contents/Resources/test.xcresult (/Users/fhaeser/code/xcresultparser/Tests/XcresultparserTests/XcresultparserTests.swift:108) diff --git a/runSonar.sh b/runSonar.sh new file mode 100644 index 0000000..717e668 --- /dev/null +++ b/runSonar.sh @@ -0,0 +1,156 @@ +#!/bin/sh + +usage() +{ + echo "" + echo "NAME: $0" + echo "" + echo "SYNOPSIS:" + echo "$0 [-t ] [-n ] [-p profileName]" + echo "" + echo "DESCRIPTION:" + echo " -- Script to create a SonarQube run and publish results" + echo "" + echo " The options are as follows:" + echo " -s | --sonarqube-host Host of the sonarqube instance." + echo " -p | --sonarqube-login-token The credentials to access the sonarqube instance." + echo " -k | --sonarqube-project-key The key which identifies the project in sonar." + echo " -n | --sonarqube-project-name The key of the project in sonar." + echo " -v | --app-version The version of the project." + echo " -r | --path-to-xcresult The xcresult bundle with data about tests and coverage." + echo " -h | --help This help." + echo "" +} + +sourcesPath=Sources +testsPath=Tests + +while [ "$1" != "" ]; do + case $1 in + -s | --sonarqube-host ) shift + sonarqube_host_url="$1" + ;; + -p | --sonarqube-login-token ) shift + sonarqube_login_token="$1" + ;; + -k | --sonarqube-project-key ) shift + sonarqube_project_key="$1" + ;; + -n | --sonarqube-project-name ) shift + sonarqube_project_name="$1" + ;; + -o | --sonarqube-organization ) shift + sonarqube_organization="$1" + ;; + -v | --app-version ) shift + app_project_version="$1" + ;; + -r | --path-to-xcresult ) shift + xcresultPath="$1" + ;; + -h | --help ) usage + exit + ;; + esac + shift +done + +root_path=`git rev-parse --show-toplevel` +if [ ! -z "$root_path" ] +then + cd "$root_path" +else + root_path="$(pwd)" +fi + + +sonarPath=$(which sonar-scanner) +if [ ! -x "$sonarPath" ] +then + sonarPath="/opt/homebrew/bin/sonar-scanner" + if [ ! -x "$sonarPath" ] + then + brewPath=$(which brew) + if [ ! -x "$brewPath" ] + then + brewPath="/usr/local/bin/brew" + if [ ! -x "$brewPath" ] + then + echo -e "Need brew to install the 'sonar-scanner' binary" + exit 1 + fi + fi + echo "Installing 'sonar-scanner' binary" + "$brewPath" update && "$brewPath" install sonar-scanner + sonarPath=$(which sonar-scanner) + fi +fi + +if [ ! -x "$sonarPath" ] +then + echo -e "The 'sonar-scanner' binary is not executble at: $sonarPath" + exit 1 +fi + +# We can either provide 'xcresultparser' as tool in the project's repository, +resultparserPath=xcresultparser +if [ ! -x "$resultparserPath" ] +then + # ...or we have installed it for all jobs on the agent: + resultparserPath=$(which xcresultparser) + if [ ! -x "$resultparserPath" ] + then + resultparserPath="/opt/homebrew/bin/xcresultparser" + fi +fi + +echo "-------------------\nStarting sonar scan for app_project_version (Build): $app_project_version\n-------------------" + +if [ ! -z "$xcresultPath" -a -d "$xcresultPath" -a -x "$resultparserPath" ] +then + echo "Convert xcresult to sonarqube compatible coverage xml. Path to xcresult file: $xcresultPath" + "$resultparserPath" -cq -o xml "$xcresultPath" > sonarqube-coverage.xml + + echo "Convert xcresult to sonarqube compatible Junit xml." + "$resultparserPath" -q -o xml -p "$root_path" "$xcresultPath" > sonarqube-testresults.xml +fi + +if [ -s sonarqube-coverage.xml ] +then + echo "Run sonar scanner with coverage now for (sonarqube_project_name) ${sonarqube_project_name} with $sonarPath" +# "$sonarPath" -X // with DEBUG messages + "$sonarPath" \ + -Dsonar.host.url="${sonarqube_host_url}" \ + -Dsonar.token="${sonarqube_login_token}" \ + -Dsonar.projectKey="${sonarqube_project_key}" \ + -Dsonar.projectName="${sonarqube_project_name}" \ + -Dsonar.projectVersion="${app_project_version}" \ + -Dsonar.organization="${sonarqube_organization}" \ + -Dsonar.sources="$(pwd)/${sourcesPath}" \ + -Dsonar.exclusions=**/*.html \ + -Dsonar.coverageReportPaths="sonarqube-coverage.xml" \ + -Dsonar.testExecutionReportPaths="sonarqube-testresults.xml" \ + -Dsonar.tests="${testsPath}" \ + -Dsonar.c.file.suffixes=- \ + -Dsonar.cpp.file.suffixes=- \ + -Dsonar.objc.file.suffixes=- +else + echo "Run sonar scanner without coverage for (sonarqube_project_name) ${sonarqube_project_name} with $sonarPath" + "$sonarPath" \ + -Dsonar.host.url="${sonarqube_host_url}" \ + -Dsonar.token="${sonarqube_login_token}" \ + -Dsonar.projectKey="${sonarqube_project_key}" \ + -Dsonar.projectName="${sonarqube_project_name}" \ + -Dsonar.projectVersion="${app_project_version}" \ + -Dsonar.organization="${sonarqube_organization}" \ + -Dsonar.sources="$sourcesPath" \ + -Dsonar.exclusions=**/*.html +fi +if [ $? -ne 0 ] +then + echo "-------------------\nSonar scan completed with error!\n-------------------" + exit 1 +else + echo "-------------------\nSonar scan completed!\n-------------------" + exit 0 +fi diff --git a/testAndRunSonar.sh b/testAndRunSonar.sh new file mode 100644 index 0000000..71de554 --- /dev/null +++ b/testAndRunSonar.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +usage() +{ + echo "" + echo "NAME: $0" + echo "" + echo "SYNOPSIS:" + echo "$0 [-t ]" + echo "" + echo "DESCRIPTION:" + echo " -- Run the tests and send the results to sonar." + echo " Here we define the project specific variables, which are required for the runSonar.sh script." + echo " Note that one single value, which is required is not hard coded into this file for obvious reasons:" + echo " The sonar API key needs to be sent to this script as parameter." + echo "" + echo " The options are as follows:" + echo " -t | --sonarqube-login-token Access token to connect to the sonar server." + echo " -h | --help This help" + echo "" +} + +## Default values for this app, so I can invoke this script with only one parameter for the token, which shall not be stored in the public repository. +sonarqube_host_url="https://sonarcloud.io" +sonarqube_project_key="a7ex_xcresultparser" +sonarqube_project_name="xcresultparser" +sonarqube_organization="a7ex" +skip_build=false + +while [ "$1" != "" ]; do + case $1 in + -t | --sonarqube-login-token ) shift + sonarqube_login_token="$1" + ;; + -s | --skip-build ) skip_build=true + ;; + -h | --help ) usage + exit + ;; + esac + shift +done + +if [ -z "$sonarqube_login_token" ] +then + echo "Error: Please provide the api key (access token) for the sonar account!" + exit 1 +fi + +# build the project for M1 and Intel: + +path_to_xcresults="$(pwd)/product/xcresultparser.xcresult" +if [ -d "$path_to_xcresults" ]; then + if [ "$skip_build" != true ]; then + rm -r "$path_to_xcresults" + fi +fi + +if [ ! -d "$path_to_xcresults" ]; then + /usr/bin/xcrun xcodebuild clean test -workspace .swiftpm/xcode/package.xcworkspace -scheme xcresultparser -destination "platform=macOS" -resultBundlePath "$path_to_xcresults" +fi + +app_project_version=$(grep 'marketingVersion =' CommandlineTool/main.swift | cut -d "=" -f2 | xargs) + +if [ -d "$path_to_xcresults" ] +then + sh runSonar.sh \ + -s $sonarqube_host_url \ + -p $sonarqube_login_token \ + -k $sonarqube_project_key \ + -n "$sonarqube_project_name" \ + -o "$sonarqube_organization" \ + -v "$app_project_version" \ + -r "$path_to_xcresults" +fi