From 1222245b8d808ea57cc87fa72d7cf41969d8ac92 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Mon, 26 May 2025 20:02:12 +0200 Subject: [PATCH 01/43] docs: update known issues in readme --- README.md | 1 + 1 file changed, 1 insertion(+) 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 From fa98ce08cadb1391cfb699b4c20d218faba8cf30 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 18:36:16 +0200 Subject: [PATCH 02/43] nitpick: use kotlin style instead of groovy for sonarqube properties --- build.gradle | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 30e0c129..5eaf6f19 100644 --- a/build.gradle +++ b/build.gradle @@ -28,17 +28,17 @@ task clean(type: Delete) { 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.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" + 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" + property("sonar.androidLint.reportPaths", "${layout.buildDirectory}/reports/lint-results-debug.xml") + property("sonar.coverage.jacoco.xmlReportPaths", "${layout.buildDirectory}/app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml") } } \ No newline at end of file From bb61b2e1481f1fa33ab49f23f960e20e986d1a52 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 18:58:13 +0200 Subject: [PATCH 03/43] bugfix: use the layout.projectDir --- app/build.gradle.kts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ebd0cf9..245f460b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,8 +70,9 @@ android { animationsDisabled = true + @Suppress("UnstableApiUsage") managedDevices { - devices { + allDevices { maybeCreate("pixel2api30").apply { device = "Pixel 2" apiLevel = 30 @@ -110,14 +111,13 @@ tasks.register("jacocoTestReport") { } 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" - + val debugTree = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) { exclude(fileFilter) } + val mainSrc = layout.projectDirectory.dir("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" + "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", + "outputs/managed_device_code_coverage/debug/pixel2api30/coverage.ec", ))}) } @@ -284,4 +284,4 @@ dependencies { // Android Serial Controller implementation("com.github.superus8r:UsbSerial:6.1.1") -} \ No newline at end of file +} From 65ebd0ca48b25e344a887ab3e07a510bd13d6a1a Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:04:30 +0200 Subject: [PATCH 04/43] bugfix: hopefully fixed the jacoco report and its file filter --- app/build.gradle.kts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 245f460b..bd35ee0a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,15 +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.dir("tmp/kotlin-classes/debug")) { exclude(fileFilter) } - val mainSrc = layout.projectDirectory.dir("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/debug/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 { From 38854c9e7252b6799a9556c6b8bb8629e06d5a70 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:06:46 +0200 Subject: [PATCH 05/43] bugfix: hopefully fix the sonar paths --- build.gradle | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 5eaf6f19..e0305299 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { } task clean(type: Delete) { - delete rootProject.buildDir + delete layout.buildDirectory } sonarqube { @@ -35,10 +35,11 @@ sonarqube { 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") + 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", "${layout.buildDirectory}/reports/lint-results-debug.xml") - property("sonar.coverage.jacoco.xmlReportPaths", "${layout.buildDirectory}/app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml") + property("sonar.androidLint.reportPaths", "app/build/reports/lint-results-debug.xml") + property("sonar.coverage.jacoco.xmlReportPaths", "app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml") } -} \ No newline at end of file +} + From 1373111b17a6b078f018a30cc394fd77e348afd8 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:24:45 +0200 Subject: [PATCH 06/43] bugfix: explicitly define sonar tests path --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e0305299..9d3b40d3 100644 --- a/build.gradle +++ b/build.gradle @@ -36,10 +36,10 @@ sonarqube { // sonar requires relative path for sources and binaries property("sonar.sources", "app/src/main/java") + property("sonar.tests", "app/src/test/java,app/src/test/kotlin,app/src/androidTest/java") property("sonar.binaries", "app/build/tmp/kotlin-classes/debug") // sonar requires absolute path for lint and jacoco reports! property("sonar.androidLint.reportPaths", "app/build/reports/lint-results-debug.xml") property("sonar.coverage.jacoco.xmlReportPaths", "app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml") } } - From 533acc41bf6a6290806f6f3962cd5827943abb72 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:40:30 +0200 Subject: [PATCH 07/43] bugfix: hopefully fix sonar report --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9d3b40d3..83aee3c1 100644 --- a/build.gradle +++ b/build.gradle @@ -35,8 +35,8 @@ sonarqube { property("sonar.host.url", "https://sonarcloud.io") // sonar requires relative path for sources and binaries - property("sonar.sources", "app/src/main/java") - property("sonar.tests", "app/src/test/java,app/src/test/kotlin,app/src/androidTest/java") + property("sonar.sources", "app/src/main/java,app/src/main/kotlin") + property("sonar.tests", "app/src/test/java,app/src/test/kotlin,app/src/androidTest/java,app/src/androidTest/kotlin") property("sonar.binaries", "app/build/tmp/kotlin-classes/debug") // sonar requires absolute path for lint and jacoco reports! property("sonar.androidLint.reportPaths", "app/build/reports/lint-results-debug.xml") From 2e0f71ae5afccdb8b9549fda05eab53aa4f924ff Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:42:48 +0200 Subject: [PATCH 08/43] ci: update codecov version for circleci config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5aca1c2e..314648e9 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: From d0dd6737bb1c441b63a293bf5939f183179d4d8a Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:49:20 +0200 Subject: [PATCH 09/43] ci: downgrade codecov orb because the new one doesn't support the file parameter (only environment variables) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 314648e9..5aca1c2e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - codecov: codecov/codecov@5.4.3 + codecov: codecov/codecov@3.2.4 ruby: circleci/ruby@2.0.0 commands: From 733436b9829ad9351d396a63adaad09fe1d0dcbc Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 27 May 2025 19:50:44 +0200 Subject: [PATCH 10/43] ci: hopefully update codecov orb --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5aca1c2e..550944d5 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: @@ -48,7 +48,7 @@ jobs: 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: From 871a1e73f5a989901ee9b73abd68855268ae0069 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 08:28:43 +0200 Subject: [PATCH 11/43] ci: hopefully fix sonar job on circleci by excluding the test directories --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 83aee3c1..2fc23681 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ sonarqube { // sonar requires relative path for sources and binaries property("sonar.sources", "app/src/main/java,app/src/main/kotlin") + property("sonar.exclusions", "app/src/test/**,app/src/androidTest/**") property("sonar.tests", "app/src/test/java,app/src/test/kotlin,app/src/androidTest/java,app/src/androidTest/kotlin") property("sonar.binaries", "app/build/tmp/kotlin-classes/debug") // sonar requires absolute path for lint and jacoco reports! From 305132421ab20a18bb645bb0d6dc970ab528f1a8 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 08:32:25 +0200 Subject: [PATCH 12/43] chore: update fastlane and its plugins --- Gemfile.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d5b4e4ee..4b4d9172 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,19 +10,19 @@ GEM 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-partitions (1.1108.0) + aws-sdk-core (3.224.1) 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-kms (1.101.0) aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.184.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.187.0) + aws-sdk-core (~> 3, >= 3.224.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.11.0) @@ -110,13 +110,13 @@ GEM 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) + fastlane-plugin-firebase_app_distribution (0.10.1) google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.78.0) + google-apis-androidpublisher_v3 (0.80.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-core (0.17.0) + google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 1.9) httpclient (>= 2.8.3, < 3.a) @@ -128,16 +128,16 @@ GEM 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-iamcredentials_v1 (0.24.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.16.0) + google-apis-playcustomapp_v1 (0.17.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.50.0) + google-apis-storage_v1 (0.51.0) google-apis-core (>= 0.15.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) + google-cloud-env (2.3.1) base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.5.0) @@ -165,7 +165,7 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.11.3) + json (2.12.2) jwt (2.10.1) base64 logger (1.7.0) From dd86951460e14e7b085107540c425707bbe8fda3 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 08:53:46 +0200 Subject: [PATCH 13/43] ci: send a slack notification once unit-test action on github runs successfully --- .github/workflows/android.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e03a03bd..26d5d569 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -96,3 +96,17 @@ jobs: with: api-level: 29 script: ./gradlew connectedCheck + + 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 From 3e7ad750cc840a7c6429111650ff7d04d3041f03 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 09:41:47 +0200 Subject: [PATCH 14/43] ci: hopefully fixed sonar issue by using default source and binary paths --- build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.gradle b/build.gradle index 2fc23681..bd430d57 100644 --- a/build.gradle +++ b/build.gradle @@ -34,10 +34,6 @@ sonarqube { 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,app/src/main/kotlin") - property("sonar.exclusions", "app/src/test/**,app/src/androidTest/**") - property("sonar.tests", "app/src/test/java,app/src/test/kotlin,app/src/androidTest/java,app/src/androidTest/kotlin") property("sonar.binaries", "app/build/tmp/kotlin-classes/debug") // sonar requires absolute path for lint and jacoco reports! property("sonar.androidLint.reportPaths", "app/build/reports/lint-results-debug.xml") From 0df590363eae7ca9d00585a92497742459d3ac85 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 10:20:15 +0200 Subject: [PATCH 15/43] ci: use layout.buildDirectory to define sonar config paths --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index bd430d57..a3b0a9ce 100644 --- a/build.gradle +++ b/build.gradle @@ -34,9 +34,9 @@ sonarqube { property("sonar.sourceEncoding", "UTF-8") property("sonar.host.url", "https://sonarcloud.io") - property("sonar.binaries", "app/build/tmp/kotlin-classes/debug") + 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", "app/build/reports/lint-results-debug.xml") - property("sonar.coverage.jacoco.xmlReportPaths", "app/build/mergedReportDir/jacocoTestReport/jacocoTestReport.xml") + 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) } } From a3a4ee8d6aafbe96e1800582f4431118ae199ae6 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 10:52:59 +0200 Subject: [PATCH 16/43] ci: update circleci cache keys for gradle and add caching for bundler as well --- .circleci/config.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 550944d5..fe3c122b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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" }}-{{ checksum "settings.gradle" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }} + 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" }}-{{ checksum "settings.gradle" }}-{{ checksum "gradle.properties" }}-{{ checksum "app/build.gradle.kts" }} + save_bundler_cache: + steps: + - save_cache: + paths: + - vendor/bundle + key: v1-bundler-cache-{{ checksum "Gemfile.lock" }} executors: android-machine: @@ -37,6 +47,7 @@ jobs: steps: - checkout - restore_gradle_cache + - restore_bundler_cache - ruby/install-deps: with-cache: true - run: @@ -44,6 +55,7 @@ jobs: command: | bundle exec fastlane testDev - save_gradle_cache + - save_bundler_cache - run: name: Analyze on SonarCloud command: ./gradlew lintDebug sonar From 3bbebfd7032c5b883770584b042b5e2b5c7aedf8 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 11:18:32 +0200 Subject: [PATCH 17/43] ci: ensure all dependencies including nokogiri are compatible with Ruby 3 --- Gemfile | 2 +- Gemfile.lock | 72 ++++++++++++++++++++++++++-------------------------- 2 files changed, 37 insertions(+), 37 deletions(-) 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 4b4d9172..924f46d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,14 +70,14 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.218.0) + fastlane (2.227.2) 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) + 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.80.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-core (0.18.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.24.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.51.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.1) - 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) @@ -188,10 +187,10 @@ GEM 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 From 5670cbaa309c64d862d5b8d79ba31d3e86543736 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 11:30:51 +0200 Subject: [PATCH 18/43] chore: exclude ui files form coverage --- app/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bd35ee0a..629b4f76 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -113,7 +113,8 @@ tasks.register("jacocoTestReport") { val fileFilter = listOf( "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", "**/*Test*.*", "android/**/*.*", - "**/Dagger*.*", "**/*_Hilt*.*", "**/*Hilt*.*" + "**/Dagger*.*", "**/*_Hilt*.*", "**/*Hilt*.*", + "**/ui/**" // Exclude UI files from coverage ) val javaDebugTree = fileTree(layout.buildDirectory.dir("intermediates/javac/debug/classes")) { exclude(fileFilter) } val kotlinDebugTree = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) { exclude(fileFilter) } From 43428caf53ed94e2a5f852c60dcacf0bd36dca1b Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 11:53:27 +0200 Subject: [PATCH 19/43] ci: add lint to android-test job --- .circleci/config.yml | 1 + fastlane/Fastfile | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index fe3c122b..74156eea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,6 +54,7 @@ jobs: name: Fastlane - run all tests with coverage report command: | bundle exec fastlane testDev + bundle exec fastlane lint - save_gradle_cache - save_bundler_cache - run: 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: [ From 627b03ea335a14cde5394b86aea0b0c2fcc09288 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Thu, 29 May 2025 12:21:26 +0200 Subject: [PATCH 20/43] refactor: update access modifiers for UserSettingRepositoryAndroidTest and MainActivityTest --- .../java/org/kabiri/android/usbterminal/MainActivityTest.kt | 2 +- .../data/repository/UserSettingRepositoryAndroidTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt index 5a9c56a9..811120b0 100644 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt @@ -13,7 +13,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class MainActivityTest { +internal class MainActivityTest { @get:Rule var rule = activityScenarioRule() 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()) From d2d34e6d4f307925a10787b3dbb6f4b174e65789 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 14:06:41 +0200 Subject: [PATCH 21/43] chore: update gradle (#32) * chore: update gradle * chore: change project-level build.gradle file to build.gradle.kts * chore: use gradle version catalog in project level build.gradle.kts file * chore: update gradle android plugin to 8.10.1 * refactor: move dependency definitions from app level build.gradle.kts to gradle catalog toml file * nitpick: update instrumented test cluster; no changes * nitpick: update comments * nitpick: make the version catalog file more readable * deps: add missing coroutines.test dependency implementation for instrumented tests * nitpick: rename MainActivityTest.kt to MainActivityAndroidTest.kt * tests: make sure clicking on the settings button opens the settings sheet * tests: make sure action items are accessible to allow testing on headless emulator e.g. on the CI * tests: separate checking action menu items in another method * tests (nitpick): rename unused cached error to _ * ci: record a video of emulator screen while running instrumented tests and store them in the artifacts folder * chore: change settings.gradle to settings.gradle.kts --- .circleci/config.yml | 4 +- .github/workflows/android.yml | 16 ++- app/build.gradle.kts | 112 ++++++++--------- .../usbterminal/MainActivityAndroidTest.kt | 74 +++++++++++ .../android/usbterminal/MainActivityTest.kt | 27 ---- build.gradle => build.gradle.kts | 28 ++--- gradle/libs.versions.toml | 116 ++++++++++++++++++ settings.gradle => settings.gradle.kts | 6 +- 8 files changed, 267 insertions(+), 116 deletions(-) create mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt delete mode 100644 app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityTest.kt rename build.gradle => build.gradle.kts (50%) create mode 100644 gradle/libs.versions.toml rename settings.gradle => settings.gradle.kts (73%) 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") From fb846a3183c2160831aecb79825b8699dbfeeabf Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 14:52:26 +0200 Subject: [PATCH 22/43] chore: add dependabot config (#33) --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml 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 From 4ff9fbd10870423fddc0bc72cfb932b2b15e8892 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:53:19 +0000 Subject: [PATCH 23/43] chore(deps): bump com.google.gms:google-services from 4.4.1 to 4.4.2 Bumps com.google.gms:google-services from 4.4.1 to 4.4.2. --- updated-dependencies: - dependency-name: com.google.gms:google-services dependency-version: 4.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9825e2ba..e11ce8fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -105,7 +105,7 @@ androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "andro # --- 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" } +google-services = { module = "com.google.gms:google-services", version = "4.4.2" } firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.9" } [plugins] From 436c33a7812b98bb446e3ae998ebf9d339807866 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:53:24 +0000 Subject: [PATCH 24/43] chore(deps): bump com.google.firebase:firebase-bom Bumps com.google.firebase:firebase-bom from 32.8.0 to 33.15.0. --- updated-dependencies: - dependency-name: com.google.firebase:firebase-bom dependency-version: 33.15.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9825e2ba..e9a160cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ composeBom = "2023.06.01" material = "1.12.0" # Firebase -firebaseBom = "32.8.0" +firebaseBom = "33.15.0" # Dagger/Hilt hilt = "2.56.2" From fe0333ace89b0290d2ff743397dc2fc7f374e63d Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 16:39:26 +0200 Subject: [PATCH 25/43] ci: Update Github Actions cache keys ot consider version catalog toml file (#39) * ci: update github actions cache keys ot consider version catalog toml file * ci: add caching for the ui-test job as well * ci: reuse cache keys by defining them in a top-level env block and reference them in each job; add caching for the lint job * ci: revert the keys to the way they used to be defined because the runner context is not available in the top-level env block * ci: store the ui test results in the artifacts --- .github/workflows/android.yml | 46 ++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d0ca2b18..fc47e197 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,14 +21,14 @@ 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.kts') }} + 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 @@ -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 @@ -106,6 +134,12 @@ jobs: 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 From b3c542a3f4684898b17d2691f4024a61823e7515 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 17:32:48 +0200 Subject: [PATCH 26/43] accessibility: add button action descriptions --- app/src/main/res/menu/activity_main_menu.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 83aa59e0fdc8c842e3e37d096146ea69bda6ade9 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 17:33:50 +0200 Subject: [PATCH 27/43] tests: make sure action buttons cannot be clicked or asserted in ui tests correctly regardless of being visible or hidden in an action menu --- .../usbterminal/MainActivityAndroidTest.kt | 94 ++++++++++++++----- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt index 9ef1a98c..bda96883 100644 --- a/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt +++ b/app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt @@ -3,13 +3,14 @@ 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.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 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -21,15 +22,19 @@ internal class MainActivityAndroidTest { @get:Rule var rule = activityScenarioRule() - private fun ensureMenuIsAccessible(menuItemId: Int) { + 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( - InstrumentationRegistry.getInstrumentation().targetContext - ) + openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) + onOverflow() } } @@ -44,31 +49,70 @@ internal class MainActivityAndroidTest { } @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) + fun checkActionMenuItemSettingsIsDisplayed() = ensureMenuIsAccessible( + menuItemId = R.id.actionSettings, + onVisible = { - // 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())) - } + // 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 - // Ensure the action item is accisble, either in the toolbar or via overflow - ensureMenuIsAccessible(R.id.actionSettings) + ensureMenuIsAccessible( + menuItemId = R.id.actionSettings, + onVisible = { - // act - onView(withId(R.id.actionSettings)).perform(click()) + // act + onView(withId(R.id.actionSettings)).perform(click()) - // assert - onView(withId(R.id.composeViewSettingContent)).check(matches(isDisplayed())) + // 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())) + } + ) } } From 56015980a06d55b2ccb782164e8e525c7bffdce9 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 17:38:26 +0200 Subject: [PATCH 28/43] ci: avoid a false true being thrown in case of failing ui tests --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d0ca2b18..2867b8fe 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -98,7 +98,7 @@ jobs: script: | adb shell screenrecord /sdcard/ui-test.mp4 & SCREENRECORD_PID=$! - ./gradlew connectedCheck || true + ./gradlew connectedCheck kill $SCREENRECORD_PID || true adb pull /sdcard/ui-test.mp4 ./ui-test.mp4 || true - name: Upload UI test video From e09020d7e2ee3358c310faa8706c94f5bca56765 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 18:11:31 +0200 Subject: [PATCH 29/43] chore: hopefully include ui test coverage in the coverage report --- app/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5efd8972..e8f0f0e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,7 +114,6 @@ tasks.register("jacocoTestReport") { "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", "**/*Test*.*", "android/**/*.*", "**/Dagger*.*", "**/*_Hilt*.*", "**/*Hilt*.*", - "**/ui/**" // Exclude UI files from coverage ) val javaDebugTree = fileTree(layout.buildDirectory.dir("intermediates/javac/debug/classes")) { exclude(fileFilter) } val kotlinDebugTree = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) { exclude(fileFilter) } From 647c876ef439deb6eeabf0367c35f88662fdb328 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 22 Jun 2025 18:23:35 +0200 Subject: [PATCH 30/43] ci: consider changes to gradle version catalog toml file in order to update the circleci cache key --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d921cc1a..b84cfb92 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.kts" }}-{{ checksum "settings.gradle.kts" }}-{{ 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" }}-{{ checksum "gradle/libs.versions.toml" }} restore_bundler_cache: steps: - restore_cache: @@ -24,7 +24,7 @@ commands: - save_cache: paths: - ~/.gradle/caches - key: v1-gradle-cache-{{ arch }}-{{ checksum "build.gradle.kts" }}-{{ checksum "settings.gradle.kts" }}-{{ 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" }}-{{ checksum "gradle/libs.versions.toml" }} save_bundler_cache: steps: - save_cache: From 5b7f885fd0771b8917209e506969f23b69470a6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:39:24 +0000 Subject: [PATCH 31/43] chore(deps): bump com.google.truth:truth from 1.1.3 to 1.4.4 Bumps [com.google.truth:truth](https://github.com/google/truth) from 1.1.3 to 1.4.4. - [Release notes](https://github.com/google/truth/releases) - [Commits](https://github.com/google/truth/commits/v1.4.4) --- updated-dependencies: - dependency-name: com.google.truth:truth dependency-version: 1.4.4 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9825e2ba..143cee78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ espressoCore = "3.6.1" hamcrest = "2.2" testRunner = "1.6.2" testRules = "1.6.1" -truth = "1.1.3" +truth = "1.4.4" androidxTruth = "1.6.0" [libraries] From 48327dd0c7fae2b3b4aec87342af4e74b6c3818a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:21:57 +0000 Subject: [PATCH 32/43] chore(deps): bump coroutines from 1.7.3 to 1.10.2 Bumps `coroutines` from 1.7.3 to 1.10.2. Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core` from 1.7.3 to 1.10.2 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.10.2) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-android` from 1.7.3 to 1.10.2 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.10.2) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.7.3 to 1.10.2 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.10.2) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a5a41bb..ee6e7ac8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ hilt = "2.56.2" hiltAndroidTesting = "2.56.2" # Coroutines -coroutines = "1.7.3" +coroutines = "1.10.2" # USB Serial usbSerial = "6.1.1" From b7b84d3599f6c1b6be158ab2b6fd179eec8c10d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:25:10 +0000 Subject: [PATCH 33/43] chore(deps): bump androidx.datastore:datastore-preferences Bumps androidx.datastore:datastore-preferences from 1.1.4 to 1.1.7. --- updated-dependencies: - dependency-name: androidx.datastore:datastore-preferences dependency-version: 1.1.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 859283f1..fbed569d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ lifecycleRuntimeKtx = "2.8.7" lifecycleViewmodelKtx = "2.8.7" lifecycleExtensions = "2.2.0" activityCompose = "1.10.1" -datastorePreferences = "1.1.4" +datastorePreferences = "1.1.7" # Compose composeBom = "2023.06.01" From 27f0fd7640a50a4c7b8e3f6323bfad46d4736781 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:25:22 +0000 Subject: [PATCH 34/43] chore(deps): bump androidx.lifecycle:lifecycle-viewmodel-ktx Bumps androidx.lifecycle:lifecycle-viewmodel-ktx from 2.8.7 to 2.9.1. --- updated-dependencies: - dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx dependency-version: 2.9.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 859283f1..df8bfe16 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ appcompat = "1.7.0" coreKtx = "1.16.0" constraintlayout = "2.2.1" lifecycleRuntimeKtx = "2.8.7" -lifecycleViewmodelKtx = "2.8.7" +lifecycleViewmodelKtx = "2.9.1" lifecycleExtensions = "2.2.0" activityCompose = "1.10.1" datastorePreferences = "1.1.4" From c017e9928928b314ce8184766817f0a8730f61a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:25:26 +0000 Subject: [PATCH 35/43] chore(deps): bump org.hamcrest:hamcrest-library from 2.2 to 3.0 Bumps [org.hamcrest:hamcrest-library](https://github.com/hamcrest/JavaHamcrest) from 2.2 to 3.0. - [Release notes](https://github.com/hamcrest/JavaHamcrest/releases) - [Changelog](https://github.com/hamcrest/JavaHamcrest/blob/master/CHANGES.md) - [Commits](https://github.com/hamcrest/JavaHamcrest/compare/v2.2...v3.0) --- updated-dependencies: - dependency-name: org.hamcrest:hamcrest-library dependency-version: '3.0' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 859283f1..7371e1f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ testCore = "1.6.1" testExtJunit = "1.2.1" testExtJunitKtx = "1.2.1" espressoCore = "3.6.1" -hamcrest = "2.2" +hamcrest = "3.0" testRunner = "1.6.2" testRules = "1.6.1" truth = "1.4.4" From 5f5ce5b6a444a2d31e18bdda5108a8490a8a485d Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Sun, 6 Jul 2025 07:42:43 +0200 Subject: [PATCH 36/43] fix: update order of generating google-services.json file --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f5e1de88..a9218a9b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -34,9 +34,9 @@ jobs: - 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 From 2288c6514e335797d835dcef6630a0aeb35938fd Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:22:11 +0200 Subject: [PATCH 37/43] deps: update fastlane --- Gemfile.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 924f46d2..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.1108.0) - aws-sdk-core (3.224.1) + 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.101.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.187.0) - aws-sdk-core (~> 3, >= 3.224.1) + 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,7 +70,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.227.2) + fastlane (2.228.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -165,7 +165,7 @@ GEM mutex_m jmespath (1.6.2) json (2.12.2) - jwt (2.10.1) + jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) @@ -174,13 +174,13 @@ 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) From 08274e4ef65c8d4f81de34b50ba247af5ede8447 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:27:07 +0200 Subject: [PATCH 38/43] deps: update appcompat --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 078b5079..0f67f88c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ kotlin = "2.1.20" jacoco = "0.8.8" # AndroidX -appcompat = "1.7.0" +appcompat = "1.7.1" coreKtx = "1.16.0" constraintlayout = "2.2.1" lifecycleRuntimeKtx = "2.8.7" From 5aec2d5cd7069c9395770716f52b12d78b210ee1 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:29:05 +0200 Subject: [PATCH 39/43] deps: update lifecycle-runtime-ktx to 2.9.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f67f88c..176bdd19 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ jacoco = "0.8.8" appcompat = "1.7.1" coreKtx = "1.16.0" constraintlayout = "2.2.1" -lifecycleRuntimeKtx = "2.8.7" +lifecycleRuntimeKtx = "2.9.1" lifecycleViewmodelKtx = "2.9.1" lifecycleExtensions = "2.2.0" activityCompose = "1.10.1" From 83be85b1187202c2d7ee2ef1695fac9f667ec112 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:30:16 +0200 Subject: [PATCH 40/43] deps: update composeBom to 2025.06.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 176bdd19..0de19f9e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ activityCompose = "1.10.1" datastorePreferences = "1.1.7" # Compose -composeBom = "2023.06.01" +composeBom = "2025.06.01" # Google/Material material = "1.12.0" From e8720984f728fe89307738767ad547a8856303b0 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:30:47 +0200 Subject: [PATCH 41/43] deps: update firebaseBom to 33.16.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0de19f9e..089ffe38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ composeBom = "2025.06.01" material = "1.12.0" # Firebase -firebaseBom = "33.15.0" +firebaseBom = "33.16.0" # Dagger/Hilt hilt = "2.56.2" From 2440dda930587911ca1a349c89a9452a8680efe3 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:33:31 +0200 Subject: [PATCH 42/43] deps: update google-services to 4.4.3 and firebase-crashlytics-gradle to 3.0.4 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 089ffe38..61b952fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -105,8 +105,8 @@ androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "andro # --- 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.2" } -firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.9" } +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 --- From cfe5e00a7a0f8de3331b39b928874303bef624c9 Mon Sep 17 00:00:00 2001 From: Ali Kabiri Date: Tue, 8 Jul 2025 19:57:49 +0200 Subject: [PATCH 43/43] chore: bump versionName to 0.9.85 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e8f0f0e3..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" }