diff --git a/.github/workflows/build-test-analyze.yml b/.github/workflows/build-test-analyze.yml index ee4213986..b5fe28486 100644 --- a/.github/workflows/build-test-analyze.yml +++ b/.github/workflows/build-test-analyze.yml @@ -42,7 +42,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3.13.0 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -69,6 +69,75 @@ jobs: flags: unittests name: codecov-coverage token: ${{ secrets.CODECOV_TOKEN }} + test: + name: Instrumented tests + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + api-level: [31, 34] + profile: ["pixel_6"] + target: ["default", "google_apis"] + steps: + - name: checkout + uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: gradle + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + - name: Enable KVM + 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: Gradle cache + uses: gradle/actions/setup-gradle@v3 + - name: AVD cache + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.profile }} + - name: Create AVD and Generate Snapshot for Caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + profile: ${{ matrix.profile }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: | + sdkmanager --list + avdmanager list devices + - name: Run Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + profile: ${{ matrix.profile }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: bundle exec fastlane connectedCheck + - name: Upload JaCoCo report to Codecov + uses: codecov/codecov-action@v4 + with: + files: '**/build/reports/coverage/androidTest/debug/connected/index.html' + flags: uitests + name: codecov-coverage + token: ${{ secrets.CODECOV_TOKEN }} dokka: name: Dokka Documentation Deployment runs-on: ubuntu-latest diff --git a/Gemfile.lock b/Gemfile.lock index d81f5983c..372dd3356 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,22 +5,22 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.941.0) - aws-sdk-core (3.197.0) + aws-partitions (1.947.0) + aws-sdk-core (3.199.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.83.0) - aws-sdk-core (~> 3, >= 3.197.0) + aws-sdk-kms (1.87.0) + aws-sdk-core (~> 3, >= 3.199.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.152.0) - aws-sdk-core (~> 3, >= 3.197.0) + aws-sdk-s3 (1.154.0) + aws-sdk-core (~> 3, >= 3.199.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -68,7 +68,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.220.0) + fastlane (2.221.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -152,9 +152,9 @@ GEM httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) - jwt (2.8.1) + jwt (2.8.2) base64 - mini_magick (4.12.0) + mini_magick (4.13.1) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) @@ -164,7 +164,7 @@ GEM optparse (0.5.0) os (1.1.4) plist (3.7.1) - public_suffix (5.0.5) + public_suffix (6.0.0) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) @@ -217,4 +217,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.5.9 + 2.5.11 diff --git a/build-logic/convention/src/main/kotlin/edu/stanford/spezi/build/logic/convention/plugins/SpeziBaseConfigConventionPlugin.kt b/build-logic/convention/src/main/kotlin/edu/stanford/spezi/build/logic/convention/plugins/SpeziBaseConfigConventionPlugin.kt index 3a8366c46..2808c0fdd 100644 --- a/build-logic/convention/src/main/kotlin/edu/stanford/spezi/build/logic/convention/plugins/SpeziBaseConfigConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/edu/stanford/spezi/build/logic/convention/plugins/SpeziBaseConfigConventionPlugin.kt @@ -30,6 +30,10 @@ class SpeziBaseConfigConventionPlugin : Plugin { targetCompatibility = java } + buildTypes { + getByName("debug").enableAndroidTestCoverage = true + } + packaging { resources { excludes += "/META-INF/**.md" diff --git a/build.gradle.kts b/build.gradle.kts index e603c1c5f..a37981aac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -119,9 +119,14 @@ fun Project.setupJacoco() { html.required.set(true) xml.required.set(true) } - sourceDirectories.setFrom(files("$projectDir/src/main")) - executionData.setFrom(files("$buildDir/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")) + + executionData.setFrom( + files("$buildDir/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec") + ) + doLast { + println("Jacoco report generated in: ${reports.html.outputLocation.get()}") + } } tasks.withType().configureEach { diff --git a/core/bluetooth/src/main/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScanner.kt b/core/bluetooth/src/main/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScanner.kt index 615755475..fd6b92d0e 100644 --- a/core/bluetooth/src/main/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScanner.kt +++ b/core/bluetooth/src/main/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScanner.kt @@ -70,7 +70,8 @@ internal class BLEDeviceScanner @Inject constructor( * If scanning is already in progress, this method does nothing. */ fun startScanning() { - if (_isScanning.getAndSet(true)) return + val scanner = bluetoothAdapter.bluetoothLeScanner + if (_isScanning.getAndSet(scanner != null)) return val filters = supportedServices.map { ScanFilter.Builder() .setServiceUuid(ParcelUuid(it.service)) @@ -79,7 +80,7 @@ internal class BLEDeviceScanner @Inject constructor( val settings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() - bluetoothAdapter.bluetoothLeScanner.startScan(filters, settings, scanCallback) + scanner?.startScan(filters, settings, scanCallback) } /** @@ -89,7 +90,7 @@ internal class BLEDeviceScanner @Inject constructor( */ fun stopScanning() { if (_isScanning.getAndSet(false).not()) return - bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) + bluetoothAdapter.bluetoothLeScanner?.stopScan(scanCallback) } private fun emit(event: Event) { diff --git a/core/bluetooth/src/test/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScannerTest.kt b/core/bluetooth/src/test/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScannerTest.kt index e93765535..6f6fd8d36 100644 --- a/core/bluetooth/src/test/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScannerTest.kt +++ b/core/bluetooth/src/test/kotlin/edu/stanford/spezi/core/bluetooth/domain/BLEDeviceScannerTest.kt @@ -12,6 +12,7 @@ import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceType import edu.stanford.spezi.core.bluetooth.data.model.SupportedServices import edu.stanford.spezi.core.testing.SpeziTestScope import edu.stanford.spezi.core.testing.runTestUnconfined +import edu.stanford.spezi.core.testing.verifyNever import io.mockk.Called import io.mockk.Runs import io.mockk.every @@ -86,6 +87,19 @@ class BLEDeviceScannerTest { assertThat(bleDeviceScanner.isScanning).isTrue() } + @Test + fun `it should not start scanning if le scanner is not available`() { + // given + every { bluetoothAdapter.bluetoothLeScanner } returns null + + // when + bleDeviceScanner.startScanning() + + // then + assertThat(bleDeviceScanner.isScanning).isFalse() + verifyNever { bluetoothLeScanner.startScan(any()) } + } + @Test fun `it should start scanning only once if already scanning`() { // given @@ -129,6 +143,22 @@ class BLEDeviceScannerTest { assertThat(bleDeviceScanner.isScanning).isFalse() } + @Test + fun `it should safely stop scanning`() { + // given + val callback = getCallback() + val startedScanning = bleDeviceScanner.isScanning + every { bluetoothAdapter.bluetoothLeScanner } returns null + + // when + bleDeviceScanner.stopScanning() + + // then + assertThat(startedScanning).isTrue() + verifyNever { bluetoothLeScanner.stopScan(callback) } + assertThat(bleDeviceScanner.isScanning).isFalse() + } + @Test fun `it should handle onScanResult correctly`() = runTestUnconfined { // given diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4e2bc2933..f17a8ca11 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -9,11 +9,16 @@ default_platform(:android) platform :android do - desc "Runs all the tests" + desc "Runs all unit tests" lane :test do gradle(task: "test") end + desc "Runs all UI tests" + lane :connectedCheck do + gradle(task: "connectedCheck") + end + desc "Deploy a new version to the Google Play (Internal)" lane :internal do version_codes = google_play_track_version_codes( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 91cc5cb59..f3e9c2d12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,12 @@ [versions] accompanistPager = "0.35.1-alpha" activityCompose = "1.9.0" -agp = "8.4.1" -androidTools = "31.4.2" +agp = "8.5.0" +androidTools = "31.5.0" appcompat = "1.7.0" compileSdk = "34" -composeBom = "2024.05.00" -composeNavigation = "2.8.0-alpha08" +composeBom = "2024.06.00" +composeNavigation = "2.8.0-beta03" coreKtx = "1.13.1" coreKtxVersion = "1.5.0" coreTestingVersion = "2.2.0" @@ -21,7 +21,7 @@ firebaseAuthKtx = "23.0.0" firebaseFirestoreKtx = "25.0.0" firebaseFunctionsKtx = "21.0.0" firebaseStorageKtx = "21.0.0" -foundation = "1.6.7" +foundation = "1.6.8" googleGmsGoogleServices = "4.4.2" googleid = "1.1.0" hapiFhirVersion = "5.7.9" @@ -33,7 +33,7 @@ junitVersion = "1.1.5" kotlin = "2.0.0" kotlinxSerializationJson = "1.6.3" kspVersion = "2.0.0-1.0.21" -lifecycleKtx = "2.8.1" +lifecycleKtx = "2.8.2" lifecycleRuntimeKtx = "2.7.0" minSdk = "31" mockKVersion = "1.13.10" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e3c7912c3..3ab6b3c3d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Apr 18 21:37:46 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactScreenTest.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactScreenTest.kt index 398df1d7c..edb5b2b8c 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactScreenTest.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactScreenTest.kt @@ -4,7 +4,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Call import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Info -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import edu.stanford.spezi.modules.contact.model.ContactOption import edu.stanford.spezi.modules.contact.model.ContactOptionType @@ -20,7 +20,7 @@ class ContactScreenTest { private val mockContactRepository: ContactRepository = mockk() @get:Rule - val composeTestRule = createAndroidComposeRule() + val composeTestRule = createComposeRule() @Test fun contactView_displaysContactName() { diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/TestActivity.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/TestActivity.kt deleted file mode 100644 index 199503515..000000000 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/TestActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package edu.stanford.spezi.modules.contact - -import androidx.activity.ComponentActivity - -class TestActivity : ComponentActivity() diff --git a/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/file/EncryptedFileKeyValueStorageTest.kt b/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/file/EncryptedFileKeyValueStorageTest.kt index d81244bdd..606119c14 100644 --- a/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/file/EncryptedFileKeyValueStorageTest.kt +++ b/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/file/EncryptedFileKeyValueStorageTest.kt @@ -35,7 +35,7 @@ class EncryptedFileKeyValueStorageTest { fileStorage.saveFile(fileName, data) // Then - val readData = fileStorage.readFile(fileName) + val readData = fileStorage.readFile(fileName).getOrNull() assertThat(readData).isEqualTo(data) } @@ -45,7 +45,7 @@ class EncryptedFileKeyValueStorageTest { val fileName = "nonExistentFile" // When - val readData = fileStorage.readFile(fileName) + val readData = fileStorage.readFile(fileName).getOrNull() // Then assertThat(readData).isNull() @@ -62,7 +62,7 @@ class EncryptedFileKeyValueStorageTest { fileStorage.saveFile(fileName, newData) // Then - val readData = fileStorage.readFile(fileName) + val readData = fileStorage.readFile(fileName).getOrNull() assertThat(readData).isEqualTo(newData) } @@ -76,7 +76,7 @@ class EncryptedFileKeyValueStorageTest { fileStorage.deleteFile(fileName) // Then - val readData = fileStorage.readFile(fileName) + val readData = fileStorage.readFile(fileName).getOrNull() assertThat(readData).isNull() } } diff --git a/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/EncryptedSharedPreferencesKeyValueStorageTest.kt b/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/EncryptedSharedPreferencesKeyValueStorageTest.kt index 12617ea17..09e61919f 100644 --- a/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/EncryptedSharedPreferencesKeyValueStorageTest.kt +++ b/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/EncryptedSharedPreferencesKeyValueStorageTest.kt @@ -5,7 +5,6 @@ import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.core.testing.runTestUnconfined import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import org.junit.Test class EncryptedSharedPreferencesKeyValueStorageTest { @@ -163,7 +162,7 @@ class EncryptedSharedPreferencesKeyValueStorageTest { storage.saveData(key, expectedValue) // When - val actualValue = runBlocking { storage.readData(key).first() } + val actualValue = storage.readData(key).first() // Then assertThat(actualValue).isEqualTo(expectedValue) diff --git a/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/LocalKeyValueStorageTest.kt b/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/LocalKeyValueStorageTest.kt index d8588451d..2a38c1672 100644 --- a/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/LocalKeyValueStorageTest.kt +++ b/modules/storage/src/androidTest/kotlin/edu/stanford/spezi/modules/storage/key/LocalKeyValueStorageTest.kt @@ -5,7 +5,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import edu.stanford.spezi.core.testing.runTestUnconfined -import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith @@ -24,7 +23,7 @@ class LocalKeyValueStorageTest { localStorage.saveData(key, data) // Then - val readData = runBlocking { localStorage.readDataBlocking(key) } + val readData = localStorage.readDataBlocking(key) assertThat(readData).isEqualTo(data) }