diff --git a/.taskcluster.yml b/.taskcluster.yml index bbdee725c..7fe7e771a 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -43,6 +43,7 @@ tasks: && rm -rf gvr-android-sdk && git clone https://github.com/MozillaReality/FirefoxReality-gvr-android-sdk.git gvr-android-sdk && git submodule update && ./gradlew --no-daemon --console=plain clean `python tools/taskcluster/build_targets.py =all+googlevr+noapi` + && ./gradlew app:testNoapiArm64DebugUnitTest metadata: name: Firefox Reality for Android - Build - Pull Request description: Building Firefox Reality for Android (via Gradle) - triggered by a pull request. diff --git a/app/build.gradle b/app/build.gradle index 538002c84..b010f0df9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -414,6 +414,10 @@ android { ] } } + + testOptions { + unitTests.includeAndroidResources = true + } } configurations { @@ -460,6 +464,7 @@ dependencies { implementation deps.android_components.support_rustlog implementation deps.android_components.support_rusthttp implementation deps.android_components.glean + implementation deps.app_services.rustlog // For production builds, the native code for all `org.mozilla.appservices` // dependencies gets compiled together into a single "megazord" build, and @@ -467,14 +472,10 @@ dependencies { // https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html // For now we can jut use the one that's specifically designed for Fenix. implementation deps.app_services.megazord - testImplementation deps.app_services.megazord_forUnitTests modules { module('org.mozilla.appservices:full-megazord') { replacedBy('org.mozilla.appservices:fenix-megazord', 'prefer the fenix megazord, to reduce final application size') } - module('org.mozilla.appservices:fenix-megazord') { - replacedBy('org.mozilla.appservices:fenix-megazord-forUnitTests', 'prefer the forUnitTests variant if present') - } } // TODO this should not be necessary at all, see Services.kt @@ -495,9 +496,16 @@ dependencies { implementation deps.disklrucache.disklrucache // Testing - testImplementation deps.junit androidTestImplementation deps.atsl.runner androidTestImplementation deps.espresso.core + testImplementation deps.junit + testImplementation deps.atsl.core + testImplementation deps.robolectric + testImplementation deps.app_services.megazord_forUnitTests + testImplementation deps.app_services.rustlog + testImplementation deps.android_components.support_test + testImplementation deps.telemetry.glean_unittests + testImplementation deps.work.testing // Daydream googlevrImplementation deps.google_vr.sdk_base diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt index 604eac7e6..3d1cadf04 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt @@ -229,7 +229,12 @@ class Accounts constructor(val context: Context) { private fun loadDefaultProfilePicture(): BitmapDrawable? { BitmapFactory.decodeResource(context.resources, R.drawable.ic_icon_settings_account)?.let { - BitmapCache.getInstance(context).addBitmap(PROFILE_PICTURE_TAG, it) + try { + BitmapCache.getInstance(context).addBitmap(PROFILE_PICTURE_TAG, it) + } catch (e: NullPointerException) { + Log.w(LOGTAG, "Bitmap is a null pointer.") + return null + } profilePicture = BitmapDrawable(context.resources, ViewUtils.getRoundedCroppedBitmap(it)) } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt index e8eb14110..c8c9ec72e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.appservices.Megazord +import mozilla.appservices.rustlog.LogAdapterCannotEnable import mozilla.components.concept.sync.* import mozilla.components.service.fxa.* import mozilla.components.service.fxa.manager.FxaAccountManager @@ -29,8 +30,6 @@ import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoSession import org.mozilla.vrbrowser.R import org.mozilla.vrbrowser.browser.engine.EngineProvider -import org.mozilla.vrbrowser.browser.engine.GeckoViewFetchClient -import org.mozilla.vrbrowser.browser.engine.SessionStore import org.mozilla.vrbrowser.utils.SystemUtils import org.mozilla.vrbrowser.telemetry.GleanMetricsService @@ -52,7 +51,11 @@ class Services(val context: Context, places: Places): GeckoSession.NavigationDel // This makes bookmarks storage accessible to background sync workers. init { Megazord.init() - RustLog.enable() + try { + RustLog.enable() + } catch (e: LogAdapterCannotEnable) { + android.util.Log.w(LOGTAG, "RustLog has been enabled.") + } RustHttpConfig.setClient(lazy { EngineProvider.createClient(context) }) // Make sure we get logs out of our android-components. diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java index 8e2a361b1..6e065a49c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.GleanMetrics.Distribution; @@ -159,6 +160,11 @@ private static void setStartupMetrics() { Distribution.INSTANCE.getChannelName().set(DeviceType.isOculusBuild() ? "oculusvr" : BuildConfig.FLAVOR_platform); } + @VisibleForTesting + public static void testSetStartupMetrics() { + setStartupMetrics(); + } + public static class FxA { public static void signIn() { diff --git a/app/src/test/java/org/mozilla/vrbrowser/GleanMetricsServiceTest.kt b/app/src/test/java/org/mozilla/vrbrowser/GleanMetricsServiceTest.kt new file mode 100644 index 000000000..f48ee0394 --- /dev/null +++ b/app/src/test/java/org/mozilla/vrbrowser/GleanMetricsServiceTest.kt @@ -0,0 +1,135 @@ +package org.mozilla.vrbrowser + +import androidx.test.core.app.ApplicationProvider +import mozilla.components.concept.sync.DeviceType +import mozilla.components.service.glean.testing.GleanTestRule +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.vrbrowser.GleanMetrics.* +import org.mozilla.vrbrowser.telemetry.GleanMetricsService +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class GleanMetricsServiceTest { + + @get:Rule + val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) + + @Test + fun testURLTelemetry() { + assertFalse(Url.domains.testHasValue()) + assertFalse(Url.visits.testHasValue()) + GleanMetricsService.stopPageLoadTimeWithURI("https://www.example.com/test.html"); + assertTrue(Url.domains.testHasValue()) + assertEquals(Url.domains.testGetValue(), 1) + + assertTrue(Url.visits.testHasValue()) + assertEquals(Url.visits.testGetValue(), 1) + + assertFalse(Url.queryType["type_link"].testHasValue()) + assertFalse(Url.queryType["type_query"].testHasValue()) + GleanMetricsService.urlBarEvent(true) + assertTrue(Url.queryType["type_link"].testHasValue()) + assertEquals(Url.queryType["type_link"].testGetValue(), 1) + GleanMetricsService.urlBarEvent(false) + assertTrue(Url.queryType["type_query"].testHasValue()) + assertEquals(Url.queryType["type_query"].testGetValue(), 1) + + assertFalse(Url.queryType["voice_query"].testHasValue()) + GleanMetricsService.voiceInputEvent() + assertTrue(Url.queryType["voice_query"].testHasValue()) + assertEquals(Url.queryType["voice_query"].testGetValue(), 1) + } + + @Test + fun testDistributionChannelTelemetry() { + assertFalse(Distribution.channelName.testHasValue()) + GleanMetricsService.testSetStartupMetrics() + assertTrue(Distribution.channelName.testHasValue()) + assertEquals(Distribution.channelName.testGetValue(), BuildConfig.FLAVOR_platform) + } + + @Test + fun testFxAAccountTelemetry() { + assertFalse(FirefoxAccount.signIn.testHasValue()) + GleanMetricsService.FxA.signIn() + assertTrue(FirefoxAccount.signIn.testHasValue()) + var events = FirefoxAccount.signIn.testGetValue() + assertEquals(events.size, 1) + + assertFalse(FirefoxAccount.signInResult.testHasValue()) + GleanMetricsService.FxA.signInResult(false) + assertTrue(FirefoxAccount.signInResult.testHasValue()) + events = FirefoxAccount.signInResult.testGetValue() + assertEquals(events.size, 1) + // We only expect 1 extra key. + assertEquals(events[0].extra!!.size, 1) + assertEquals(events[0].extra!!["state"], "false") + + GleanMetricsService.FxA.signInResult(true) + events = FirefoxAccount.signInResult.testGetValue() + assertEquals(events.size, 2) + // We only expect 1 extra key. + assertEquals(events[1].extra!!.size, 1) + assertEquals(events[1].extra!!["state"], "true") + + assertFalse(FirefoxAccount.signOut.testHasValue()) + GleanMetricsService.FxA.signOut() + assertTrue(FirefoxAccount.signOut.testHasValue()) + events = FirefoxAccount.signOut.testGetValue() + assertEquals(events.size, 1) + } + + @Test + fun testFxABookmarkTelemetry() { + assertFalse(FirefoxAccount.bookmarksSyncStatus.testHasValue()) + GleanMetricsService.FxA.bookmarksSyncStatus(false) + assertTrue(FirefoxAccount.bookmarksSyncStatus.testHasValue()) + assertEquals(FirefoxAccount.bookmarksSyncStatus.testGetValue(), false) + + GleanMetricsService.FxA.bookmarksSyncStatus(true) + assertEquals(FirefoxAccount.bookmarksSyncStatus.testGetValue(), true) + } + + @Test + fun testFxAHistoryTelemetry() { + assertFalse(FirefoxAccount.historySyncStatus.testHasValue()) + GleanMetricsService.FxA.historySyncStatus(false) + assertTrue(FirefoxAccount.historySyncStatus.testHasValue()) + assertEquals(FirefoxAccount.historySyncStatus.testGetValue(), false) + + GleanMetricsService.FxA.historySyncStatus(true) + assertEquals(FirefoxAccount.historySyncStatus.testGetValue(), true) + } + + @Test + fun testFxATabTelemetry() { + assertFalse(FirefoxAccount.tabSent.testHasValue()) + GleanMetricsService.FxA.sentTab() + assertTrue(FirefoxAccount.tabSent.testHasValue()) + assertEquals(FirefoxAccount.tabSent.testGetValue(), 1) + + assertFalse(FirefoxAccount.receivedTab[DeviceType.MOBILE.name].testHasValue()) + GleanMetricsService.FxA.receivedTab(DeviceType.MOBILE) + assertTrue(FirefoxAccount.receivedTab[DeviceType.MOBILE.name].testHasValue()) + assertEquals(FirefoxAccount.receivedTab[DeviceType.MOBILE.name].testGetValue(), 1) + } + + @Test + fun testTabTelemetry() { + assertFalse(Tabs.opened[GleanMetricsService.Tabs.TabSource.BOOKMARKS.name].testHasValue()) + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.BOOKMARKS) + assertTrue(Tabs.opened[GleanMetricsService.Tabs.TabSource.BOOKMARKS.name].testHasValue()) + assertEquals(Tabs.opened[GleanMetricsService.Tabs.TabSource.BOOKMARKS.name].testGetValue(), 1) + + assertFalse(Tabs.activated.testHasValue()) + GleanMetricsService.Tabs.activatedEvent() + assertTrue(Tabs.activated.testHasValue()) + assertEquals(Tabs.activated.testGetValue(), 1) + } +} diff --git a/versions.gradle b/versions.gradle index 902d8fb82..a70b00e74 100644 --- a/versions.gradle +++ b/versions.gradle @@ -42,6 +42,7 @@ versions.support = "1.0.0" versions.recyclerview = "1.1.0" versions.constraint_layout = "2.0.0-alpha2" versions.junit = "4.12" +versions.atsl_core = "1.2.0" versions.atsl_runner = "1.1.0-alpha4" versions.atsl_rules = "1.1.0-alpha4" versions.espresso = "3.1.0-alpha4" @@ -50,7 +51,11 @@ versions.kotlin = "1.3.31" versions.kotlin_coroutines = "1.2.1" versions.snakeyaml = "1.24" versions.gson = "2.8.5" +versions.robolectric = "4.2.1" +versions.work = "2.2.0" +versions.telemetry = "22.0.0" ext.versions = versions + def deps = [:] def gecko_view = [:] @@ -72,11 +77,13 @@ android_components.concept_fetch = "org.mozilla.components:concept-fetch:$versio android_components.lib_fetch = "org.mozilla.components:lib-fetch-httpurlconnection:$versions.android_components" android_components.support_rustlog = "org.mozilla.components:support-rustlog:$versions.android_components" android_components.support_rusthttp = "org.mozilla.components:support-rusthttp:$versions.android_components" +android_components.support_test = "org.mozilla.components:support-test:$versions.android_components" deps.android_components = android_components def app_services = [:] app_services.megazord = "org.mozilla.appservices:fenix-megazord:${versions.mozilla_appservices}" app_services.megazord_forUnitTests = "org.mozilla.appservices:fenix-megazord-forUnitTests:${versions.mozilla_appservices}" +app_services.rustlog = "org.mozilla.appservices:rustlog:${versions.mozilla_appservices}" deps.app_services = app_services deps.mozilla_speech = "com.github.mozilla:mozillaspeechlibrary:$versions.mozilla_speech" @@ -102,6 +109,7 @@ deps.support = support // TODO this should not be necessary at all, see Services.kt def work = [:] work.runtime = "androidx.work:work-runtime-ktx:$versions.work" +work.testing = "androidx.work:work-testing:$versions.work" deps.work = work def room = [:] @@ -124,6 +132,7 @@ espresso.intents = "androidx.test.espresso:espresso-intents:$versions.espresso" deps.espresso = espresso def atsl = [:] +atsl.core = "androidx.test:core-ktx:$versions.atsl_core" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_runner" deps.atsl = atsl @@ -145,6 +154,10 @@ kotlin.coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.kot kotlin.coroutines_jdk8 = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$versions.kotlin_coroutines" deps.kotlin = kotlin +def telemetry = [:] +telemetry.glean_unittests = "org.mozilla.telemetry:glean-forUnitTests:$versions.telemetry" +deps.telemetry = telemetry + deps.constraint_layout = "androidx.constraintlayout:constraintlayout:$versions.constraint_layout" deps.junit = "junit:junit:$versions.junit" @@ -157,6 +170,8 @@ deps.snakeyaml = "org.yaml:snakeyaml:$versions.snakeyaml:android" deps.gson = "com.google.code.gson:gson:$versions.gson" +deps.robolectric = "org.robolectric:robolectric:$versions.robolectric" + ext.deps = deps def build_versions = [:]