Skip to content

Commit 68b8994

Browse files
authored
Add coverage report (#8)
* fix tests * remove unused files * fix getting coverage report * add sonarcloud action * update action versions * remove test * update sonarcloud action * update to arm * no accel * use ubuntu machine * update action * improve robustness * fix action * fix script * fix script * fix sonar analysis * remove logging, update sonarqube confi * revert name -> id trigger change
1 parent 6b113ef commit 68b8994

File tree

12 files changed

+271
-24
lines changed

12 files changed

+271
-24
lines changed

.github/workflows/sonarcloud.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: SonarCloud Analysis
2+
on:
3+
workflow_dispatch:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
types: [opened, synchronize, reopened]
9+
merge_group:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
sonarcloud:
16+
name: SonarCloud
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Harden Runner
21+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
22+
with:
23+
egress-policy: audit
24+
25+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
26+
with:
27+
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
28+
29+
- name: Set up JDK 17
30+
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
31+
with:
32+
java-version: "17"
33+
distribution: "temurin"
34+
cache: gradle
35+
36+
- name: Setup Android SDK
37+
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
38+
39+
# Setup KVM for hardware acceleration
40+
- name: Setup KVM
41+
run: |
42+
sudo apt-get update
43+
sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
44+
sudo adduser $USER kvm
45+
sudo chown $USER /dev/kvm
46+
sudo chmod 777 /dev/kvm
47+
48+
# Accept Android SDK licenses
49+
- name: Accept Android SDK licenses
50+
run: yes | sdkmanager --licenses || true
51+
52+
# Install required SDK components
53+
- name: Install SDK components
54+
run: |
55+
sdkmanager "platform-tools" "platforms;android-33" "system-images;android-33;google_apis;x86_64"
56+
sdkmanager --install "emulator"
57+
58+
- name: Run Instrumented Tests
59+
uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0
60+
with:
61+
api-level: 33
62+
target: google_apis
63+
arch: x86_64
64+
profile: pixel_6
65+
force-avd-creation: true
66+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -accel on -no-snapshot
67+
disable-animations: true
68+
script: adb start-server && adb wait-for-device && until adb shell getprop sys.boot_completed 2>/dev/null | grep -q '^1$'; do echo "Waiting for boot completion..."; sleep 5; done && adb devices && ./gradlew jacocoAndroidTestReport && (adb emu kill || true) && sleep 5
69+
70+
- name: SonarCloud Scan
71+
uses: SonarSource/[email protected]
72+
env:
73+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
74+
with:
75+
projectBaseDir: .
76+
args: >
77+
-Dsonar.organization=formbricks
78+
-Dsonar.projectKey=formbricks_android
79+
-Dsonar.java.binaries=android/build/tmp/kotlin-classes/debug
80+
-Dsonar.sources=android/src/main/java
81+
-Dsonar.tests=android/src/androidTest/java
82+
-Dsonar.coverage.jacoco.xmlReportPaths=android/build/reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml
83+
-Dsonar.verbose=true

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,19 @@
88
.externalNativeBuild
99
.cxx
1010
local.properties
11+
12+
# IDE
13+
.vscode/
14+
.settings/
15+
16+
# Coverage
17+
/tools/
18+
*.exec
19+
*.ec
20+
/coverage/
21+
/reports/
22+
generate-*-coverage.sh
23+
24+
# Generated files
25+
**/build/
26+
**/reports/

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies {
1717
}
1818
```
1919

20-
Enable DataBinding in your apps module build.gradle.kts:
20+
Enable DataBinding in your app's module build.gradle.kts:
2121

2222
```kotlin
2323
android {
@@ -63,6 +63,42 @@ Formbricks.logout()
6363

6464
We welcome issues and pull requests on our GitHub repository.
6565

66+
## Testing and Code Coverage
67+
68+
### Running Tests
69+
70+
To run the instrumented tests, make sure you have an Android emulator running or a physical device connected, then execute:
71+
72+
```bash
73+
./gradlew connectedDebugAndroidTest
74+
```
75+
76+
### Generating Coverage Reports
77+
78+
The SDK uses JaCoCo for code coverage reporting. To generate a coverage report for instrumented tests:
79+
80+
1. Make sure you have an Android emulator running or a physical device connected
81+
2. Run the provided script:
82+
```bash
83+
./generate-instrumented-coverage.sh
84+
```
85+
This will:
86+
- Run the instrumented tests
87+
- Generate a JaCoCo coverage report
88+
- Open the HTML report in your default browser
89+
90+
Alternatively, you can run the Gradle task directly:
91+
92+
```bash
93+
./gradlew jacocoAndroidTestReport
94+
```
95+
96+
The coverage report will be generated at:
97+
98+
```
99+
android/build/reports/jacoco/jacocoAndroidTestReport/html/index.html
100+
```
101+
66102
## License
67103

68104
This SDK is released under the MIT License.

android/build.gradle.kts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ plugins {
88
id("org.jetbrains.dokka") version "1.9.10"
99
id("jacoco")
1010
id("com.vanniktech.maven.publish") version "0.31.0"
11+
id("org.sonarqube") version "4.4.1.3373"
1112
}
1213

14+
// Import JaCoCo configuration
15+
// apply(from = "../jacoco.gradle.kts")
16+
1317
version = "1.1.0"
1418
val groupId = "com.formbricks"
1519
val artifactId = "android"
1620

21+
// Configure JaCoCo version
22+
jacoco {
23+
toolVersion = "0.8.11"
24+
}
25+
1726
android {
1827
namespace = "com.formbricks.android"
1928
compileSdk = 35
@@ -28,6 +37,7 @@ android {
2837
buildTypes {
2938
getByName("debug") {
3039
enableAndroidTestCoverage = true
40+
isTestCoverageEnabled = true // For backward compatibility
3141
}
3242
release {
3343
isMinifyEnabled = true
@@ -62,15 +72,6 @@ android {
6272
}
6373
}
6474

65-
tasks.withType<Test>().configureEach {
66-
extensions.configure<JacocoTaskExtension> {
67-
isIncludeNoLocationClasses = true
68-
excludes = listOf(
69-
"jdk.internal.*",
70-
)
71-
}
72-
}
73-
7475
dependencies {
7576
implementation(libs.androidx.core.ktx)
7677
implementation(libs.androidx.annotation)
@@ -91,7 +92,6 @@ dependencies {
9192
implementation(libs.androidx.fragment.ktx)
9293
implementation(libs.androidx.databinding.common)
9394

94-
testImplementation(libs.junit)
9595
androidTestImplementation(libs.androidx.junit)
9696
androidTestImplementation(libs.androidx.espresso.core)
9797
}
@@ -126,4 +126,63 @@ mavenPublishing {
126126
url = "https://github.com/formbricks/android"
127127
}
128128
}
129+
}
130+
131+
// Add JaCoCo tasks
132+
tasks.register<JacocoReport>("jacocoAndroidTestReport") {
133+
dependsOn("connectedDebugAndroidTest")
134+
135+
reports {
136+
xml.required.set(true)
137+
html.required.set(true)
138+
}
139+
140+
val fileFilter = listOf(
141+
"**/R.class",
142+
"**/R\$*.class",
143+
"**/BuildConfig.*",
144+
"**/Manifest*.*",
145+
"**/*Test*.*",
146+
"android/databinding/**/*.class",
147+
"android/databinding/*Binding.*",
148+
"android/BuildConfig.*",
149+
"**/*\$*.*",
150+
"**/Lambda\$*.class",
151+
"**/Lambda.class",
152+
"**/*Lambda.class",
153+
"**/*Lambda*.class",
154+
"**/*_MembersInjector.class",
155+
"**/Dagger*Component.class",
156+
"**/Dagger*Component\$*.class",
157+
"**/*Module_*Factory.class"
158+
)
159+
160+
val debugTree = fileTree(mapOf(
161+
"dir" to layout.buildDirectory.dir("tmp/kotlin-classes/debug").get().asFile,
162+
"excludes" to fileFilter
163+
))
164+
165+
val mainSrc = "${project.projectDir}/src/main/java"
166+
167+
sourceDirectories.setFrom(files(mainSrc))
168+
classDirectories.setFrom(files(debugTree))
169+
executionData.setFrom(fileTree(mapOf(
170+
"dir" to layout.buildDirectory.get().asFile,
171+
"includes" to listOf(
172+
"outputs/code_coverage/debugAndroidTest/connected/**/*.ec",
173+
"outputs/code_coverage/debugAndroidTest/connected/**/*.exec"
174+
)
175+
)))
176+
}
177+
178+
// Configure Sonar
179+
sonar {
180+
properties {
181+
property("sonar.coverage.jacoco.xmlReportPaths",
182+
layout.buildDirectory.file("reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml").get().asFile.path)
183+
}
184+
}
185+
186+
tasks.sonar {
187+
dependsOn("jacocoAndroidTestReport")
129188
}

android/src/androidTest/resources/Environment.json renamed to android/src/androidTest/assets/Environment.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@
346346
"triggers": [
347347
{
348348
"actionClass": {
349+
"id": "cm6ow6hht000isf0k39hbmi5f",
349350
"name": "Clicked the demo button"
350351
}
351352
}
File renamed without changes.

android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
66
import com.formbricks.android.api.FormbricksApi
77
import com.formbricks.android.helper.FormbricksConfig
8+
import com.formbricks.android.logger.Logger
89
import com.formbricks.android.manager.SurveyManager
910
import com.formbricks.android.manager.UserManager
1011
import org.junit.Assert.assertEquals
@@ -44,7 +45,7 @@ class FormbricksInstrumentedTest {
4445
@Test
4546
fun testFormbricks() {
4647
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
47-
assertEquals("com.formbricks.formbrickssdk.test", appContext.packageName)
48+
assertEquals("com.formbricks.android.test", appContext.packageName)
4849

4950
// Everything should be in the default state
5051
assertFalse(Formbricks.isInitialized)
@@ -90,7 +91,7 @@ class FormbricksInstrumentedTest {
9091
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = true
9192
assertFalse(SurveyManager.hasApiError)
9293
SurveyManager.refreshEnvironmentIfNeeded(true)
93-
waitForSeconds(1)
94+
waitForSeconds(3) // Increased wait time to 3 seconds
9495
assertTrue(SurveyManager.hasApiError)
9596
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = false
9697

@@ -113,9 +114,24 @@ class FormbricksInstrumentedTest {
113114

114115
// Track a known event, thus, the survey should be shown.
115116
SurveyManager.isShowingSurvey = false
117+
118+
// Track the event but don't show the survey
119+
val firstSurveyBeforeTrack = SurveyManager.filteredSurveys.firstOrNull()
120+
assertNotNull("Should have a survey before tracking", firstSurveyBeforeTrack)
121+
assertEquals("Should have the correct survey ID", surveyID, firstSurveyBeforeTrack?.id)
122+
123+
val actionClasses = SurveyManager.environmentDataHolder?.data?.data?.actionClasses ?: listOf()
124+
val clickDemoButtonAction = actionClasses.firstOrNull { it.key == "click_demo_button" }
125+
assertNotNull("Should have click_demo_button action class", clickDemoButtonAction)
126+
127+
val triggers = firstSurveyBeforeTrack?.triggers ?: listOf()
128+
val matchingTrigger = triggers.firstOrNull { it.actionClass?.id == clickDemoButtonAction?.id }
129+
assertNotNull("Survey should have matching trigger", matchingTrigger)
130+
131+
// Now track the event
116132
Formbricks.track("click_demo_button")
117133
waitForSeconds(1)
118-
assertTrue(SurveyManager.isShowingSurvey)
134+
assertTrue("Survey should be marked as showing", SurveyManager.isShowingSurvey)
119135

120136
// Validate display and response
121137
SurveyManager.onNewDisplay(surveyID)
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,42 @@
11
package com.formbricks.android
22

3+
import androidx.test.platform.app.InstrumentationRegistry
34
import com.formbricks.android.model.environment.EnvironmentDataHolder
45
import com.formbricks.android.model.environment.EnvironmentResponse
56
import com.formbricks.android.model.user.PostUserBody
67
import com.formbricks.android.model.user.UserResponse
78
import com.formbricks.android.network.FormbricksApiService
89
import com.google.gson.Gson
10+
import com.formbricks.android.model.error.SDKError
911

1012
class MockFormbricksApiService: FormbricksApiService() {
1113
private val gson = Gson()
12-
private val environmentJson = MockFormbricksApiService::class.java.getResource("/Environment.json")!!.readText()
13-
private val userJson = MockFormbricksApiService::class.java.getResource("/User.json")!!.readText()
14-
private val environment = gson.fromJson(environmentJson, EnvironmentResponse::class.java)
15-
private val user = gson.fromJson(userJson, UserResponse::class.java)
14+
private val environment: EnvironmentResponse
15+
private val user: UserResponse
1616
var isErrorResponseNeeded = false
1717

18+
init {
19+
val context = InstrumentationRegistry.getInstrumentation().context
20+
val environmentJson = context.assets.open("Environment.json").bufferedReader().readText()
21+
val userJson = context.assets.open("User.json").bufferedReader().readText()
22+
23+
environment = gson.fromJson(environmentJson, EnvironmentResponse::class.java)
24+
user = gson.fromJson(userJson, UserResponse::class.java)
25+
}
26+
1827
override fun getEnvironmentStateObject(environmentId: String): Result<EnvironmentDataHolder> {
1928
return if (isErrorResponseNeeded) {
20-
Result.failure(RuntimeException())
29+
Result.failure(SDKError.unableToRefreshEnvironment)
2130
} else {
2231
Result.success(EnvironmentDataHolder(environment.data, mapOf()))
2332
}
2433
}
2534

2635
override fun postUser(environmentId: String, body: PostUserBody): Result<UserResponse> {
2736
return if (isErrorResponseNeeded) {
28-
Result.failure(RuntimeException())
37+
Result.failure(SDKError.unableToPostResponse)
2938
} else {
3039
Result.success(user)
3140
}
32-
3341
}
34-
3542
}

android/src/main/java/com/formbricks/android/manager/SurveyManager.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ object SurveyManager {
135135
val actionClass = codeActionClasses.firstOrNull { it.key == action }
136136
val firstSurveyWithActionClass = filteredSurveys.firstOrNull { survey ->
137137
val triggers = survey.triggers ?: listOf()
138-
triggers.firstOrNull { it.actionClass?.name.equals(actionClass?.name) } != null
138+
triggers.firstOrNull { trigger ->
139+
trigger.actionClass?.name == actionClass?.name
140+
} != null
139141
}
140142

141143
if (firstSurveyWithActionClass == null) {

0 commit comments

Comments
 (0)