diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7fb949341..cd81cefbb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,9 @@ name: Build and Test on: push: - branches: [ main, beta ] + branches: [ main ] pull_request: - branches: [ main, beta ] + branches: [ main ] jobs: build: @@ -44,19 +44,26 @@ jobs: restore-keys: ${{ runner.os }}-sonar - name: Build and analyze - if: ${{ github.secret_source != 'Actions' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any run: | - echo "Secret source: \"${{ github.secret_source }}\"" - ./gradlew build jacocoTestReport + ./gradlew build testDebugUnitTest - - name: Build and analyze (Sonar) + - name: Run Android Instrumented Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 35 + target: default + arch: x86_64 + profile: Nexus 6 + script: ./gradlew connectedAndroidTest jacocoCombinedReport + + - name: Run Sonar if: ${{ github.secret_source == 'Actions' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build jacocoTestReport sonar + run: ./gradlew sonar - name: Upload build artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/app/build.gradle b/app/build.gradle index d033c2d32..9eec1cbc4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,12 +126,65 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { // Make sure the path is correct (if not run the unit tests and try find the .exec file that is generated after the unit tests are finished should be similar to that one) executionData.from = fileTree(dir: "$buildDir", includes: ["outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec"]) +} + +task jacocoCombinedReport(type: JacocoReport, dependsOn: ['testDebugUnitTest','connectedDebugAndroidTest']) { + // Ensure this task runs after unit and instrumented tests have completed +// dependsOn tasks.named("testDebugUnitTest") +// dependsOn tasks.named("connectedDebugAndroidTest") // Or the specific task like 'createDebugCoverageReport' + group = "Reporting" + description = "Generates a combined JaCoCo code coverage report for unit and instrumented tests." + + reports { + xml.required = true + html.required = false + csv.required = false + } + + // Paths to execution data files + def unitTestExecFile = "$buildDir/jacoco/testDebugUnitTest.exec" + // For AGP 7.x+, instrumented test .ec files are often here: + def instrumentedTestExecFiles = fileTree(dir: "$buildDir/outputs/code_coverage/debugAndroidTest/connected", includes: ["**/*.ec"]) + // For older AGP versions, the path might differ, e.g., "$buildDir/outputs/androidTest-results/connected/coverage.ec" + + executionData.from = files(unitTestExecFile, instrumentedTestExecFiles) + + // Source directories (adjust if your structure is different) + sourceDirectories.from = files( + "$projectDir/src/main/java", + "$projectDir/src/main/kotlin" // Add if you use Kotlin + ) + + // Class directories (compiled .class files) + // This is the most complex part and highly dependent on your AGP version and project setup. + // The goal is to point to the compiled classes of your 'main' source set for the 'debug' variant. + // Exclude Android generated classes, Dagger/Hilt, DataBinding, etc. + def mainClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", // Common for Kotlin projects + excludes: [ + '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', + '**/*_MembersInjector.class', '**/*_Factory.class', '**/*_Provide*Factory.class', // Dagger/Hilt + '**/*Args.class', '**/*Args$*.class', '**/*Directions.class', '**/*Directions$*.class', // Navigation Components + '**/*Binding.class', '**/BR.class', // Data Binding + '**/*JsonAdapter.class' // Moshi or similar + // Add more exclusions as needed for your project + ]) + // If you also have Java sources, you might need to add paths like: + // def javaMainClasses = fileTree(dir: "$buildDir/intermediates/javac/debug/classes", excludes: [...]) + // classDirectories.from = files(mainClasses, javaMainClasses) + classDirectories.from = files(mainClasses) + + + // Output location for the combined report + reports.html.outputLocation = file("$buildDir/reports/jacoco/combined/html") + reports.xml.outputLocation = file("$buildDir/reports/jacoco/combined/report.xml") // Common for CI/Sonar + reports.csv.outputLocation = file("$buildDir/reports/jacoco/combined/report.csv") } + sonar { properties { property "sonar.junit.reportPaths", "build/test-results/testDebugUnitTest/" - property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml" + property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/combined/report.xml" } } diff --git a/app/src/androidTest/java/org/blitzortung/android/app/MainTest.kt b/app/src/androidTest/java/org/blitzortung/android/app/MainTest.kt index a81a09dbc..f2a48bc7b 100644 --- a/app/src/androidTest/java/org/blitzortung/android/app/MainTest.kt +++ b/app/src/androidTest/java/org/blitzortung/android/app/MainTest.kt @@ -12,7 +12,6 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until -import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test @@ -27,7 +26,7 @@ class MainTest { val activityRule = ActivityScenarioRule(Main::class.java) private lateinit var uiDevice: androidx.test.uiautomator.UiDevice - private val timeout = 5000L // 5 seconds + private val timeout = 1000L // 5 seconds @Before fun setUp() { @@ -59,8 +58,14 @@ class MainTest { )?.click() uiDevice.wait( - Until.hasObject(By.res(InstrumentationRegistry.getInstrumentation().targetContext.packageName, "activity_settings")), - timeout) + Until.hasObject( + By.res( + InstrumentationRegistry.getInstrumentation().targetContext.packageName, + "activity_settings" + ) + ), + timeout + ) // Check if the settings fragment is displayed onView(withId(R.id.activity_settings)).check(matches(isDisplayed())) @@ -88,17 +93,12 @@ class MainTest { timeout ) - // Verify that the button was found - Assert.assertNotNull( - "Location permission dialog with 'Allow' button not found within ${timeout}ms. " + - "Permissions might have been already granted or the dialog has different text.", - allowButton - ) - - // Click the "allow" button - allowButton.click() + if (allowButton != null) { + // Click the "allow" button + allowButton.click() - // Optional: Wait for the dialog to disappear to ensure the action is processed. - uiDevice.wait(Until.gone(By.text(allowButtonTextPattern)), 2000L) + // Optional: Wait for the dialog to disappear to ensure the action is processed. + uiDevice.wait(Until.gone(By.text(allowButtonTextPattern)), 2000L) + } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6045a983..20014ed36 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip