Import test coverage from Glean-legacy for Counters and Strings

This commit is contained in:
Alessio Placitelli 2019-05-10 10:40:39 +02:00
Родитель 139fdd72ef
Коммит 202ee528a2
7 изменённых файлов: 543 добавлений и 3 удалений

Просмотреть файл

@ -10,8 +10,17 @@ apply plugin: 'com.android.library'
apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
import org.apache.tools.ant.taskdefs.condition.Os
/*
* This defines the location of the JSON schema used to validate the pings
* created during unit testing.
* This uses a specific version of the schema identified by a git commit hash.
*/
String GLEAN_PING_SCHEMA_GIT_HASH = "64b852c"
String GLEAN_PING_SCHEMA_URL = "https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas/$GLEAN_PING_SCHEMA_GIT_HASH/schemas/glean/baseline/baseline.1.schema.json"
android {
compileSdkVersion 27
@ -19,6 +28,8 @@ android {
minSdkVersion rootProject.ext.build['minSdkVersion']
targetSdkVersion rootProject.ext.build['targetSdkVersion']
buildConfigField("String", "GLEAN_PING_SCHEMA_URL", "\"" + GLEAN_PING_SCHEMA_URL + "\"")
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
@ -151,8 +162,8 @@ dependencies {
// avoiding other configurations from being resolved. Tricky!
testImplementation files(configurations.jnaForTest.copyRecursive().files)
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.1'
testImplementation 'org.mockito:mockito-core:2.21.0'
testImplementation 'org.robolectric:robolectric:4.2.1'
testImplementation 'org.mockito:mockito-core:2.24.5'
testImplementation 'androidx.test:core-ktx:1.1.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'

Просмотреть файл

@ -111,6 +111,20 @@ open class GleanInternalAPI internal constructor () {
private fun sendPing(pingName: String) {
LibGleanFFI.INSTANCE.glean_send_ping(handle, pingName)
}
/**
* Test-only method to destroy the owned glean-core handle.
*/
internal fun testDestroyGleanHandle() {
if (!isInitialized()) {
// We don't need to destroy the Glean handle: it wasn't initialized.
return
}
val e = RustError.ByReference()
LibGleanFFI.INSTANCE.glean_destroy_glean(handle, e)
handle = 0L
}
}
object Glean : GleanInternalAPI()

Просмотреть файл

@ -102,6 +102,9 @@ class CounterMetricType(
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun testGetValue(pingName: String = sendInPings.first()): Int {
// FIXME(#19): glean-core should give us an int to begin with
if (!testHasValue(pingName)) {
throw NullPointerException()
}
return LibGleanFFI.INSTANCE.glean_counter_test_get_value(Glean.handle, this.handle, pingName).toInt()
}
}

Просмотреть файл

@ -0,0 +1,246 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.telemetry.glean
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.test.core.app.ApplicationProvider
/*import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.testing.WorkManagerTestInitHelper
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Headers
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.firstrun.FileFirstRunDetector
import mozilla.components.service.glean.ping.PingMaker
import mozilla.components.service.glean.private.PingType
import mozilla.components.service.glean.scheduler.PingUploadWorker
import mozilla.components.service.glean.storages.ExperimentsStorageEngine
import mozilla.components.service.glean.storages.StorageEngineManager*/
import org.json.JSONObject
import org.junit.Assert
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mozilla.glean_rs.BuildConfig
import java.io.File
import java.util.concurrent.ExecutionException
/**
* Checks ping content against the Glean ping schema.
*
* This uses the Python utility, glean_parser, to perform the actual checking.
* This is installed in its own Miniconda environment as part of the build
* configuration in sdk_generator.gradle.
*
* @param content The JSON content of the ping
* @throws AssertionError If the JSON content is not valid
*/
internal fun checkPingSchema(content: JSONObject) {
val os = System.getProperty("os.name")?.toLowerCase()
val pythonExecutable =
if (os?.indexOf("win")?.compareTo(0) == 0)
"${BuildConfig.GLEAN_MINICONDA_DIR}/python"
else
"${BuildConfig.GLEAN_MINICONDA_DIR}/bin/python"
val proc = ProcessBuilder(
listOf(
pythonExecutable,
"-m",
"glean_parser",
"check",
"-s",
"${BuildConfig.GLEAN_PING_SCHEMA_URL}"
)
).redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT)
val process = proc.start()
val jsonString = content.toString()
with(process.outputStream.bufferedWriter()) {
write(jsonString)
newLine()
flush()
close()
}
val exitCode = process.waitFor()
assert(exitCode == 0)
}
/**
* Checks ping content against the Glean ping schema.
*
* This uses the Python utility, glean_parser, to perform the actual checking.
* This is installed in its own Miniconda environment as part of the build
* configuration in sdk_generator.gradle.
*
* @param content The JSON content of the ping
* @return the content string, parsed into a JSONObject
* @throws AssertionError If the JSON content is not valid
*/
internal fun checkPingSchema(content: String): JSONObject {
val jsonContent = JSONObject(content)
checkPingSchema(jsonContent)
return jsonContent
}
/**
* Collects a specified ping type and checks it against the Glean ping schema.
*
* @param ping The ping to check
* @return the ping contents, in a JSONObject
* @throws AssertionError If the JSON content is not valid
*/
/*internal fun collectAndCheckPingSchema(ping: PingType): JSONObject {
val appContext = ApplicationProvider.getApplicationContext<Context>()
val jsonString = PingMaker(
StorageEngineManager(applicationContext = appContext),
appContext
).collect(ping)!!
return checkPingSchema(jsonString)
}*/
/**
* Resets the Glean state and trigger init again.
*
* @param context the application context to init Glean with
* @param config the [Configuration] to init Glean with
* @param clearStores if true, clear the contents of all stores
*/
internal fun resetGlean(
context: Context = ApplicationProvider.getApplicationContext(),
//config: Configuration = Configuration(),
clearStores: Boolean = true
) {
//Glean.enableTestingMode()
// We're using the WorkManager in a bunch of places, and Glean will crash
// in tests without this line. Let's simply put it here.
//WorkManagerTestInitHelper.initializeTestWorkManager(context)
/*if (clearStores) {
// Clear all the stored data.
val storageManager = StorageEngineManager(applicationContext = context)
storageManager.clearAllStores()
// The experiments storage engine needs to be cleared manually as it's not listed
// in the `StorageEngineManager`.
ExperimentsStorageEngine.clearAllStores()
}*/
// Clear the "first run" flag.
//val firstRun = FileFirstRunDetector(File(context.applicationInfo.dataDir, Glean.GLEAN_DATA_DIR))
//firstRun.reset()
// Init Glean.
//Glean.setUploadEnabled(true)
Glean.testDestroyGleanHandle()
Glean.initialize(context)//, config)
}
/**
* Get a context that contains [PackageInfo.versionName] mocked to
* "glean.version.name".
*
* @return an application [Context] that can be used to init Glean
*/
internal fun getContextWithMockedInfo(): Context {
val context = Mockito.spy<Context>(ApplicationProvider.getApplicationContext<Context>())
val packageInfo = Mockito.mock(PackageInfo::class.java)
packageInfo.versionName = "glean.version.name"
val packageManager = Mockito.mock(PackageManager::class.java)
Mockito.`when`(packageManager.getPackageInfo(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenReturn(packageInfo)
Mockito.`when`(context.packageManager).thenReturn(packageManager)
return context
}
/**
* Helper function to check to see if a worker has been scheduled with the [WorkManager]
*
* @param tag a string representing the worker tag
* @return True if the task found in [WorkManager], false otherwise
*/
/*internal fun isWorkScheduled(tag: String): Boolean {
val instance = WorkManager.getInstance()
val statuses = instance.getWorkInfosByTag(tag)
try {
val workInfoList = statuses.get()
for (workInfo in workInfoList) {
val state = workInfo.state
if ((state === WorkInfo.State.RUNNING) || (state === WorkInfo.State.ENQUEUED)) {
return true
}
}
} catch (e: ExecutionException) {
// Do nothing but will return false
} catch (e: InterruptedException) {
// Do nothing but will return false
}
return false
}*/
/**
* Wait for a specifically tagged [WorkManager]'s Worker to be enqueued.
*
* @param workTag the tag of the expected Worker
* @param timeoutMillis how log before stopping the wait. This defaults to 5000ms (5 seconds).
*/
/*internal fun waitForEnqueuedWorker(workTag: String, timeoutMillis: Long = 5000) = runBlocking {
runBlocking {
withTimeout(timeoutMillis) {
do {
if (isWorkScheduled(workTag)) {
return@withTimeout
}
} while (true)
}
}
}*/
/**
* Helper function to simulate WorkManager being triggered since there appears to be a bug in
* the current WorkManager test utilites that prevent it from being triggered by a test. Once this
* is fixed, the contents of this can be amended to trigger WorkManager directly.
*/
/*internal fun triggerWorkManager() {
// Check that the work is scheduled
Assert.assertTrue("A scheduled PingUploadWorker must exist",
isWorkScheduled(PingUploadWorker.PING_WORKER_TAG))
// Since WorkManager does not properly run in tests, simulate the work being done
// We also assertTrue here to ensure that uploadPings() was successful
Assert.assertTrue("Upload Pings must return true", PingUploadWorker.uploadPings())
}*/
/**
* This is a helper class to facilitate testing of ping tagging
*/
/*internal class TestPingTagClient(
private val responseUrl: String = Configuration.DEFAULT_DEBUGVIEW_ENDPOINT,
private val responseStatus: Int = 200,
private val responseHeaders: Headers = MutableHeaders(),
private val responseBody: Response.Body = Response.Body.empty(),
private val debugHeaderValue: String? = null
) : Client() {
override fun fetch(request: Request): Response {
Assert.assertTrue("URL must be redirected for tagged pings",
request.url.startsWith(responseUrl))
Assert.assertEquals("Debug headers must match what the ping tag was set to",
debugHeaderValue, request.headers!!["X-Debug-ID"])
// Have to return a response here.
return Response(
responseUrl,
responseStatus,
request.headers ?: responseHeaders,
responseBody)
}
}*/

Просмотреть файл

@ -0,0 +1,136 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.telemetry.glean.private
//import kotlinx.coroutines.ExperimentalCoroutinesApi
//import kotlinx.coroutines.ObsoleteCoroutinesApi
//import mozilla.components.service.glean.resetGlean
import mozilla.telemetry.glean.resetGlean
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.lang.NullPointerException
//@ObsoleteCoroutinesApi
//@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
class CounterMetricTypeTest {
@Before
fun setUp() {
resetGlean()
}
@Test
fun `The API saves to its storage engine`() {
// Define a 'counterMetric' counter metric, which will be stored in "store1"
val counterMetric = CounterMetricType(
disabled = false,
category = "telemetry",
lifetime = Lifetime.Application,
name = "counter_metric",
sendInPings = listOf("store1")
)
assertFalse(counterMetric.testHasValue())
// Add to the counter a couple of times with a little delay. The first call will check
// calling add() without parameters to test increment by 1.
counterMetric.add()
// Check that the count was incremented and properly recorded.
assertTrue(counterMetric.testHasValue())
assertEquals(1, counterMetric.testGetValue())
counterMetric.add(10)
// Check that count was incremented and properly recorded. This second call will check
// calling add() with 10 to test increment by other amount
assertTrue(counterMetric.testHasValue())
assertEquals(11, counterMetric.testGetValue())
}
@Ignore("Ignoring the test as 'disabled' is not passed through FFI")
@Test
fun `counters with no lifetime must not record data`() {
// Define a 'counterMetric' counter metric, which will be stored in "store1".
// It's disabled so it should not record anything.
val counterMetric = CounterMetricType(
disabled = true,
category = "telemetry",
lifetime = Lifetime.Ping,
name = "counter_metric",
sendInPings = listOf("store1")
)
// Attempt to increment the counter
counterMetric.add(1)
// Check that nothing was recorded.
assertFalse("Counters must not be recorded if they are disabled",
counterMetric.testHasValue())
}
@Ignore("Ignoring the test as 'disabled' is not passed through FFI")
@Test
fun `disabled counters must not record data`() {
// Define a 'counterMetric' counter metric, which will be stored in "store1". It's disabled
// so it should not record anything.
val counterMetric = CounterMetricType(
disabled = true,
category = "telemetry",
lifetime = Lifetime.Application,
name = "counter_metric",
sendInPings = listOf("store1")
)
// Attempt to store the counter.
counterMetric.add()
// Check that nothing was recorded.
assertFalse("Counters must not be recorded if they are disabled",
counterMetric.testHasValue())
}
@Test(expected = NullPointerException::class)
fun `testGetValue() throws NullPointerException if nothing is stored`() {
val counterMetric = CounterMetricType(
disabled = true,
category = "telemetry",
lifetime = Lifetime.Application,
name = "counter_metric",
sendInPings = listOf("store1")
)
counterMetric.testGetValue()
}
@Test
fun `The API saves to secondary pings`() {
// Define a 'counterMetric' counter metric, which will be stored in "store1" and "store2"
val counterMetric = CounterMetricType(
disabled = false,
category = "telemetry",
lifetime = Lifetime.Application,
name = "counter_metric",
sendInPings = listOf("store1", "store2")
)
// Add to the counter a couple of times with a little delay. The first call will check
// calling add() without parameters to test increment by 1.
counterMetric.add()
// Check that the count was incremented and properly recorded for the second ping.
assertTrue(counterMetric.testHasValue("store2"))
assertEquals(1, counterMetric.testGetValue("store2"))
counterMetric.add(10)
// Check that count was incremented and properly recorded for the second ping.
// This second call will check calling add() with 10 to test increment by other amount
assertTrue(counterMetric.testHasValue("store2"))
assertEquals(11, counterMetric.testGetValue("store2"))
}
}

Просмотреть файл

@ -0,0 +1,130 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.telemetry.glean.private
// import kotlinx.coroutines.ExperimentalCoroutinesApi
// import kotlinx.coroutines.ObsoleteCoroutinesApi
import mozilla.telemetry.glean.resetGlean
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.lang.NullPointerException
// @ObsoleteCoroutinesApi
// @ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
class StringMetricTypeTest {
@Before
fun setUp() {
resetGlean()
}
@Ignore("Ignoring the test as the testing API for strings is not implemented")
@Test
fun `The API saves to its storage engine`() {
// Define a 'stringMetric' string metric, which will be stored in "store1"
val stringMetric = StringMetricType(
disabled = false,
category = "telemetry",
lifetime = Lifetime.Application,
name = "string_metric",
sendInPings = listOf("store1")
)
// Record two strings of the same type, with a little delay.
stringMetric.set("value")
// Check that data was properly recorded.
assertTrue(stringMetric.testHasValue())
assertEquals("value", stringMetric.testGetValue())
stringMetric.set("overriddenValue")
// Check that data was properly recorded.
assertTrue(stringMetric.testHasValue())
assertEquals("overriddenValue", stringMetric.testGetValue())
}
@Test
fun `strings with no lifetime must not record data`() {
// Define a 'stringMetric' string metric, which will be stored in
// "store1". It's disabled so it should not record anything.
val stringMetric = StringMetricType(
disabled = true,
category = "telemetry",
lifetime = Lifetime.Ping,
name = "stringMetric",
sendInPings = listOf("store1")
)
// Attempt to store the string.
stringMetric.set("value")
// Check that nothing was recorded.
assertFalse("Strings must not be recorded if they have no lifetime",
stringMetric.testHasValue())
}
@Test
fun `disabled strings must not record data`() {
// Define a 'stringMetric' string metric, which will be stored in "store1". It's disabled
// so it should not record anything.
val stringMetric = StringMetricType(
disabled = true,
category = "telemetry",
lifetime = Lifetime.Application,
name = "stringMetric",
sendInPings = listOf("store1")
)
// Attempt to store the string.
stringMetric.set("value")
// Check that nothing was recorded.
assertFalse("Strings must not be recorded if they are disabled",
stringMetric.testHasValue())
}
@Ignore("Ignoring the test as the testing API for strings is not implemented")
@Test(expected = NullPointerException::class)
fun `testGetValue() throws NullPointerException if nothing is stored`() {
val stringMetric = StringMetricType(
disabled = true,
category = "telemetry",
lifetime = Lifetime.Application,
name = "stringMetric",
sendInPings = listOf("store1")
)
stringMetric.testGetValue()
}
@Ignore("Ignoring the test as the testing API for strings is not implemented")
@Test
fun `The API saves to secondary pings`() {
// Define a 'stringMetric' string metric, which will be stored in "store1" and "store2"
val stringMetric = StringMetricType(
disabled = false,
category = "telemetry",
lifetime = Lifetime.Application,
name = "string_metric",
sendInPings = listOf("store1", "store2")
)
// Record two strings of the same type, with a little delay.
stringMetric.set("value")
// Check that data was properly recorded in the second ping.
assertTrue(stringMetric.testHasValue("store2"))
assertEquals("value", stringMetric.testGetValue("store2"))
stringMetric.set("overriddenValue")
// Check that data was properly recorded in the second ping.
assertTrue(stringMetric.testHasValue("store2"))
assertEquals("overriddenValue", stringMetric.testGetValue("store2"))
}
}

Просмотреть файл

@ -203,7 +203,7 @@ pub extern "C" fn glean_counter_test_has_value(
COUNTER_METRICS.call_with_output(&mut err, metric_id, |metric| {
metric
.test_get_value(glean, storage_name.as_str())
.is_none()
.is_some()
})
})
}