diff --git a/.circleci/config.yml b/.circleci/config.yml index 371e685527e..2f3a817b826 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ - # Use the latest 2.1 version of CircleCI pipeline process engine. + # Use the latest 2.1 version of CircleCI pipeline process engine. # See: https://circleci.com/docs/2.0/configuration-reference version: 2.1 @@ -8,12 +8,20 @@ orbs: codecov: codecov/codecov@3.3.0 jobs: - # Below is the definition of your job to build and test your app, you can rename and customize it as you want. - build-and-test: + # Compile job: compiles sources and persists the workspace for downstream jobs + compile: machine: true resource_class: nightscout/android steps: + - run: + name: Skip CircleCI on pull requests + command: | + if [ -n "${CIRCLE_PULL_REQUEST:-}" ]; then + echo "Pull request detected; skipping CircleCI job." + circleci-agent step halt + fi + - checkout - run: @@ -23,12 +31,66 @@ jobs: export ANDROID_HOME=/usr/lib/android-sdk env ./gradlew \ - -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \ + -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss4m" \ -Dkotlin.daemon.jvm.options="-Xmx2g" \ -Dkotlin.compiler.execution.strategy="in-process" \ -Dorg.gradle.daemon=true \ compileFullDebugAndroidTestSources + - persist_to_workspace: + root: . + paths: + - . + + # Unit test job: runs in parallel with instrumented-test + unit-test: + machine: true + resource_class: nightscout/android + + steps: + - attach_workspace: + at: . + + - run: + name: Run testFullDebugUnitTest + command: | + export ANDROID_SDK_ROOT=/usr/lib/android-sdk + export ANDROID_HOME=/usr/lib/android-sdk + ./gradlew \ + -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss4m" \ + -Dkotlin.daemon.jvm.options="-Xmx2g" \ + -Dkotlin.compiler.execution.strategy="in-process" \ + -Dorg.gradle.workers.max=22 \ + -Dorg.gradle.daemon=true \ + testFullDebugUnitTest + + - run: + name: Save unit test results + command: | + mkdir -p ~/test-results/junit/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/junit/ \; + when: always + + - store_test_results: + path: ~/test-results + + - store_artifacts: + path: ~/test-results/junit + + - persist_to_workspace: + root: . + paths: + - "*/build/test-results" + + # Instrumented test job: runs in parallel with unit-test + instrumented-test: + machine: true + resource_class: nightscout/android + + steps: + - attach_workspace: + at: . + - run: name: Create avd command: | @@ -49,40 +111,20 @@ jobs: export ANDROID_HOME=/usr/lib/android-sdk env ./gradlew \ - -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \ + -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss4m" \ -Dkotlin.daemon.jvm.options="-Xmx2g" \ -Dkotlin.compiler.execution.strategy="in-process" \ -Dorg.gradle.daemon=true \ connectedFullDebugAndroidTest - + - run: name: Kill emulators command: | echo "Killing emulators" adb devices | grep emulator | cut -f1 | while read -r line; do adb -s $line emu kill; done - - - run: - name: Run testFullDebugUnitTest - command: | - export ANDROID_SDK_ROOT=/usr/lib/android-sdk - export ANDROID_HOME=/usr/lib/android-sdk - ./gradlew \ - -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \ - -Dkotlin.daemon.jvm.options="-Xmx2g" \ - -Dkotlin.compiler.execution.strategy="in-process" \ - -Dorg.gradle.workers.max=22 \ - -Dorg.gradle.daemon=true \ - testFullDebugUnitTest - - - run: - run: Run jacocoAllDebugReport - command: | - export ANDROID_SDK_ROOT=/usr/lib/android-sdk - export ANDROID_HOME=/usr/lib/android-sdk - ./gradlew --stacktrace jacocoAllDebugReport - run: - name: Save test results + name: Save instrumented test results command: | mkdir -p ~/test-results/junit/ find . -type f -regex ".*/build/outputs/androidTest-results/.*xml" -exec cp {} ~/test-results/junit/ \; @@ -94,6 +136,33 @@ jobs: - store_artifacts: path: ~/test-results/junit + - persist_to_workspace: + root: . + paths: + - "*/build/outputs/androidTest-results" + + - run: + name: Kill java processes + command: | + killall java + when: always + + # Coverage job: depends on both test jobs, merges and uploads coverage + coverage: + machine: true + resource_class: nightscout/android + + steps: + - attach_workspace: + at: . + + - run: + name: Run jacocoAllDebugReport + command: | + export ANDROID_SDK_ROOT=/usr/lib/android-sdk + export ANDROID_HOME=/usr/lib/android-sdk + ./gradlew --stacktrace jacocoAllDebugReport + - codecov/upload: file: './build/reports/jacoco/jacocoAllDebugReport/jacocoAllDebugReport.xml' @@ -104,10 +173,18 @@ jobs: when: always workflows: - # Below is the definition of your workflow. - # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above. # CircleCI will run this workflow on every commit. - # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows + # unit-test and instrumented-test run in parallel after compile, then coverage runs last. dotests: jobs: - - build-and-test + - compile + - unit-test: + requires: + - compile + - instrumented-test: + requires: + - compile + - coverage: + requires: + - unit-test + - instrumented-test diff --git a/.circleci/config.yml.cloud b/.circleci/config.yml.cloud index 4593de71fc5..1270afb8c6f 100644 --- a/.circleci/config.yml.cloud +++ b/.circleci/config.yml.cloud @@ -17,6 +17,14 @@ jobs: tag: 2023.11.1 steps: + - run: + name: Skip CircleCI on pull requests + command: | + if [ -n "${CIRCLE_PULL_REQUEST:-}" ]; then + echo "Pull request detected; skipping CircleCI job." + circleci-agent step halt + fi + - checkout - android/change-java-version: diff --git a/.github/workflows/aaps-ci.yml b/.github/workflows/aaps-ci.yml index e103317ce39..7d5de7c46ae 100644 --- a/.github/workflows/aaps-ci.yml +++ b/.github/workflows/aaps-ci.yml @@ -181,7 +181,9 @@ jobs: # When upgrading the JDK, please update this section accordingly as well. java-version: 21 distribution: 'temurin' - cache: gradle + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -189,12 +191,11 @@ jobs: - name: Build APKs run: | ./gradlew :app:assemble${{ env.BUILD_VARIANT }} :wear:assemble${{ env.BUILD_VARIANT }} \ - -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \ + -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss4m" \ -Dkotlin.daemon.jvm.options="-Xmx2g" \ -Dkotlin.compiler.execution.strategy="in-process" \ -Dorg.gradle.daemon=true \ -Dorg.gradle.workers.max=8 \ - -Dorg.gradle.caching=true \ -Pandroid.injected.signing.store.file="$RUNNER_TEMP/keystore/keystore.jks" \ -Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \ -Pandroid.injected.signing.key.alias="$KEY_ALIAS" \ diff --git a/.github/workflows/cherry-pick-ci.yml b/.github/workflows/cherry-pick-ci.yml index d213f5c64c7..7a30ac0af96 100644 --- a/.github/workflows/cherry-pick-ci.yml +++ b/.github/workflows/cherry-pick-ci.yml @@ -211,7 +211,9 @@ jobs: # When upgrading the JDK, please update this section accordingly as well. java-version: 21 distribution: 'temurin' - cache: gradle + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -219,12 +221,11 @@ jobs: - name: Build APKs run: | ./gradlew :app:assemble${{ env.BUILD_VARIANT }} :wear:assemble${{ env.BUILD_VARIANT }} \ - -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \ + -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss4m" \ -Dkotlin.daemon.jvm.options="-Xmx2g" \ -Dkotlin.compiler.execution.strategy="in-process" \ -Dorg.gradle.daemon=true \ -Dorg.gradle.workers.max=8 \ - -Dorg.gradle.caching=true \ -Pandroid.injected.signing.store.file="$RUNNER_TEMP/keystore/keystore.jks" \ -Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \ -Pandroid.injected.signing.key.alias="$KEY_ALIAS" \ diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 02b297e87c3..87f632b2c6e 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,6 +12,8 @@ on: jobs: claude-review: + continue-on-error: true + if: ${{ false }} # Optional: Filter by PR author # if: | # github.event.pull_request.user.login == 'external-contributor' || diff --git a/.github/workflows/pr-ci-untrusted.yml b/.github/workflows/pr-ci-untrusted.yml new file mode 100644 index 00000000000..16fa9ed6a38 --- /dev/null +++ b/.github/workflows/pr-ci-untrusted.yml @@ -0,0 +1,151 @@ +name: Pull Request CI (Untrusted) + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + +permissions: + contents: read + +concurrency: + group: pr-ci-untrusted-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + compile: + name: Compile + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-home-cache-cleanup: true + cache-read-only: false + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK platform + run: sdkmanager "platforms;android-36" + + - name: Compile fullDebug + run: | + ./gradlew --stacktrace assembleFullDebug compileFullDebugAndroidTestSources \ + -Dorg.gradle.workers.max=4 \ + -Dkotlin.compiler.execution.strategy=in-process + + unit-tests: + name: Unit tests + runs-on: ubuntu-latest + needs: compile + timeout-minutes: 30 + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: true + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + # Unit tests are JVM-only — no need to download the platform SDK here. + # Compilation already ran in the compile job; we just execute cached test tasks. + + - name: Run unit tests + run: | + ./gradlew --stacktrace testFullDebugUnitTest \ + -Dorg.gradle.workers.max=4 \ + -Dkotlin.compiler.execution.strategy=in-process + + - name: Upload unit test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: unit-test-results + if-no-files-found: warn + path: | + **/build/test-results/**/*.xml + **/build/reports/tests/** + + connected-tests: + name: Connected tests + runs-on: ubuntu-latest + needs: compile + timeout-minutes: 45 + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: true + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK platform + run: sdkmanager "platforms;android-36" + + - name: Enable KVM access + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run connected tests and coverage + uses: ReactiveCircus/android-emulator-runner@v2 + with: + api-level: 31 + arch: x86_64 + profile: pixel_6 + target: google_apis + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none + script: >- + ./gradlew --stacktrace connectedFullDebugAndroidTest jacocoAllDebugReport + -Dorg.gradle.workers.max=4 + -Dorg.gradle.jvmargs="-Xmx5g -XX:+UseParallelGC -Xss4m" + + - name: Upload connected test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: connected-test-results + if-no-files-found: warn + path: | + **/build/outputs/androidTest-results/** + **/build/reports/androidTests/** + build/reports/jacoco/jacocoAllDebugReport/** diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 59ea7750de7..99f43337e1d 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -217,7 +217,9 @@ jobs: # When upgrading the JDK, please update this section accordingly as well. java-version: 21 distribution: 'temurin' - cache: gradle + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -225,12 +227,11 @@ jobs: - name: Build APKs run: | ./gradlew :app:assemble${{ env.BUILD_VARIANT }} :wear:assemble${{ env.BUILD_VARIANT }} \ - -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss1024m" \ + -Dorg.gradle.jvmargs="-Xmx8g -XX:+UseParallelGC -Xss4m" \ -Dkotlin.daemon.jvm.options="-Xmx2g" \ -Dkotlin.compiler.execution.strategy="in-process" \ -Dorg.gradle.daemon=true \ -Dorg.gradle.workers.max=8 \ - -Dorg.gradle.caching=true \ -Pandroid.injected.signing.store.file="$RUNNER_TEMP/keystore/keystore.jks" \ -Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \ -Pandroid.injected.signing.key.alias="$KEY_ALIAS" \ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f0b9315ff4..512e11081f1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,69 +18,37 @@ repositories { google() } -fun generateGitBuild(): String { - try { - val processBuilder = ProcessBuilder("git", "describe", "--always") - val output = File.createTempFile("git-build", "") - processBuilder.redirectOutput(output) - val process = processBuilder.start() - process.waitFor() - return output.readText().trim() - } catch (_: Exception) { - return "NoGitSystemAvailable" - } -} - -fun generateGitRemote(): String { - try { - val processBuilder = ProcessBuilder("git", "remote", "get-url", "origin") - val output = File.createTempFile("git-remote", "") - processBuilder.redirectOutput(output) - val process = processBuilder.start() - process.waitFor() - return output.readText().trim() - } catch (_: Exception) { - return "NoGitSystemAvailable" - } -} +val gitBuild: Provider = providers.exec { + commandLine("git", "describe", "--always") + isIgnoreExitValue = true +}.standardOutput.asText.map { it.trim() }.orElse("NoGitSystemAvailable") + +val gitRemote: Provider = providers.exec { + commandLine("git", "remote", "get-url", "origin") + isIgnoreExitValue = true +}.standardOutput.asText.map { it.trim() }.orElse("NoGitSystemAvailable") + +val gitAvailable: Provider = providers.exec { + commandLine("git", "--version") + isIgnoreExitValue = true +}.result.map { it.exitValue == 0 }.orElse(false) + +val allCommitted: Provider = providers.exec { + commandLine("git", "status", "-s") + isIgnoreExitValue = true +}.standardOutput.asText.map { text -> + text.replace(Regex("""(?m)^\s*(M|A|D|\?\?)\s*.*?\.idea\/codeStyles\/.*?\s*$"""), "") + // ignore all files added to project dir but not staged/known to GIT + .replace(Regex("""(?m)^\s*(\?\?)\s*.*?\s*$"""), "").trim().isEmpty() +}.orElse(false) fun generateDate(): String { - val stringBuilder: StringBuilder = StringBuilder() // showing only date prevents app to rebuild everytime - stringBuilder.append(SimpleDateFormat("yyyy.MM.dd").format(Date())) - return stringBuilder.toString() + return SimpleDateFormat("yyyy.MM.dd").format(Date()) } fun isMaster(): Boolean = !Versions.appVersion.contains("-") -fun gitAvailable(): Boolean { - try { - val processBuilder = ProcessBuilder("git", "--version") - val output = File.createTempFile("git-version", "") - processBuilder.redirectOutput(output) - val process = processBuilder.start() - process.waitFor() - return output.readText().isNotEmpty() - } catch (_: Exception) { - return false - } -} - -fun allCommitted(): Boolean { - try { - val processBuilder = ProcessBuilder("git", "status", "-s") - val output = File.createTempFile("git-comited", "") - processBuilder.redirectOutput(output) - val process = processBuilder.start() - process.waitFor() - return output.readText().replace(Regex("""(?m)^\s*(M|A|D|\?\?)\s*.*?\.idea\/codeStyles\/.*?\s*$"""), "") - // ignore all files added to project dir but not staged/known to GIT - .replace(Regex("""(?m)^\s*(\?\?)\s*.*?\s*$"""), "").trim().isEmpty() - } catch (_: Exception) { - return false - } -} - android { namespace = "app.aaps" @@ -90,10 +58,10 @@ android { targetSdk = Versions.targetSdk buildConfigField("String", "VERSION", "\"$version\"") - buildConfigField("String", "BUILDVERSION", "\"${generateGitBuild()}-${generateDate()}\"") - buildConfigField("String", "REMOTE", "\"${generateGitRemote()}\"") - buildConfigField("String", "HEAD", "\"${generateGitBuild()}\"") - buildConfigField("String", "COMMITTED", "\"${allCommitted()}\"") + buildConfigField("String", "BUILDVERSION", "\"${gitBuild.get()}-${generateDate()}\"") + buildConfigField("String", "REMOTE", "\"${gitRemote.get()}\"") + buildConfigField("String", "HEAD", "\"${gitBuild.get()}\"") + buildConfigField("String", "COMMITTED", "\"${allCommitted.get()}\"") // For Dagger injected instrumentation tests in app module testInstrumentationRunner = "app.aaps.runners.InjectedTestRunner" @@ -222,13 +190,13 @@ dependencies { println("-------------------") println("isMaster: ${isMaster()}") -println("gitAvailable: ${gitAvailable()}") -println("allCommitted: ${allCommitted()}") +println("gitAvailable: ${gitAvailable.get()}") +println("allCommitted: ${allCommitted.get()}") println("-------------------") -if (!gitAvailable()) { +if (!gitAvailable.get()) { throw GradleException("GIT system is not available. On Windows try to run Android Studio as an Administrator. Check if GIT is installed and Studio have permissions to use it") } -if (isMaster() && !allCommitted()) { +if (isMaster() && !allCommitted.get()) { throw GradleException("There are uncommitted changes. Clone sources again as described in wiki and do not allow gradle update") } diff --git a/app/src/androidTest/kotlin/app/aaps/LoopTest.kt b/app/src/androidTest/kotlin/app/aaps/LoopTest.kt index 3d37a14689a..57587f53762 100644 --- a/app/src/androidTest/kotlin/app/aaps/LoopTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/LoopTest.kt @@ -150,7 +150,7 @@ class LoopTest @Inject constructor() { // Loop should fail on no result from APS plugin loop.invoke("test3", allowNotification = false) - loopStatusEvent = rxHelper.waitFor(EventLoopSetLastRunGui::class.java, comment = "step4") + loopStatusEvent = rxHelper.waitFor(EventLoopSetLastRunGui::class.java, maxSeconds = 120, comment = "step4") assertThat(loopStatusEvent.first).isTrue() assertThat((loopStatusEvent.second as EventLoopSetLastRunGui).text).contains("NO APS SELECTED OR PROVIDED RESULT") val apsStatusEvent = rxHelper.waitFor(EventResetOpenAPSGui::class.java, comment = "step5") diff --git a/gradle.properties b/gradle.properties index df3dbde77de..f6262e7c8d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,14 +17,21 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true org.gradle.warning.mode=all -org.gradle.jvmargs=-Xmx8g -XX:+UseParallelGC -Xss1024m +org.gradle.jvmargs=-Xmx8g -XX:+UseParallelGC -Xss4m -XX:+HeapDumpOnOutOfMemoryError org.gradle.workers.max=12 kotlin.daemon.jvm.options=-Xmx2g +# Enable local build cache (speeds up incremental builds and branch switches) +org.gradle.caching=true + +# Enable configuration cache (Gradle 9+ / AGP 8+ compatible) +org.gradle.configuration-cache=true + android.enableJetifier=false android.useAndroidX=true android.nonTransitiveRClass=true -# Cache is causeing issues with CircleCI nad maybe Studio 2021 -# org.gradle.unsafe.configuration-cache=true android.nonFinalResIds=true + +# Kotlin incremental compilation +kotlin.incremental=true diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 6325b731618..23f48377e44 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -16,24 +16,14 @@ repositories { google() } -fun generateGitBuild(): String { - try { - val processBuilder = ProcessBuilder("git", "describe", "--always") - val output = File.createTempFile("git-build", "") - processBuilder.redirectOutput(output) - val process = processBuilder.start() - process.waitFor() - return output.readText().trim() - } catch (_: Exception) { - return "NoGitSystemAvailable" - } -} +val gitBuild: Provider = providers.exec { + commandLine("git", "describe", "--always") + isIgnoreExitValue = true +}.standardOutput.asText.map { it.trim() }.orElse("NoGitSystemAvailable") fun generateDate(): String { - val stringBuilder: StringBuilder = StringBuilder() // showing only date prevents app to rebuild everytime - stringBuilder.append(SimpleDateFormat("yyyy.MM.dd").format(Date())) - return stringBuilder.toString() + return SimpleDateFormat("yyyy.MM.dd").format(Date()) } @@ -44,7 +34,7 @@ android { minSdk = Versions.wearMinSdk targetSdk = Versions.wearTargetSdk - buildConfigField("String", "BUILDVERSION", "\"${generateGitBuild()}-${generateDate()}\"") + buildConfigField("String", "BUILDVERSION", "\"${gitBuild.get()}-${generateDate()}\"") } android {