Skip to content
Permalink
Browse files

Issue #3: Implement client for loading (partial) experiment configura…

…tion from server

Issue #4: Implement storage for saving experiment configuration to disk

Closes #3: Implement client for loading (partial) experiment configuration from server
Closes #4: Implement storage for saving experiment configuration to disk

Remove duplicate classes and fix one test

Reformat code

Added test for HttpURLConnection-based http client

Check request http method in http client test

Added http client tests for 404 and 500 status codes

Make client, source and storage private inside Fretboard class

Renamed modules

Removed client-httpurlconnection module and moved into fretboard-client-kinto

Make Fretboard instantiable

Add license to AndroidManifest

Reformat code

Refactored Experiment class and added optional parameters

Fixed tests to the new experiment structure

Check status code in http client before processing

More idiomatic Kotlin in HttpURLConnectionHttpClient

JSON keys as constants

Add license header to ExperimentDownloadException class

Add method for loading experiments from disk and keep them in memory

Add JvmOverloads to improve Java interop

JvmOverloads for Bucket and Matcher classes

Refactor ExperimentSource mergeExperimentsFromDiff

Internal classes and removed JvmOverloads

Add tests for Fretboard class
  • Loading branch information...
fercarcedo committed May 24, 2018
1 parent 9fd3e53 commit 7996c0c11a40939cac73914b83fc2ec578927596
Showing with 1,172 additions and 4 deletions.
  1. +3 −0 build.gradle
  2. +1 −0 fretboard-client-kinto/.gitignore
  3. +46 −0 fretboard-client-kinto/build.gradle
  4. +21 −0 fretboard-client-kinto/proguard-rules.pro
  5. +5 −0 fretboard-client-kinto/src/main/AndroidManifest.xml
  6. +24 −0 fretboard-client-kinto/src/main/java/mozilla/components/service/fretboard/source/kinto/HttpClient.kt
  7. +35 −0 ...to/src/main/java/mozilla/components/service/fretboard/source/kinto/HttpURLConnectionHttpClient.kt
  8. +46 −0 ...board-client-kinto/src/main/java/mozilla/components/service/fretboard/source/kinto/KintoClient.kt
  9. +93 −0 ...nt-kinto/src/main/java/mozilla/components/service/fretboard/source/kinto/KintoExperimentSource.kt
  10. +64 −0 ...rc/test/java/mozilla/components/service/fretboard/source/kinto/HttpURLConnectionHttpClientTest.kt
  11. +35 −0 ...d-client-kinto/src/test/java/mozilla/components/service/fretboard/source/kinto/KintoClientTest.kt
  12. +148 −0 ...into/src/test/java/mozilla/components/service/fretboard/source/kinto/KintoExperimentSourceTest.kt
  13. +1 −0 fretboard-storage-flatfile/.gitignore
  14. +49 −0 fretboard-storage-flatfile/build.gradle
  15. +21 −0 fretboard-storage-flatfile/proguard-rules.pro
  16. +56 −0 ...t/java/mozilla/components/service/fretboard/storage/atomicfile/AtomicFileExperimentStorageTest.kt
  17. +5 −0 fretboard-storage-flatfile/src/main/AndroidManifest.xml
  18. +23 −0 .../main/java/mozilla/components/service/fretboard/storage/atomicfile/AtomicFileExperimentStorage.kt
  19. +54 −0 ...le/src/main/java/mozilla/components/service/fretboard/storage/atomicfile/ExperimentsSerializer.kt
  20. +118 −0 ...rc/test/java/mozilla/components/service/fretboard/storage/atomicfile/ExperimentsSerializerTest.kt
  21. +30 −0 fretboard/src/main/java/mozilla/components/service/fretboard/Experiment.kt
  22. +7 −0 fretboard/src/main/java/mozilla/components/service/fretboard/ExperimentDownloadException.kt
  23. +22 −0 fretboard/src/main/java/mozilla/components/service/fretboard/ExperimentSource.kt
  24. +25 −0 fretboard/src/main/java/mozilla/components/service/fretboard/ExperimentStorage.kt
  25. +34 −1 fretboard/src/main/java/mozilla/components/service/fretboard/Fretboard.kt
  26. +117 −0 fretboard/src/main/java/mozilla/components/service/fretboard/JSONExperimentParser.kt
  27. +29 −2 fretboard/src/test/java/mozilla/components/service/fretboard/FretboardTest.kt
  28. +59 −0 fretboard/src/test/java/mozilla/components/service/fretboard/JSONExperimentParserTest.kt
  29. +1 −1 settings.gradle
@@ -8,6 +8,9 @@ buildscript {
junit: '4.12',
robolectric: '3.8',
mockito: '2.18.0',
runner: '1.0.2',
annotations: '27.1.1',
mockwebserver: '3.10.0'
]

ext.build = [
@@ -0,0 +1 @@
/build
@@ -0,0 +1,46 @@
/* 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/. */

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'jacoco'
apply plugin: 'jacoco-android'

android {
compileSdkVersion rootProject.ext.build['compileSdkVersion']

defaultConfig {
minSdkVersion rootProject.ext.build['minSdkVersion']
targetSdkVersion rootProject.ext.build['targetSdkVersion']
}

lintOptions {
warningsAsErrors true
abortOnError true
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

jacocoAndroidUnitTestReport {
csv.enabled false
html.enabled true
xml.enabled true
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:${rootProject.ext.dependencies['kotlin']}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${rootProject.ext.dependencies['coroutines']}"
implementation project(':fretboard')

testImplementation "junit:junit:${rootProject.ext.dependencies['junit']}"
testImplementation "org.robolectric:robolectric:${rootProject.ext.dependencies['robolectric']}"
testImplementation "org.mockito:mockito-core:${rootProject.ext.dependencies['mockito']}"
testImplementation "com.squareup.okhttp3:mockwebserver:${rootProject.ext.dependencies['mockwebserver']}"
}
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,5 @@
<!-- 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/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.service.fretboard.source.kinto" />
@@ -0,0 +1,24 @@
/* 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.components.service.fretboard.source.kinto

import java.net.URL

/**
* Represents an http client, used to
* make it easy to swap implementations
* as needed
*/
interface HttpClient {
/**
* Performs a GET request to the specified URL, supplying
* the provided headers
*
* @param url destination url
* @param headers headers to submit with the request
* @return HTTP response
*/
fun get(url: URL, headers: Map<String, String>? = null): String
}
@@ -0,0 +1,35 @@
/* 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.components.service.fretboard.source.kinto

import mozilla.components.service.fretboard.ExperimentDownloadException
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL

/**
* HttpURLConnection-based Http client
*/
internal class HttpURLConnectionHttpClient : HttpClient {
override fun get(url: URL, headers: Map<String, String>?): String {
var urlConnection: HttpURLConnection? = null
try {
urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.useCaches = false
headers?.forEach { urlConnection.setRequestProperty(it.key, it.value) }

val responseCode = urlConnection.responseCode
if (responseCode !in 200..299)
throw ExperimentDownloadException("Status code: $responseCode")

return urlConnection.inputStream.bufferedReader().use { it.readText() }
} catch (e: IOException) {
throw ExperimentDownloadException(e.message)
} finally {
urlConnection?.disconnect()
}
}
}
@@ -0,0 +1,46 @@
/* 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.components.service.fretboard.source.kinto

import java.net.URL

/**
* Helper class to make it easier to interact with Kinto
*
* @param httpClient http client to use
* @param baseUrl Kinto server url
* @param bucketName name of the bucket to fetch
* @param collectionName name of the collection to fetch
* @param headers headers to provide along with the request
*/
internal class KintoClient(
private val httpClient: HttpClient = HttpURLConnectionHttpClient(),
val baseUrl: String,
val bucketName: String,
val collectionName: String,
val headers: Map<String, String>? = null
) {

/**
* Returns all records from the collection
*
* @return Kinto response with all records
*/
fun get(): String {
return httpClient.get(URL(recordsUrl()), headers)
}

/**
* Performs a diff, given the last_modified time
*
* @param lastModified last modified time
* @return Kinto diff response
*/
fun diff(lastModified: Long): String {
return httpClient.get(URL("${recordsUrl()}?_since=$lastModified"), headers)
}

private fun recordsUrl() = "$baseUrl/buckets/$bucketName/collections/$collectionName/records"
}
@@ -0,0 +1,93 @@
/* 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.components.service.fretboard.source.kinto

import mozilla.components.service.fretboard.Experiment
import mozilla.components.service.fretboard.ExperimentSource
import mozilla.components.service.fretboard.JSONExperimentParser
import org.json.JSONArray
import org.json.JSONObject

/**
* Class responsible for fetching and
* parsing experiments from a Kinto server
*
* @param baseUrl Kinto server url
* @param bucketName name of the bucket to fetch
* @param collectionName name of the collection to fetch
*/
class KintoExperimentSource(
val baseUrl: String,
val bucketName: String,
val collectionName: String,
private val client: HttpClient = HttpURLConnectionHttpClient()
) : ExperimentSource {
override fun getExperiments(experiments: List<Experiment>): List<Experiment> {
val experimentsDiff = getExperimentsDiff(client, experiments)
return mergeExperimentsFromDiff(experimentsDiff, experiments)
}

private fun getExperimentsDiff(client: HttpClient, experiments: List<Experiment>): String {
val lastModified = getMaxLastModified(experiments)
val kintoClient = KintoClient(client, baseUrl, bucketName, collectionName)
return if (lastModified != null) {
kintoClient.diff(lastModified)
} else {
kintoClient.get()
}
}

private fun mergeExperimentsFromDiff(experimentsDiff: String, experiments: List<Experiment>): List<Experiment> {
val mutableExperiments = experiments.toMutableList()
val experimentParser = JSONExperimentParser()
val diffJsonObject = JSONObject(experimentsDiff)
val data = diffJsonObject.get(DATA_KEY)
if (data is JSONObject) {
if (data.getBoolean(DELETED_KEY)) {
mergeDeleteDiff(data, mutableExperiments)
}
} else {
mergeAddUpdateDiff(experimentParser, data as JSONArray, mutableExperiments)
}
return mutableExperiments
}

private fun mergeDeleteDiff(data: JSONObject, mutableExperiments: MutableList<Experiment>) {
mutableExperiments.remove(mutableExperiments.single { it.id == data.getString(ID_KEY) })
}

private fun mergeAddUpdateDiff(
experimentParser: JSONExperimentParser,
experimentsJsonArray: JSONArray,
mutableExperiments: MutableList<Experiment>
) {
for (i in 0 until experimentsJsonArray.length()) {
val experimentJsonObject = experimentsJsonArray[i] as JSONObject
val experiment = mutableExperiments.singleOrNull { it.id == experimentJsonObject.getString(ID_KEY) }
if (experiment != null)
mutableExperiments.remove(experiment)
mutableExperiments.add(experimentParser.fromJson(experimentJsonObject))
}
}

private fun getMaxLastModified(experiments: List<Experiment>): Long? {
var maxLastModified: Long = -1
for (experiment in experiments) {
val lastModified = experiment.lastModified
if (lastModified != null) {
if (lastModified > maxLastModified) {
maxLastModified = lastModified
}
}
}
return if (maxLastModified > 0) maxLastModified else null
}

companion object {
private const val ID_KEY = "id"
private const val DATA_KEY = "data"
private const val DELETED_KEY = "deleted"
}
}
@@ -0,0 +1,64 @@
/* 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.components.service.fretboard.source.kinto

import mozilla.components.service.fretboard.ExperimentDownloadException
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class HttpURLConnectionHttpClientTest {
@Test(expected = ExperimentDownloadException::class)
fun testGET404() {
testGETError(404)
}

@Test(expected = ExperimentDownloadException::class)
fun testGET500() {
testGETError(500)
}

@Test
fun testGETWithoutHeaders() {
testGET()
}

@Test
fun testGETWithHeaders() {
testGET(mapOf("Accept" to "application/json")) {
assertEquals("application/json", it.headers["Accept"])
}
}

private fun testGET(headers: Map<String, String>? = null, assertions: ((RecordedRequest) -> Unit)? = null) {
val response = """{"data":[]}"""
val server = MockWebServer()
server.enqueue(MockResponse().setBody(response))
assertEquals(response, HttpURLConnectionHttpClient().get(server.url("/").url(), headers))
val request = server.takeRequest()
assertEquals("GET", request.method)
assertEquals("no-cache", request.headers["Cache-Control"])
assertEquals("no-cache", request.headers["Pragma"])
if (assertions != null) {
assertions(request)
}
server.shutdown()
}

private fun testGETError(responseCode: Int) {
val server = MockWebServer()
server.enqueue(MockResponse().setResponseCode(responseCode))
try {
HttpURLConnectionHttpClient().get(server.url("/").url())
} finally {
server.shutdown()
}
}
}
@@ -0,0 +1,35 @@
/* 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.components.service.fretboard.source.kinto

import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.robolectric.RobolectricTestRunner
import java.net.URL

@RunWith(RobolectricTestRunner::class)
class KintoClientTest {
private val baseUrl = "http://example.test"
private val bucketName = "fretboard"
private val collectionName = "experiments"

@Test
fun testGet() {
val httpClient = mock(HttpClient::class.java)
val kintoClient = KintoClient(httpClient, baseUrl, bucketName, collectionName)
kintoClient.get()
verify(httpClient).get(URL("http://example.test/buckets/fretboard/collections/experiments/records"))
}

@Test
fun testDiff() {
val httpClient = mock(HttpClient::class.java)
val kintoClient = KintoClient(httpClient, baseUrl, bucketName, collectionName)
kintoClient.diff(1527179995)
verify(httpClient).get(URL("http://example.test/buckets/fretboard/collections/experiments/records?_since=1527179995"))
}
}
Oops, something went wrong.

0 comments on commit 7996c0c

Please sign in to comment.
You can’t perform that action at this time.