diff --git a/.circleci/config.yml b/.circleci/config.yml index 5aca1c2e..b84cfb92 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - codecov: codecov/codecov@3.2.4 + codecov: codecov/codecov@5.4.3 ruby: circleci/ruby@2.0.0 commands: @@ -9,7 +9,11 @@ commands: - restore_cache: key: v1-gradle-wrapper-{{ arch }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} - restore_cache: - key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle" }} + key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle.kts" }}-{{ checksum "settings.gradle.kts" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }}-{{ checksum "gradle/libs.versions.toml" }} + restore_bundler_cache: + steps: + - restore_cache: + key: v1-bundler-cache-{{ checksum "Gemfile.lock" }} save_gradle_cache: steps: @@ -20,7 +24,13 @@ commands: - save_cache: paths: - ~/.gradle/caches - key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle" }} + key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle.kts" }}-{{ checksum "settings.gradle.kts" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }}-{{ checksum "gradle/libs.versions.toml" }} + save_bundler_cache: + steps: + - save_cache: + paths: + - vendor/bundle + key: v1-bundler-cache-{{ checksum "Gemfile.lock" }} executors: android-machine: @@ -37,18 +47,21 @@ jobs: steps: - checkout - restore_gradle_cache + - restore_bundler_cache - ruby/install-deps: with-cache: true - run: name: Fastlane - run all tests with coverage report command: | bundle exec fastlane testDev + bundle exec fastlane lint - save_gradle_cache + - save_bundler_cache - run: name: Analyze on SonarCloud command: ./gradlew lintDebug sonar - codecov/upload: - file: app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml + files: app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml - store_test_results: path: app/build/test-results/testDebugUnitTest - store_artifacts: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5384f529 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e03a03bd..a9218a9b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -7,7 +7,7 @@ jobs: unit-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.1' @@ -21,22 +21,22 @@ jobs: uses: actions/cache@v3 with: path: ~/.gradle/wrapper - key: ${{ runner.OS }}-gradle-wrapper-cache-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} + key: ${{ runner.OS }}-gradle-wrapper-cache-${{ hashFiles('build.gradle.kts', 'settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml') }} restore-keys: | ${{ runner.OS }}-gradle-wrapper-cache- - name: Cache Gradle caches 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', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} restore-keys: | ${{ runner.OS }}-gradle-caches-cache- - name: generate ksProp file run: ./gradlew generateKsPropFile - name: generate google-services.json file - run: ./gradlew generateGoogleServicesJson env: GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: ./gradlew generateGoogleServicesJson - name: setup fastlane run: bundle install - name: run unit tests @@ -50,12 +50,26 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: set up JDK 17 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' + - name: Cache Gradle wrapper + uses: actions/cache@v3 + with: + path: ~/.gradle/wrapper + key: ${{ runner.OS }}-gradle-wrapper-cache-${{ hashFiles('build.gradle.kts', 'settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml') }} + restore-keys: | + ${{ runner.OS }}-gradle-wrapper-cache- + - name: Cache Gradle caches + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle.kts', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} + restore-keys: | + ${{ runner.OS }}-gradle-caches-cache- - name: generate ksProp file run: ./gradlew generateKsPropFile - name: generate google-services.json file @@ -74,12 +88,26 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' + - name: Cache Gradle wrapper + uses: actions/cache@v3 + with: + path: ~/.gradle/wrapper + key: ${{ runner.OS }}-gradle-wrapper-cache-${{ hashFiles('build.gradle.kts', 'settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml') }} + restore-keys: | + ${{ runner.OS }}-gradle-wrapper-cache- + - name: Cache Gradle caches + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.OS }}-gradle-caches-cache-${{ hashFiles('build.gradle.kts', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} + restore-keys: | + ${{ runner.OS }}-gradle-caches-cache- - name: generate ksProp file run: ./gradlew generateKsPropFile - name: generate google-services.json file @@ -91,8 +119,38 @@ 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 + 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 + - name: Upload UI test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: ui-test-results + path: app/build/reports/androidTests/connected/ + + notify-slack: + needs: unit-test + runs-on: ubuntu-latest + if: always() # Runs regardless of success or failure of unit-test + steps: + - name: Send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_GITHUB_BUILD_INFO }} + run: | + STATUS="${{ needs.unit-test.result }}" + curl -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"Unit tests completed: $STATUS\"}" \ + $SLACK_WEBHOOK_URL diff --git a/Gemfile b/Gemfile index 2ccf2ecb..68230327 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -gem "fastlane" +gem "fastlane", ">= 2.220.0" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index d5b4e4ee..4ec9b032 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,26 +9,26 @@ GEM public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.2) - aws-partitions (1.1095.0) - aws-sdk-core (3.222.3) + aws-eventstream (1.4.0) + aws-partitions (1.1125.0) + aws-sdk-core (3.226.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.99.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.106.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.184.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.192.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) + base64 (0.3.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -57,10 +57,10 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) + faraday-multipart (1.1.1) multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) @@ -70,14 +70,14 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.218.0) + fastlane (2.228.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -86,9 +86,11 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -97,10 +99,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -108,53 +110,50 @@ GEM tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.9.1) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-firebase_app_distribution (0.10.1) google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.78.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-core (0.17.0) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) - googleauth (~> 1.9) - httpclient (>= 2.8.3, < 3.a) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) mini_mime (~> 1.0) - mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) + rexml google-apis-firebaseappdistribution_v1 (0.3.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-firebaseappdistribution_v1alpha (0.2.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-iamcredentials_v1 (0.23.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.16.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.50.0) - google-apis-core (>= 0.15.0, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.3.0) - base64 (~> 0.2) - faraday (>= 1.0, < 3.a) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.5.0) - google-cloud-storage (1.56.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-core (~> 0.13) - google-apis-iamcredentials_v1 (~> 0.18) - google-apis-storage_v1 (>= 0.42) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) - googleauth (~> 1.9) + googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - google-logging-utils (0.2.0) - googleauth (1.14.0) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.2) - google-logging-utils (~> 0.1) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -165,8 +164,8 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.11.3) - jwt (2.10.1) + json (2.12.2) + jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) @@ -175,23 +174,23 @@ GEM multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) - naturally (2.2.1) + naturally (2.3.0) nkf (0.2.0) optparse (0.6.0) os (1.1.4) plist (3.7.2) public_suffix (6.0.2) - rake (13.2.1) + rake (13.3.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) rexml (3.4.1) - rouge (2.0.7) + rouge (3.28.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) - security (0.1.3) + security (0.1.5) signet (0.20.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -200,6 +199,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -218,8 +218,8 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.4.0) rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) + xcpretty (0.4.1) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) @@ -230,7 +230,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - fastlane + fastlane (>= 2.220.0) fastlane-plugin-firebase_app_distribution BUNDLED WITH diff --git a/README.md b/README.md index 01708181..b5f32636 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ More info on official Sonar docs: [SonarScanner for Gradle](https://docs.sonarcl ## Knows Issues +- Jacoco coverage report is incorrect - Please feel free inform me about new issues diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ebd0cf9..306f01ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,8 +36,8 @@ android { applicationId = "org.kabiri.android.usbterminal" minSdk = 24 targetSdk = 35 - versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 14 - versionName = "0.9.84${System.getenv("CIRCLE_BUILD_NUM") ?: ""}" + versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 15 + versionName = "0.9.85${System.getenv("CIRCLE_BUILD_NUM") ?: ""}" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -62,7 +62,8 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } named("debug") { - isTestCoverageEnabled = true + enableUnitTestCoverage = true + enableAndroidTestCoverage = true } } @@ -70,8 +71,9 @@ android { animationsDisabled = true + @Suppress("UnstableApiUsage") managedDevices { - devices { + allDevices { maybeCreate("pixel2api30").apply { device = "Pixel 2" apiLevel = 30 @@ -93,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")) } @@ -109,16 +110,24 @@ tasks.register("jacocoTestReport") { csv.required.set(false) } - val fileFilter = listOf("**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", "**/*Test*.*", "android/**/*.*") - val debugTree = fileTree("${layout.buildDirectory}/tmp/kotlin-classes/debug") { exclude(fileFilter) } - val mainSrc = "${layout.projectDirectory}/src/main/kotlin" - - sourceDirectories.from(files(setOf(mainSrc))) - classDirectories.from(files(setOf(debugTree))) - executionData.from(fileTree(layout.buildDirectory) { include(setOf( - "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", - "outputs/managed_device_code_coverage/pixel2api30/coverage.ec" - ))}) + val fileFilter = listOf( + "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", + "**/*Test*.*", "android/**/*.*", + "**/Dagger*.*", "**/*_Hilt*.*", "**/*Hilt*.*", + ) + val javaDebugTree = fileTree(layout.buildDirectory.dir("intermediates/javac/debug/classes")) { exclude(fileFilter) } + val kotlinDebugTree = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) { exclude(fileFilter) } + val mainJavaSrc = layout.projectDirectory.dir("src/main/java") + val mainKotlinSrc = layout.projectDirectory.dir("src/main/kotlin") + sourceDirectories.from(files(mainJavaSrc, mainKotlinSrc)) + classDirectories.from(files(javaDebugTree, kotlinDebugTree)) + executionData.from(fileTree(layout.buildDirectory) { + include( + "outputs/unit_test_code_coverage/**/*.exec", + "outputs/managed_device_code_coverage/**/*.ec", + "outputs/managed_device_code_coverage/**/*.exec" + ) + }) } sonarqube { @@ -206,82 +215,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") -} \ No newline at end of file + 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..bda96883 --- /dev/null +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt @@ -0,0 +1,118 @@ +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.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +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, + onVisible: () -> Unit, + onOverflow: () -> Unit + ) { + try { + // Try to find the menu item first + onView(withId(menuItemId)).check(matches(isDisplayed())) + onVisible() + } catch (_: NoMatchingViewException) { + // If not found then open the overflow menu + openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) + onOverflow() + } + } + + @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 checkActionMenuItemSettingsIsDisplayed() = ensureMenuIsAccessible( + menuItemId = R.id.actionSettings, + onVisible = { + + // assert + onView(withId(R.id.actionSettings)).check(matches(isDisplayed())) + }, + onOverflow = { + + // assert + onView(withText(R.string.title_settings)).check(matches(isDisplayed())) + } + ) + + @Test + fun checkActionMenuItemConnectIsDisplayed() = ensureMenuIsAccessible( + menuItemId = R.id.actionSettings, + onVisible = { + + // assert + onView(withId(R.id.actionConnect)).check(matches(isDisplayed())) + }, + onOverflow = { + + // assert + onView(withText(R.string.title_connect)).check(matches(isDisplayed())) + } + ) + + @Test + fun checkActionMenuItemDisconnectIsDisplayed() = ensureMenuIsAccessible( + menuItemId = R.id.actionSettings, + onVisible = { + + // assert + onView(withId(R.id.actionDisconnect)).check(matches(isDisplayed())) + }, + onOverflow = { + + // assert + onView(withText(R.string.title_disconnect)).check(matches(isDisplayed())) + } + ) + + @Test + fun clickingSettingsOpensSettingsBottomSheet() { + // arrange + ensureMenuIsAccessible( + menuItemId = R.id.actionSettings, + onVisible = { + + // act + onView(withId(R.id.actionSettings)).perform(click()) + + // assert + onView(withId(R.id.composeViewSettingContent)).check(matches(isDisplayed())) + }, + onOverflow = { + // act + onView(withText(R.string.title_settings)).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 5a9c56a9..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) -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/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt index de19d3ae..9057fbe1 100644 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/data/repository/UserSettingRepositoryAndroidTest.kt @@ -20,7 +20,7 @@ import org.kabiri.android.usbterminal.model.UserSettingPreferences private const val TEST_DATA_STORE_NAME = "test_data_store" @RunWith(AndroidJUnit4::class) -class UserSettingRepositoryAndroidTest { +internal class UserSettingRepositoryAndroidTest { private val testCoroutineDispatcher: TestDispatcher = StandardTestDispatcher() private val testCoroutineScope = TestScope(testCoroutineDispatcher + Job()) diff --git a/app/src/main/res/menu/activity_main_menu.xml b/app/src/main/res/menu/activity_main_menu.xml index 1f0561ef..f0821973 100644 --- a/app/src/main/res/menu/activity_main_menu.xml +++ b/app/src/main/res/menu/activity_main_menu.xml @@ -1,7 +1,7 @@ - - - + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 30e0c129..00000000 --- a/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -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" - } -} - -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" -} - -task clean(type: Delete) { - delete rootProject.buildDir -} - -sonarqube { - properties { - property "sonar.organization", "superus8r" - property "sonar.projectKey", "superus8r_arduino-usb-terminal" - property "sonar.projectName", "arduino-usb-terminal" - property "sonar.sourceEncoding", "UTF-8" - property "sonar.host.url", "https://sonarcloud.io" - - // sonar requires relative path for sources and binaries - property "sonar.sources", "/app/src/main/java" - property "sonar.binaries", "/app/build/tmp/kotlin-classes/debug" - // sonar requires absolute path for lint and jacoco reports! - property "sonar.androidLint.reportPaths", "$rootDir/app/build/reports/lint-results-debug.xml" - property "sonar.coverage.jacoco.xmlReportPaths", "$rootDir/app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml" - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..30d6adf8 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,32 @@ +buildscript { + dependencies { + classpath(libs.hilt.android.gradle.plugin) + classpath(libs.google.services) + classpath(libs.firebase.crashlytics.gradle) + } +} + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.sonarqube) +} + +tasks.register("clean") { + delete(layout.buildDirectory) +} + +sonarqube { + properties { + property("sonar.organization", "superus8r") + property("sonar.projectKey", "superus8r_arduino-usb-terminal") + property("sonar.projectName", "arduino-usb-terminal") + property("sonar.sourceEncoding", "UTF-8") + property("sonar.host.url", "https://sonarcloud.io") + + property("sonar.binaries", project(":app").layout.buildDirectory.dir("tmp/kotlin-classes/debug").get().asFile.absolutePath) + 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/fastlane/Fastfile b/fastlane/Fastfile index 620c3601..8cf3ac62 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -33,6 +33,11 @@ platform :android do ]) end + desc "Runs lint" + lane :lint do + gradle(task: "lintDebug") + end + desc "Deploy to Firebase AppTester Dev channel" lane :distDev do gradle(tasks: [ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..61b952fa --- /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.1" +coreKtx = "1.16.0" +constraintlayout = "2.2.1" +lifecycleRuntimeKtx = "2.9.1" +lifecycleViewmodelKtx = "2.9.1" +lifecycleExtensions = "2.2.0" +activityCompose = "1.10.1" +datastorePreferences = "1.1.7" + +# Compose +composeBom = "2025.06.01" + +# Google/Material +material = "1.12.0" + +# Firebase +firebaseBom = "33.16.0" + +# Dagger/Hilt +hilt = "2.56.2" +hiltAndroidTesting = "2.56.2" + +# Coroutines +coroutines = "1.10.2" + +# 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 = "3.0" +testRunner = "1.6.2" +testRules = "1.6.1" +truth = "1.4.4" +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.3" } +firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.4" } + +[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")