From ac6305eb18a02d0bb2b623addab6603b563126ca Mon Sep 17 00:00:00 2001 From: pablo-guardiola Date: Wed, 4 Sep 2024 14:36:28 -0400 Subject: [PATCH 1/3] fix ProvidersTest#network_attributes() test TODO: - remove @Ignore annotation - refactor ClientAttributes, DeviceAttributes and NetworkAttributes tests out of ProvidersTest and into their respective test classes - add more coverage to ClientAttributes, DeviceAttributes and NetworkAttributes - bump robolectric dependency version to 4.13 --- .../bitdrift/capture/ClientAttributesTest.kt | 187 ++++++++++++++++++ .../bitdrift/capture/DeviceAttributesTest.kt | 35 ++++ .../bitdrift/capture/NetworkAttributesTest.kt | 136 +++++++++++++ .../io/bitdrift/capture/ProvidersTest.kt | 106 +--------- platform/jvm/gradle/libs.versions.toml | 2 +- 5 files changed, 363 insertions(+), 103 deletions(-) create mode 100644 platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ClientAttributesTest.kt create mode 100644 platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/DeviceAttributesTest.kt create mode 100644 platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/NetworkAttributesTest.kt diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ClientAttributesTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ClientAttributesTest.kt new file mode 100644 index 00000000..6af525ae --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ClientAttributesTest.kt @@ -0,0 +1,187 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.test.core.app.ApplicationProvider +import io.bitdrift.capture.attributes.ClientAttributes +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.anyString +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@Suppress("DEPRECATION") +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21]) +class ClientAttributesTest { + + @Test + fun foreground() { + val context = ApplicationProvider.getApplicationContext() + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("foreground", "1") + } + + @Test + fun not_foreground() { + val context = ApplicationProvider.getApplicationContext() + val mockedLifecycleOwnerLifecycleStateCreated = obtainMockedLifecycleOwnerWith(Lifecycle.State.CREATED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateCreated).invoke() + + assertThat(clientAttributes).containsEntry("foreground", "0") + } + + @Test + fun app_id() { + val packageName = "my.bitdrift.test" + val context = spy(ApplicationProvider.getApplicationContext()) + doReturn(packageName).`when`(context).packageName + val mockedLifecycleOwnerLifecycleStateStarted = + obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("app_id", packageName) + } + + @Test + fun app_id_unknown() { + val context = spy(ApplicationProvider.getApplicationContext()) + doReturn(null).`when`(context).packageName + val mockedLifecycleOwnerLifecycleStateStarted = + obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("app_id", "unknown") + } + + @Test + fun app_version() { + val versionName = "1.2.3.4" + val context = spy(ApplicationProvider.getApplicationContext()) + val mockedPackageInfo = obtainMockedPackageInfo(context) + mockedPackageInfo.versionName = versionName + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("app_version", versionName) + } + + @Test + fun app_version_unknown() { + val context = ApplicationProvider.getApplicationContext() + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("app_version", "?.?.?") + } + + @Test + fun app_version_code() { + val versionCode = 66 + val context = spy(ApplicationProvider.getApplicationContext()) + val mockedPackageInfo = obtainMockedPackageInfo(context) + mockedPackageInfo.versionCode = versionCode + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("_app_version_code", versionCode.toString()) + } + + @Test + fun app_version_code_unknown() { + val context = spy(ApplicationProvider.getApplicationContext()) + val packageManager = obtainMockedPackageManager(context) + doReturn(null).`when`(packageManager).getPackageInfo(anyString(), eq(0)) + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("_app_version_code", "-1") + } + + @Test + @Config(sdk = [28]) + fun app_version_code_android_pie() { + val versionCode = 66L + val context = spy(ApplicationProvider.getApplicationContext()) + val mockedPackageInfo = obtainMockedPackageInfo(context) + doReturn(versionCode).`when`(mockedPackageInfo).longVersionCode + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("_app_version_code", versionCode.toString()) + } + + @Test + @Config(sdk = [28]) + fun app_version_code_unknown_android_pie() { + val context = spy(ApplicationProvider.getApplicationContext()) + val packageManager = obtainMockedPackageManager(context) + doReturn(null).`when`(packageManager).getPackageInfo(anyString(), eq(0)) + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + val clientAttributes = ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + assertThat(clientAttributes).containsEntry("_app_version_code", "-1") + } + + @Test + @Config(sdk = [33]) + fun package_info_android_tiramisu() { + val context = spy(ApplicationProvider.getApplicationContext()) + val packageManager = obtainMockedPackageManager(context) + val mockedLifecycleOwnerLifecycleStateStarted = obtainMockedLifecycleOwnerWith(Lifecycle.State.STARTED) + + ClientAttributes(context, mockedLifecycleOwnerLifecycleStateStarted).invoke() + + verify(packageManager).getPackageInfo(anyString(), any(PackageManager.PackageInfoFlags::class.java)) + } + + private fun obtainMockedLifecycleOwnerWith(state: Lifecycle.State): LifecycleOwner { + val mockedLifecycle = mock(Lifecycle::class.java) + doReturn(state).`when`(mockedLifecycle).currentState + val mockedLifecycleOwner = mock(LifecycleOwner::class.java) + doReturn(mockedLifecycle).`when`(mockedLifecycleOwner).lifecycle + return mockedLifecycleOwner + } + + private fun obtainMockedPackageInfo(context: Context): PackageInfo { + val mockedPackageManager = obtainMockedPackageManager(context) + val mockedPackageInfo: PackageInfo = mock(PackageInfo::class.java) + doReturn(mockedPackageInfo).`when`(mockedPackageManager).getPackageInfo(anyString(), eq(0)) + return mockedPackageInfo + } + + private fun obtainMockedPackageManager(context: Context): PackageManager { + val mockedPackageManager: PackageManager = mock(PackageManager::class.java) + doReturn(mockedPackageManager).`when`(context).packageManager + return mockedPackageManager + } +} diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/DeviceAttributesTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/DeviceAttributesTest.kt new file mode 100644 index 00000000..293b4b39 --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/DeviceAttributesTest.kt @@ -0,0 +1,35 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import androidx.test.core.app.ApplicationProvider +import io.bitdrift.capture.attributes.DeviceAttributes +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21]) +class DeviceAttributesTest { + + @Test + fun model() { + val deviceAttributes = DeviceAttributes(ApplicationProvider.getApplicationContext()).invoke() + + assertThat(deviceAttributes).containsEntry("model", "robolectric") + } + + @Test + fun locale() { + val deviceAttributes = DeviceAttributes(ApplicationProvider.getApplicationContext()).invoke() + + assertThat(deviceAttributes).containsEntry("_locale", "en_US") + } +} diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/NetworkAttributesTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/NetworkAttributesTest.kt new file mode 100644 index 00000000..f57bd1fc --- /dev/null +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/NetworkAttributesTest.kt @@ -0,0 +1,136 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +package io.bitdrift.capture + +import android.Manifest +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.verify +import io.bitdrift.capture.attributes.NetworkAttributes +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [23]) +class NetworkAttributesTest { + + @Test + fun carrier() { + val context = ApplicationProvider.getApplicationContext() + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("carrier", "") + } + + @Test + fun network_type_access_network_state_granted() { + grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE) + val context = ApplicationProvider.getApplicationContext() + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("network_type", "wwan") + } + + @Test + @Config(sdk = [21]) + fun network_type_access_network_state_granted_android_lollipop() { + grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE) + val context = ApplicationProvider.getApplicationContext() + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("network_type", "other") + } + + @Test + fun network_type_access_network_state_not_granted() { + val context = ApplicationProvider.getApplicationContext() + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("network_type", "unknown") + } + + @Test + fun network_type_access_network_state_granted_null_network_capabilities() { + grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE) + val context = spy(ApplicationProvider.getApplicationContext()) + val mockedConnectivityManager = obtainMockedConnectivityManager(context) + val mockedActiveNetwork = obtainMockedActiveNetwork(mockedConnectivityManager) + doReturn(null).`when`(mockedConnectivityManager).getNetworkCapabilities(eq(mockedActiveNetwork)) + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("network_type", "unknown") + } + + @Test + fun network_type_access_network_state_granted_register_network_callback() { + grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE) + val context = spy(ApplicationProvider.getApplicationContext()) + val mockedConnectivityManager = obtainMockedConnectivityManager(context) + + NetworkAttributes(context).invoke() + + verify(mockedConnectivityManager).registerNetworkCallback( + any(NetworkRequest::class.java), + any(ConnectivityManager.NetworkCallback::class.java), + ) + } + + @Test + fun radio_type_read_phone_state_granted() { + grantPermissions(Manifest.permission.READ_PHONE_STATE) + val context = ApplicationProvider.getApplicationContext() + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("radio_type", "unknown") + } + + @Test + fun radio_type_read_phone_state_not_granted() { + val context = ApplicationProvider.getApplicationContext() + + val networkAttributes = NetworkAttributes(context).invoke() + + assertThat(networkAttributes).containsEntry("radio_type", "forbidden") + } + + private fun grantPermissions(vararg permissionNames: String) { + val app = Shadows.shadowOf(RuntimeEnvironment.getApplication()) + app.grantPermissions(*permissionNames) + } + + private fun obtainMockedConnectivityManager(context: Context): ConnectivityManager { + val mockedConnectivityManager: ConnectivityManager = mock(ConnectivityManager::class.java) + doReturn(mockedConnectivityManager).`when`(context).getSystemService(eq(Context.CONNECTIVITY_SERVICE)) + return mockedConnectivityManager + } + + private fun obtainMockedActiveNetwork(connectivityManager: ConnectivityManager): Network { + val mockedNetwork: Network = mock(Network::class.java) + doReturn(mockedNetwork).`when`(connectivityManager).activeNetwork + return mockedNetwork + } +} diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ProvidersTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ProvidersTest.kt index a9ded3e3..be94918e 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ProvidersTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ProvidersTest.kt @@ -7,120 +7,22 @@ package io.bitdrift.capture -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.test.core.app.ApplicationProvider -import io.bitdrift.capture.attributes.ClientAttributes -import io.bitdrift.capture.attributes.DeviceAttributes -import io.bitdrift.capture.attributes.NetworkAttributes import io.bitdrift.capture.providers.SystemDateProvider import org.assertj.core.api.Assertions.assertThat -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -@Suppress("DEPRECATION") @RunWith(RobolectricTestRunner::class) -@Config(sdk = [21]) +@Config(sdk = [23]) class ProvidersTest { + @Test fun system_date_provider() { - // ACT - just exercise the codepath to make sure we're not using invalid APIs for version 23 - val provider = SystemDateProvider() - val date = provider() + // Just exercise the codepath to make sure we're not using invalid APIs for version 23 + val date = SystemDateProvider().invoke() - // ASSERT assertThat(date).isNotNull } - - @Test - fun client_attributes() { - // ARRANGE - val packageName = "my.bitdrift.test" - val versionName = "1.2.3.4" - val versionCode = 66 - - val currentState = Lifecycle.State.STARTED - val lifecycle = mock(Lifecycle::class.java) - doReturn(currentState).`when`(lifecycle).currentState - val lifecycleOwner = mock(LifecycleOwner::class.java) - doReturn(lifecycle).`when`(lifecycleOwner).lifecycle - - val context = spy(ApplicationProvider.getApplicationContext()) - doReturn(packageName).`when`(context).packageName - - val packageManager: PackageManager = mock(PackageManager::class.java) - doReturn(packageManager).`when`(context).packageManager - - val packageInfo: PackageInfo = mock(PackageInfo::class.java) - doReturn(packageInfo).`when`(packageManager).getPackageInfo(packageName, 0) - packageInfo.versionName = versionName - packageInfo.versionCode = versionCode - - // ACT - val clientAttributes = ClientAttributes(context, lifecycleOwner).invoke() - - // ASSERT - assertThat(clientAttributes).containsEntry("app_version", versionName) - assertThat(clientAttributes).containsEntry("_app_version_code", versionCode.toString()) - assertThat(clientAttributes).containsEntry("app_id", packageName) - assertThat(clientAttributes).containsEntry("foreground", "1") - } - - @Test - fun client_attributes_not_foreground() { - // ARRANGE - val currentState = Lifecycle.State.CREATED - val lifecycle = mock(Lifecycle::class.java) - doReturn(currentState).`when`(lifecycle).currentState - val lifecycleOwner = mock(LifecycleOwner::class.java) - doReturn(lifecycle).`when`(lifecycleOwner).lifecycle - - val context = spy(ApplicationProvider.getApplicationContext()) - - val packageManager: PackageManager = mock(PackageManager::class.java) - doReturn(packageManager).`when`(context).packageManager - - val packageInfo: PackageInfo = mock(PackageInfo::class.java) - doReturn(packageInfo).`when`(packageManager).getPackageInfo("my.bitdrift.test", 0) - - // ACT - val clientAttributes = ClientAttributes(context, lifecycleOwner).invoke() - - // ASSERT - assertThat(clientAttributes).containsEntry("foreground", "0") - } - - @Test - @Ignore("Robolectric throwing ClassNotFoundException: android.telephony.TelephonyCallback") - @Suppress("ForbiddenComment") - fun network_attributes() { - // TODO: Fix test - // ARRANGE - val context = spy(ApplicationProvider.getApplicationContext()) - - // ACT - val networkAttributes = NetworkAttributes(context).invoke() - - // ASSERT - assertThat(networkAttributes).containsEntry("carrier", "tbd") - } - - @Test - fun device_attributes() { - // ACT - val deviceAttributes = DeviceAttributes(ApplicationProvider.getApplicationContext()).invoke() - - // ASSERT - assertThat(deviceAttributes).containsEntry("model", "robolectric") - assertThat(deviceAttributes).containsEntry("_locale", "en_US") - } } diff --git a/platform/jvm/gradle/libs.versions.toml b/platform/jvm/gradle/libs.versions.toml index bc9a1988..5b0e01bd 100644 --- a/platform/jvm/gradle/libs.versions.toml +++ b/platform/jvm/gradle/libs.versions.toml @@ -19,7 +19,7 @@ mockitoCore = "4.9.0" mockitoKotlin = "2.2.0" mockitoKotlinVersion = "4.1.0" okhttp = "4.12.0" -robolectric = "4.9" +robolectric = "4.13" rustAndroidPlugin = "0.9.3" material3Android = "1.2.1" startupRuntime = "1.1.1" From b3ac74c4cd4cd66ef0fc9d1469a5740447ee411c Mon Sep 17 00:00:00 2001 From: pablo-guardiola Date: Thu, 5 Sep 2024 11:16:13 -0400 Subject: [PATCH 2/3] bump robolectric bazel dependency version to 4.13 --- WORKSPACE | 6 +++--- bazel/capture_dependencies.bzl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 104efabf..91be9d6e 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -275,9 +275,9 @@ http_archive( http_archive( name = "robolectric", - sha256 = "ba1269064f5509531b024cdea70349e97756f0f639e53b7cbb0938127218d6f8", - strip_prefix = "robolectric-bazel-4.11", - urls = ["https://github.com/robolectric/robolectric-bazel/archive/4.11.tar.gz"], + sha256 = "a270fd6fd83f9f024623e787696e6b73c44664b7c95f3d937ed35bf0a94a67ae", + strip_prefix = "robolectric-bazel-4.13", + urls = ["https://github.com/robolectric/robolectric-bazel/releases/download/4.13/robolectric-bazel-4.13.tar.gz"], ) load("@robolectric//bazel:robolectric.bzl", "robolectric_repositories") diff --git a/bazel/capture_dependencies.bzl b/bazel/capture_dependencies.bzl index c1b0444e..fc3f54a2 100644 --- a/bazel/capture_dependencies.bzl +++ b/bazel/capture_dependencies.bzl @@ -18,7 +18,7 @@ def jvm_dependencies(): "org.mockito:mockito-inline:4.11.0", "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0", "androidx.test:core:1.6.0", - "org.robolectric:robolectric:4.11", + "org.robolectric:robolectric:4.13", "org.assertj:assertj-core:3.22.0", "com.squareup.okhttp3:mockwebserver:{}".format(okhttp_version), "junit:junit:4.13.2", From f43625b441f4c89de5f4eda9902f44127bf27789 Mon Sep 17 00:00:00 2001 From: pablo-guardiola Date: Thu, 5 Sep 2024 22:27:06 -0400 Subject: [PATCH 3/3] implement "fail-closed" mode --- .github/workflows/android.yaml | 50 ++++++++++++++++++++++++++++++---- ci/check_bazel.sh | 41 ++++++++++++++++++++-------- ci/files_changed.sh | 13 ++++++++- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index b0046814..4ed1615d 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -21,11 +21,49 @@ jobs: with: fetch-depth: 0 - - name: check for relevant changes - id: check_changes + - name: Check for Bazel build file changes + id: bazel_check + run: ./ci/check_bazel.sh //examples/android:android_app + continue-on-error: true + + - name: Check for workflow file changes + id: workflow_check + run: ./ci/files_changed.sh .github/workflows/android.yaml + continue-on-error: true + + - name: Check for relevant Gradle changes + id: gradle_check + run: ./ci/files_changed.sh "^platform/jvm/gradle-test-app/.*\.(gradle|kts|kt|xml)$" + continue-on-error: true + + - name: Determine if tests should run + id: check_changes_separate run: | - ./ci/check_bazel.sh //examples/android:android_app || ./ci/files_changed.sh .github/workflows/android.yaml || ./ci/files_changed.sh "^platform/jvm/gradle-test-app/.*\.(gradle|kts|kt|xml)$" && ./ci/run_tests.sh - true + bazel_status="${{ steps.bazel_check.outputs.check_result }}" + workflow_status="${{ steps.workflow_check.outputs.check_result }}" + gradle_status="${{ steps.gradle_check.outputs.check_result }}" + + # Check if any status indicates a relevant change or error + if [[ "$bazel_status" == "1" || "$workflow_status" == "1" || "$gradle_status" == "1" ]]; then + echo "An unexpected issue occurred during checks." + exit 1 + elif [[ "$bazel_status" == "0" || "$workflow_status" == "0" || "$gradle_status" == "0" ]]; then + echo "Changes detected in one or more checks. Running tests." + echo "run_tests=true" >> $GITHUB_ENV + elif [[ "$bazel_status" == "2" && "$workflow_status" == "2" && "$gradle_status" == "2" ]]; then + echo "No relevant changes found." + echo "run_tests=false" >> $GITHUB_ENV + else + echo "Unknown issue." + exit 1 + fi + shell: bash + + - name: Run downstream tests if changes are detected + id: check_changes + if: env.run_tests == 'true' + run: ./ci/run_tests.sh + build_apk: runs-on: ubuntu-latest if: needs.pre_check.outputs.should_run == 'true' @@ -178,7 +216,7 @@ jobs: # job completing, we are able to gate it on all the previous jobs without explicitly enumerating them. verify_android: runs-on: ubuntu-latest - needs: ["build_apk", "verify_android_hello_world_per_version", "gradle_tests"] + needs: ["pre_check", "build_apk", "verify_android_hello_world_per_version", "gradle_tests"] if: always() steps: # Checkout repo to Github Actions runner @@ -187,4 +225,4 @@ jobs: with: fetch-depth: 1 - name: check result - run: ./ci/check_result.sh ${{ needs.build_apk.result }} && ./ci/check_result.sh ${{ needs.verify_android_hello_world_per_version.result }} && ./ci/check_result.sh ${{ needs.gradle_tests.result }} + run: ./ci/check_result.sh ${{ needs.pre_check.result }} && ./ci/check_result.sh ${{ needs.build_apk.result }} && ./ci/check_result.sh ${{ needs.verify_android_hello_world_per_version.result }} && ./ci/check_result.sh ${{ needs.gradle_tests.result }} diff --git a/ci/check_bazel.sh b/ci/check_bazel.sh index e7aad034..f11fc47d 100755 --- a/ci/check_bazel.sh +++ b/ci/check_bazel.sh @@ -2,21 +2,30 @@ set -euo pipefail -# Compares $GITHUB_HEAD_REF and $GITHUB_BASE_REF (PR branch + target branch, usually main) to -# determine which Bazel targets have changed. This is done by analysizing the cache keys and -# should be authoritive assuming the builds are hermietic. +# Compares the head ref and $GITHUB_BASE_REF (PR branch + target branch, usually main) to +# determine which Bazel targets have changed. This is done by analyzing the cache keys and +# should be authoritative assuming the builds are hermetic. # # Usage ./ci/check_bazel.sh +# Trap to handle unexpected errors and log them +trap 'echo "An unexpected error occurred during Bazel check."; echo "check_result=1" >> "$GITHUB_OUTPUT"; exit 1' ERR + +# Ensure we fetch the base branch (main) to make it available +git fetch origin "$GITHUB_BASE_REF":"$GITHUB_BASE_REF" + +# Get the latest commit SHA for the base branch (target branch of the PR) +base_sha=$(git rev-parse "$GITHUB_BASE_REF") +# Get the latest commit SHA for the PR branch (the head ref in the forked repository) +final_revision=$GITHUB_SHA + +# Use git merge-base to find the common ancestor of the two commits +previous_revision=$(git merge-base "$base_sha" "$final_revision") + # Path to your Bazel WORKSPACE directory workspace_path=$(pwd) # Path to your Bazel executable bazel_path=$(pwd)/bazelw -# Starting Revision SHA. We use the merge-base to better handle the case where HEAD is not ahead of main. -base_sha=$(git rev-parse "origin/$GITHUB_BASE_REF") -previous_revision=$(git merge-base "$base_sha" "origin/$GITHUB_HEAD_REF") -# Final Revision SHA -final_revision=$GITHUB_HEAD_REF starting_hashes_json="/tmp/starting_hashes.json" final_hashes_json="/tmp/final_hashes.json" @@ -53,14 +62,22 @@ pattern_impacted() { grep -q "$1" /tmp/impacted_targets.txt } +changes_detected=false + for pattern in "$@" do if pattern_impacted "$pattern"; then echo "$pattern changed!" - exit 0 + changes_detected=true + break fi done -# No relevant changes detected via Bazel. -echo "Nothing changed" -exit 1 +# Exit code based on whether changes were detected +if [ "$changes_detected" = true ]; then + echo "check_result=0" >> "$GITHUB_OUTPUT" + exit 0 # Changes found +else + echo "No changes detected." + echo "check_result=2" >> "$GITHUB_OUTPUT" +fi diff --git a/ci/files_changed.sh b/ci/files_changed.sh index edbd69a0..6b1e5e4e 100755 --- a/ci/files_changed.sh +++ b/ci/files_changed.sh @@ -7,4 +7,15 @@ set -e -git rev-parse --abbrev-ref HEAD | grep -q ^main$ || git diff --name-only "origin/$GITHUB_BASE_REF" | grep -E "$1" +# Trap to handle unexpected errors and log them +trap 'echo "An unexpected error occurred during file change check."; echo "check_result=1" >> "$GITHUB_OUTPUT"; exit 1' ERR + +# Check for file changes +if git rev-parse --abbrev-ref HEAD | grep -q ^main$ || git diff --name-only "origin/$GITHUB_BASE_REF" | grep -E "$1" ; then + echo "Relevant file changes detected!" + echo "check_result=0" >> "$GITHUB_OUTPUT" + exit 0 # Relevant changes detected +else + echo "No relevant changes found." + echo "check_result=2" >> "$GITHUB_OUTPUT" +fi