diff --git a/.circleci/config.yml b/.circleci/config.yml index 74156eea..d921cc1a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ commands: - restore_cache: key: v1-gradle-wrapper-{{ arch }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} - restore_cache: - key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle" }}-{{ checksum "settings.gradle" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }} + key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle.kts" }}-{{ checksum "settings.gradle.kts" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }} restore_bundler_cache: steps: - restore_cache: @@ -24,7 +24,7 @@ commands: - save_cache: paths: - ~/.gradle/caches - key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle" }}-{{ checksum "settings.gradle" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }} + key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle.kts" }}-{{ checksum "settings.gradle.kts" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }} save_bundler_cache: steps: - save_cache: diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 26d5d569..d0ca2b18 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -28,7 +28,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.gradle/caches - key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle') }} + key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle.kts') }} restore-keys: | ${{ runner.OS }}-gradle-caches-cache- - name: generate ksProp file @@ -91,11 +91,21 @@ jobs: 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: run tests + - name: run tests with screen record uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 - script: ./gradlew connectedCheck + script: | + adb shell screenrecord /sdcard/ui-test.mp4 & + SCREENRECORD_PID=$! + ./gradlew connectedCheck || true + kill $SCREENRECORD_PID || true + adb pull /sdcard/ui-test.mp4 ./ui-test.mp4 || true + - name: Upload UI test video + uses: actions/upload-artifact@v4 + with: + name: ui-test-video + path: ./ui-test.mp4 notify-slack: needs: unit-test diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 629b4f76..5efd8972 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,7 +62,8 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } named("debug") { - isTestCoverageEnabled = true + enableUnitTestCoverage = true + enableAndroidTestCoverage = true } } @@ -94,8 +95,7 @@ android { } jacoco { - val jacoco_version: String by project - toolVersion = jacoco_version + toolVersion = libs.versions.jacoco.get() reportsDirectory.set(layout.buildDirectory.dir("mergedReportDir")) } @@ -216,82 +216,70 @@ fun loadKeyStore(name: String): Properties? { } } -val firebase_bom_version: String by project -val hilt_version: String by project -val coroutines_version: String by project -val material_version: String by project -val mockk_version: String by project dependencies { - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.core:core-ktx:1.16.0") - implementation("androidx.constraintlayout:constraintlayout:2.2.1") + // AndroidX + implementation(libs.appcompat) + implementation(libs.core.ktx) + implementation(libs.constraintlayout) // Firebase - implementation(platform("com.google.firebase:firebase-bom:$firebase_bom_version")) - implementation("com.google.firebase:firebase-analytics-ktx") - implementation("com.google.firebase:firebase-crashlytics-ktx") + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics.ktx) + implementation(libs.firebase.crashlytics.ktx) // Dependency Injection - implementation("com.google.dagger:hilt-android:$hilt_version") - kapt("com.google.dagger:hilt-compiler:$hilt_version") + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) // Coroutines - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") - implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.extensions) + implementation(libs.coroutines.core) + implementation(libs.coroutines.android) // Compose Bom - val composeBom = platform("androidx.compose:compose-bom:2023.06.01") + val composeBom = platform(libs.compose.bom) implementation(composeBom) - androidTestImplementation(composeBom) - implementation("androidx.compose.foundation:foundation") - implementation("androidx.compose.material3:material3") + implementation(libs.compose.foundation) + implementation(libs.compose.material3) // Compose - Android Studio Preview support - implementation("androidx.compose.ui:ui-tooling-preview") - debugImplementation("androidx.compose.ui:ui-tooling") - implementation("androidx.activity:activity-compose:1.10.1") + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) + implementation(libs.activity.compose) // Other UI Libraries - implementation("com.google.android.material:material:$material_version") - - // data - implementation("androidx.datastore:datastore-preferences:1.1.4") - - // unit test libs - testImplementation("junit:junit:4.13.2") - - // instrumented test libs - androidTestImplementation("androidx.test:core:1.6.1") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + implementation(libs.material) - // Hamcrest for view matching - androidTestImplementation("org.hamcrest:hamcrest-library:2.2") - androidTestImplementation("androidx.test:runner:1.6.2") - androidTestImplementation("androidx.test:rules:1.6.1") + // Data + implementation(libs.datastore.preferences) - // coroutine testing - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") + // Unit Test Libraries + testImplementation(libs.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.truth) + testImplementation(libs.mockk.android) + testImplementation(libs.mockk.agent) - // google truth for assertions - testImplementation("com.google.truth:truth:1.1.3") - androidTestImplementation("androidx.test.ext:truth:1.6.0") - - // mockk - testImplementation("io.mockk:mockk-android:$mockk_version") - testImplementation("io.mockk:mockk-agent:$mockk_version") - androidTestImplementation("io.mockk:mockk-android:$mockk_version") - androidTestImplementation("io.mockk:mockk-agent:$mockk_version") - - // hilt testing - https://developer.android.com/training/dependency-injection/hilt-testing - androidTestImplementation("com.google.dagger:hilt-android-testing:$hilt_version") - kaptAndroidTest("com.google.dagger:hilt-android-compiler:$hilt_version") + // Instrumented Test Libraries + androidTestImplementation(composeBom) + androidTestImplementation(libs.coroutines.test) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.ext.junit.ktx) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.hamcrest) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.truth) + androidTestImplementation(libs.mockk.android) + androidTestImplementation(libs.mockk.agent) + + // Hilt Testing + androidTestImplementation(libs.hilt.android.testing) + kaptAndroidTest(libs.hilt.android.compiler) // Android Serial Controller - implementation("com.github.superus8r:UsbSerial:6.1.1") + implementation(libs.usb.serial) } diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt new file mode 100644 index 00000000..9ef1a98c --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt @@ -0,0 +1,74 @@ +package org.kabiri.android.usbterminal + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +internal class MainActivityAndroidTest { + + @get:Rule + var rule = activityScenarioRule() + + private fun ensureMenuIsAccessible(menuItemId: Int) { + try { + // Try to find the menu item first + onView(withId(menuItemId)).check(matches(isDisplayed())) + } catch (_: NoMatchingViewException) { + // If not found then open the overflow menu + openActionBarOverflowOrOptionsMenu( + InstrumentationRegistry.getInstrumentation().targetContext + ) + } + } + + @Test + fun checkUiViewsAreDisplayed() { + // arrange + // act + // assert + onView(withId(R.id.tvOutput)).check(matches(isDisplayed())) + onView(withId(R.id.btEnter)).check(matches(isDisplayed())) + onView(withId(R.id.etInput)).check(matches(isDisplayed())) + } + + @Test + fun checkActionMenuItemsAreDisplayed() { + // arrange + // act + // Ensure action items are accessible, either in the toolbar or via overflow + ensureMenuIsAccessible(R.id.actionSettings) + ensureMenuIsAccessible(R.id.actionConnect) + ensureMenuIsAccessible(R.id.actionDisconnect) + + // assert + // Check menu items are displayed + onView(withId(R.id.actionSettings)).check(matches(isDisplayed())) + onView(withId(R.id.actionConnect)).check(matches(isDisplayed())) + onView(withId(R.id.actionDisconnect)).check(matches(isDisplayed())) + } + + @Test + fun clickingSettingsOpensSettingsBottomSheet() { + // arrange + // Ensure the action item is accisble, either in the toolbar or via overflow + ensureMenuIsAccessible(R.id.actionSettings) + + // act + onView(withId(R.id.actionSettings)).perform(click()) + + // assert + onView(withId(R.id.composeViewSettingContent)).check(matches(isDisplayed())) + } +} diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt deleted file mode 100644 index 811120b0..00000000 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.kabiri.android.usbterminal - -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.rules.activityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - - -@RunWith(AndroidJUnit4::class) -internal class MainActivityTest { - - @get:Rule - var rule = activityScenarioRule() - - @Test - fun checkUiViewsAreDisplayed() { - onView(withId(R.id.tvOutput)).check(matches(isDisplayed())) - onView(withId(R.id.btEnter)).check(matches(isDisplayed())) - onView(withId(R.id.etInput)).check(matches(isDisplayed())) - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle.kts similarity index 50% rename from build.gradle rename to build.gradle.kts index a3b0a9ce..30d6adf8 100644 --- a/build.gradle +++ b/build.gradle.kts @@ -1,29 +1,20 @@ buildscript { - ext { - coroutines_version = "1.6.4" - firebase_bom_version = "32.8.0" - hilt_version = "2.56.2" - jacoco_version = "0.8.8" - kotlin_version = "2.1.20" - material_version = "1.12.0" - mockk_version = "1.14.2" - } dependencies { - classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath "com.google.gms:google-services:4.4.1" - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9" + classpath(libs.hilt.android.gradle.plugin) + classpath(libs.google.services) + classpath(libs.firebase.crashlytics.gradle) } } plugins { - id("com.android.application") version '8.10.0' apply false - id("org.jetbrains.kotlin.android") version "2.1.20" apply false - id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" - id("org.sonarqube") version "3.5.0.2730" + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.sonarqube) } -task clean(type: Delete) { - delete layout.buildDirectory +tasks.register("clean") { + delete(layout.buildDirectory) } sonarqube { @@ -35,7 +26,6 @@ sonarqube { property("sonar.host.url", "https://sonarcloud.io") property("sonar.binaries", project(":app").layout.buildDirectory.dir("tmp/kotlin-classes/debug").get().asFile.absolutePath) - // sonar requires absolute path for lint and jacoco reports! property("sonar.androidLint.reportPaths", project(":app").layout.buildDirectory.dir("reports/lint-results-debug.xml").get().asFile.absolutePath) property("sonar.coverage.jacoco.xmlReportPaths", project(":app").layout.buildDirectory.dir("mergedReportDir/jacocoTestReport/jacocoTestReport.xml").get().asFile.absolutePath) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..9825e2ba --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,116 @@ +[versions] +# Build tools & plugins +gradle = "8.10.1" +kotlin = "2.1.20" +jacoco = "0.8.8" + +# AndroidX +appcompat = "1.7.0" +coreKtx = "1.16.0" +constraintlayout = "2.2.1" +lifecycleRuntimeKtx = "2.8.7" +lifecycleViewmodelKtx = "2.8.7" +lifecycleExtensions = "2.2.0" +activityCompose = "1.10.1" +datastorePreferences = "1.1.4" + +# Compose +composeBom = "2023.06.01" + +# Google/Material +material = "1.12.0" + +# Firebase +firebaseBom = "32.8.0" + +# Dagger/Hilt +hilt = "2.56.2" +hiltAndroidTesting = "2.56.2" + +# Coroutines +coroutines = "1.7.3" + +# USB Serial +usbSerial = "6.1.1" + +# Testing +junit = "4.13.2" +mockk = "1.14.2" +testCore = "1.6.1" +testExtJunit = "1.2.1" +testExtJunitKtx = "1.2.1" +espressoCore = "3.6.1" +hamcrest = "2.2" +testRunner = "1.6.2" +testRules = "1.6.1" +truth = "1.1.3" +androidxTruth = "1.6.0" + +[libraries] + +# --- AndroidX --- +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } +lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } +lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleExtensions" } +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } + +# --- Compose --- +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +compose-foundation = { module = "androidx.compose.foundation:foundation" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } + +# --- Google/Material --- +material = { module = "com.google.android.material:material", version.ref = "material" } + +# --- Firebase --- +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" } +firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" } + +# --- Dagger/Hilt --- +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hiltAndroidTesting" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidTesting" } + +# --- Coroutines --- +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } + +# --- USB Serial --- +usb-serial = { module = "com.github.superus8r:UsbSerial", version.ref = "usbSerial" } + +# --- Unit Test --- +junit = { module = "junit:junit", version.ref = "junit" } +mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockk" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } + +# --- Instrumented Test --- +androidx-test-core = { module = "androidx.test:core", version.ref = "testCore" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "testExtJunit" } +androidx-test-ext-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "testExtJunitKtx" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +hamcrest = { module = "org.hamcrest:hamcrest-library", version.ref = "hamcrest" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "testRunner" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "testRules" } +androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "androidxTruth" } + +# --- Plugins (classpath dependencies) --- +hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } +google-services = { module = "com.google.gms:google-services", version = "4.4.1" } +firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.9" } + +[plugins] +# --- Gradle Plugins --- +android-application = { id = "com.android.application", version.ref = "gradle" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +sonarqube = { id = "org.sonarqube", version = "3.5.0.2730" } diff --git a/settings.gradle b/settings.gradle.kts similarity index 73% rename from settings.gradle rename to settings.gradle.kts index c2c1085e..34fde504 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -10,8 +10,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url = uri("https://www.jitpack.io" ) } + maven("https://www.jitpack.io") } } -rootProject.name='USBTerminal' -include ':app' \ No newline at end of file +rootProject.name = "USBTerminal" +include(":app")