diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 000000000..fcd96beab --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,385 @@ +name: SonarCloud Analysis + +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + sonarcloud: + name: SonarCloud Scan + runs-on: ubuntu-latest + steps: + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + cache: 'gradle' + + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + + - name: Configure Gradle memory settings + run: | + echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError" >> gradle.properties + echo "android.enableJetifier=false" >> gradle.properties + echo "android.enableR8.fullMode=false" >> gradle.properties + cat gradle.properties + + - name: Build project and run tests with coverage + run: | + # Build the project + ./gradlew assembleDebug --stacktrace + + # Run tests with coverage - allow test failures + ./gradlew testDebugUnitTest jacocoTestReport --stacktrace + TEST_RESULT=$? + if [ $TEST_RESULT -ne 0 ]; then + echo "Some tests failed, but continuing to check for coverage data..." + # Even if tests fail, JaCoCo should generate a report with partial coverage + # from the tests that did pass + fi + + # Check if the report was generated with content + if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then + # Use stat command compatible with both Linux and macOS + if [[ "$(uname)" == "Darwin" ]]; then + # macOS syntax + REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + else + # Linux syntax + REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + fi + + if [ "$REPORT_SIZE" -lt 1000 ]; then + echo "JaCoCo report is too small ($REPORT_SIZE bytes), likely empty. Trying to generate from test execution data..." + # Try to generate the report directly from the exec file + if [ -f build/jacoco/testDebugUnitTest.exec ]; then + java -jar ~/.gradle/caches/modules-2/files-2.1/org.jacoco/org.jacoco.cli/0.8.8/*/org.jacoco.cli-0.8.8.jar report build/jacoco/testDebugUnitTest.exec \ + --classfiles build/intermediates/runtime_library_classes_dir/debug \ + --sourcefiles src/main/java \ + --xml build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml + + # Check if the report was successfully generated + if [[ "$(uname)" == "Darwin" ]]; then + # macOS syntax + NEW_REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + else + # Linux syntax + NEW_REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) + fi + + if [ "$NEW_REPORT_SIZE" -lt 1000 ]; then + echo "ERROR: Failed to generate a valid JaCoCo report with coverage data" + exit 1 + else + echo "JaCoCo report successfully generated with size $NEW_REPORT_SIZE bytes" + fi + else + echo "ERROR: No JaCoCo execution data file found. Tests may not have run correctly." + exit 1 + fi + else + echo "JaCoCo report generated successfully with size $REPORT_SIZE bytes" + fi + else + echo "ERROR: JaCoCo report file not found. Coverage data is missing." + exit 1 + fi + + - name: Prepare class files for SonarQube analysis + run: | + echo "Searching for compiled class files..." + + # Create the target directory + mkdir -p build/intermediates/runtime_library_classes_dir/debug + + # Find all directories containing class files + CLASS_DIRS=$(find build -name "*.class" -type f -exec dirname {} \; | sort -u) + + if [ -z "$CLASS_DIRS" ]; then + echo "WARNING: No class files found in the build directory!" + else + echo "Found class files in the following directories:" + echo "$CLASS_DIRS" + + # Copy classes from the first directory with classes + FIRST_CLASS_DIR=$(echo "$CLASS_DIRS" | head -1) + echo "Copying classes from $FIRST_CLASS_DIR to build/intermediates/runtime_library_classes_dir/debug" + cp -r $FIRST_CLASS_DIR/* build/intermediates/runtime_library_classes_dir/debug/ || echo "Failed to copy from $FIRST_CLASS_DIR" + + # Verify the target directory now has class files + CLASS_COUNT=$(find build/intermediates/runtime_library_classes_dir/debug -name "*.class" | wc -l) + echo "Target directory now contains $CLASS_COUNT class files" + fi + + # Update sonar-project.properties with all found class directories as a fallback + echo "\n# Additional binary paths found during build" >> sonar-project.properties + echo "sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug,$CLASS_DIRS" >> sonar-project.properties + + echo "Checking for JaCoCo report files..." + find build -name "*.xml" | grep jacoco || echo "No XML files found" + find build -name "*.exec" | grep jacoco || echo "No exec files found" + echo "Contents of JaCoCo report directory:" + ls -la build/reports/jacoco/jacocoTestReport/ || echo "Directory not found" + + echo "\nChecking test execution results:" + find build -name "TEST-*.xml" | xargs cat | grep -E 'tests|failures|errors|skipped' || echo "No test result files found" + + echo "\nChecking JaCoCo report content:" + if [ -f build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml ]; then + echo "Report file size: $(wc -c < build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml) bytes" + echo "First 500 chars of report:" + head -c 500 build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml + echo "\n\nCounting coverage elements:" + grep -c " "build/outputs/code_coverage/debugAndroidTest/connected/$filename" + echo "Pulled coverage file to build/outputs/code_coverage/debugAndroidTest/connected/$filename" + done + + # Also try with the hardcoded package name as fallback + echo "\n[DEBUG] Searching in /data/data/io.split.android.android_client/:" + adb shell run-as io.split.android.android_client find /data/data/io.split.android.android_client -name "*.ec" | while read -r file; do + echo "Found coverage file: $file" + filename=$(basename "$file") + adb shell run-as io.split.android.android_client cat "$file" > "build/outputs/code_coverage/debugAndroidTest/connected/$filename" + echo "Pulled coverage file to build/outputs/code_coverage/debugAndroidTest/connected/$filename" + done + + # Also check sdcard location + echo "\n[DEBUG] Checking /sdcard/ location:" + adb pull /sdcard/coverage.ec build/outputs/code_coverage/debugAndroidTest/connected/ || echo "No coverage file at /sdcard/coverage.ec" + + # Try alternative locations + echo "\n[DEBUG] Checking alternative locations for coverage files:" + adb pull /data/local/tmp/coverage.ec build/outputs/code_coverage/debugAndroidTest/connected/ || echo "No coverage file at /data/local/tmp/coverage.ec" + + # List all coverage files to verify + echo "\n[DEBUG] Listing all found coverage files:" + find build -name "*.ec" -o -name "*.exec" + + # Run the JaCoCo report task with debug info + echo "\n[DEBUG] Running JaCoCo Android test report task with debug info:" + ./gradlew jacocoAndroidTestReport --debug > jacoco_debug.log + REPORT_RESULT=$? + + # Save the important parts of the debug log + echo "\n[DEBUG] Extracting coverage-related info from debug log:" + grep -A 10 -B 2 "JaCoCo" jacoco_debug.log > jacoco_important.log || echo "No JaCoCo info found in debug log" + grep -A 5 -B 2 "coverage" jacoco_debug.log >> jacoco_important.log || echo "No coverage info found in debug log" + grep -A 3 -B 1 "exec" jacoco_debug.log >> jacoco_important.log || echo "No exec info found in debug log" + grep -A 3 -B 1 "ec" jacoco_debug.log >> jacoco_important.log || echo "No .ec info found in debug log" + + echo "\n[DEBUG] Important JaCoCo debug info:" + cat jacoco_important.log + + if [ $REPORT_RESULT -ne 0 ]; then + echo "\n[DEBUG] Failed to generate Android test coverage report, but continuing..." + echo "\n[DEBUG] Last 20 lines of debug log:" + tail -20 jacoco_debug.log + else + echo "\n[DEBUG] JaCoCo Android test report generated successfully" + fi + + # Check if the Android test report was generated with content + if [ -f build/reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml ]; then + # Use stat command compatible with both Linux and macOS + if [[ "$(uname)" == "Darwin" ]]; then + # macOS syntax + REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml) + else + # Linux syntax + REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml) + fi + + echo "Android test JaCoCo report size: $REPORT_SIZE bytes" + + if [ "$REPORT_SIZE" -lt 1000 ]; then + echo "WARNING: Android test JaCoCo report is too small, likely empty" + else + echo "Android test JaCoCo report generated successfully" + fi + else + echo "WARNING: Android test JaCoCo report file not found" + fi + continue-on-error: true + + - name: Generate combined coverage report + run: | + # Generate combined report - allow failures + ./gradlew jacocoCodeCoverageReport + COMBINED_RESULT=$? + if [ $COMBINED_RESULT -ne 0 ]; then + echo "Failed to generate combined report, but continuing..." + fi + + # Check if the combined report was generated with content + if [ -f build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml ]; then + # Use stat command compatible with both Linux and macOS + if [[ "$(uname)" == "Darwin" ]]; then + # macOS syntax + REPORT_SIZE=$(stat -f%z build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml) + else + # Linux syntax + REPORT_SIZE=$(stat -c%s build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml) + fi + + echo "Combined JaCoCo report size: $REPORT_SIZE bytes" + + if [ "$REPORT_SIZE" -lt 1000 ]; then + echo "WARNING: Combined JaCoCo report is too small, likely empty" + # Try to manually combine the reports + mkdir -p build/reports/jacoco/jacocoCombinedReport/ + cp build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml + else + echo "Combined JaCoCo report generated successfully" + fi + else + echo "WARNING: Combined JaCoCo report file not found, using unit test report as fallback" + mkdir -p build/reports/jacoco/jacocoCombinedReport/ + cp build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml + fi + continue-on-error: true + + - name: Check combined coverage report + run: | + echo "Checking for all coverage data files:" + find build -name "*.exec" -o -name "*.ec" | sort + + echo "\nChecking combined JaCoCo report content:" + if [ -f build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml ]; then + echo "Report file size: $(wc -c < build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml) bytes" + echo "First 500 chars of report:" + head -c 500 build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml + echo "\n\nCounting coverage elements:" + grep -c "\n\n" + println "Created empty JaCoCo report at ${reportFile.absolutePath}" + } else { + println "Found JaCoCo exec files: ${execFiles.files}" + // If we have exec files but no report, we could use JaCoCo's ant task to generate one + // This is a simplified version - in a real scenario you'd use the JaCoCo ant task + } + + // Check if the report exists and log its content + def reportFile = new File(reportDir, "jacocoTestReport.xml") + if (reportFile.exists()) { + println "\n==== JaCoCo Report Content ====" + println "Report file size: ${reportFile.length()} bytes" + + if (reportFile.length() > 0) { + def xmlContent = reportFile.text + println "First 500 chars of report: ${xmlContent.take(500)}..." + + // Count packages, classes, and methods + def packageCount = (xmlContent =~ / + if (file.exists()) { + logger.lifecycle("Found JaCoCo execution data file: $file") + } + } + } + } +} + +// Custom task for running filtered Android instrumented tests +tasks.register('connectedFilteredAndroidTest') { + doLast { + def testClass = project.hasProperty('testClass') ? project.getProperty('testClass') : 'tests.database.DatabaseInitializationTest' + exec { + commandLine android.getAdbExecutable().toString(), 'shell', 'am', 'instrument', '-w', '-e', 'class', testClass, + '-e', 'coverage', 'true', "${android.defaultConfig.testInstrumentationRunner}/androidx.test.runner.AndroidJUnitRunner" + } + + // Create directory for coverage files + def coverageDir = new File("${buildDir}/outputs/code_coverage/debugAndroidTest/connected/") + coverageDir.mkdirs() + + // Pull coverage file from device + exec { + commandLine android.getAdbExecutable().toString(), 'pull', '/sdcard/coverage.ec', + "${buildDir}/outputs/code_coverage/debugAndroidTest/connected/" + ignoreExitValue true + } + } +} + +// Comprehensive JaCoCo coverage report task +tasks.register('jacocoCodeCoverageReport', JacocoReport) { + group = 'Reporting' + description = 'Generate Jacoco coverage report for both unit and instrumented tests' + + // Only depend on unit tests - instrumented tests should be run separately + dependsOn 'testDebugUnitTest' + + reports { + xml.required = true + html.required = true + csv.required = false + } + + // Use the defined exclusions + def debugTree = fileTree(dir: "${buildDir}/intermediates/runtime_library_classes_dir/debug", excludes: coverageExclusions) + def kotlinTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: coverageExclusions) + + sourceDirectories.from(files(['src/main/java', 'src/main/kotlin'].findAll { new File(it).exists() })) + classDirectories.from(files([debugTree, kotlinTree])) + + // Include all execution data files + executionData.from(fileTree(dir: "$buildDir", includes: [ + 'jacoco/testDebugUnitTest.exec', + 'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec', + '**/*.exec', + '**/*.ec' + ])) + + // Ensure this task always runs and doesn't get skipped + outputs.upToDateWhen { false } + + doFirst { + // Log a warning if no execution data files exist + def execFiles = executionData.files + executionData = executionData.filter { it.exists() } + if (execFiles.isEmpty() || !execFiles.any { it.exists() }) { + logger.warn("JaCoCo will not generate coverage report because no execution data files were found") + } else { + execFiles.each { file -> + if (file.exists()) { + logger.lifecycle("Found JaCoCo execution data file: $file") + } + } + } + } +} + +// Keep the original tasks for backward compatibility +tasks.register('jacocoAndroidTestReport', JacocoReport) { + dependsOn 'connectedDebugAndroidTest' + + reports { + xml.required = true + html.required = true + csv.required = false + } + + def debugTree = fileTree(dir: "${buildDir}/intermediates/runtime_library_classes_dir/debug", excludes: coverageExclusions) + + sourceDirectories.from(files(['src/main/java'])) + classDirectories.from(files([debugTree])) + executionData.from(fileTree(dir: "$buildDir", includes: [ + 'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec', + '**/*.exec', + '**/*.ec' + ])) + + // Ensure this task always runs and doesn't get skipped + outputs.upToDateWhen { false } + + doFirst { + // Log a warning if no execution data files exist + def execFiles = executionData.files + if (execFiles.isEmpty() || !execFiles.any { it.exists() }) { + logger.warn("JaCoCo will not generate coverage report because no execution data files were found") + } else { + execFiles.each { file -> + if (file.exists()) { + logger.lifecycle("Found JaCoCo execution data file: $file") + } + } + } + } +} + +// Combined coverage report task +tasks.register('jacocoCombinedReport', JacocoReport) { + dependsOn 'testDebugUnitTest', 'connectedDebugAndroidTest' + + reports { + xml.required = true + html.required = true + csv.required = false + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] + def debugTree = fileTree(dir: "${buildDir}/intermediates/runtime_library_classes_dir/debug", excludes: fileFilter) + + sourceDirectories.from(files(['src/main/java'])) + classDirectories.from(files([debugTree])) + executionData.from(fileTree(dir: "$buildDir", includes: [ + 'jacoco/testDebugUnitTest.exec', + 'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec', + '**/*.exec', + '**/*.ec' + ])) + + // Ensure this task always runs and doesn't get skipped + outputs.upToDateWhen { false } + + doFirst { + // Log a warning if no execution data files exist + def execFiles = executionData.files + if (execFiles.isEmpty() || !execFiles.any { it.exists() }) { + logger.warn("JaCoCo will not generate coverage report because no execution data files were found") + } else { + execFiles.each { file -> + if (file.exists()) { + logger.lifecycle("Found JaCoCo execution data file: $file") + } + } + } + } +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..8e6d67f94 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,25 @@ +# Required metadata +sonar.projectKey=splitio_android-client +sonar.projectName=android-client + +# Path to source directories +sonar.sources=src/main/java + +# Path to compiled classes +sonar.java.binaries=build/intermediates/runtime_library_classes_dir/debug + +# Path to test directories +sonar.tests=src/test/java,src/androidTest/java,src/sharedTest/java + +# Encoding of the source code +sonar.sourceEncoding=UTF-8 + +# Include test coverage reports - prioritize combined report +sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml,build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml,build/reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml,build/reports/jacoco/test/jacocoTestReport.xml,target/site/jacoco/jacoco.xml + +# Exclusions +sonar.exclusions=**/R.class,**/R$*.class,**/BuildConfig.*,**/Manifest*.*,**/*Test*.*,android/**/*.* + +# Java specific configuration +sonar.java.source=1.8 +sonar.java.target=1.8