Commit a445c602 authored by Christophe Henry's avatar Christophe Henry

Refacto DB

parent 1ce8a4a3
Pipeline #3581 passed with stage
in 0 seconds
......@@ -2,15 +2,17 @@ pipeline {
agent {
docker {
image "bitriseio/docker-android"
args "-v /etc/passwd:/etc/passwd:ro -u root --privileged"
args "-v /etc/passwd:/etc/passwd:ro " +
"-v /home/android/android-sdk-linux/:/opt/android-sdk-linux/:rw " +
"-u root --privileged"
}
}
environment {
MIN_SDK_VERSION = 21
MIN_SDK_VERSION = 23
MAX_SDK_VERSION = 28
MIN_SDK_IMAGE = "system-images;android-$MIN_SDK_VERSION;default;x86_64"
MAX_SDK_IMAGE = "system-images;android-$MAX_SDK_VERSION;default;x86_64"
MIN_SDK_IMAGE = "system-images;android-$MIN_SDK_VERSION;default;x86_64".toString()
MAX_SDK_IMAGE = "system-images;android-$MAX_SDK_VERSION;default;x86_64".toString()
ANDROID_HOME = "/opt/android-sdk-linux"
}
......@@ -18,6 +20,17 @@ pipeline {
buildDiscarder(logRotator(numToKeepStr: "2"))
skipStagesAfterUnstable()
gitLabConnection('GitlabFeneas')
gitlabBuilds(builds: [
'Compile',
'Lint',
'Unit tests',
'Instrumented tests on min SDK image',
'Instrumented tests on max SDK image'
])
disableConcurrentBuilds()
newContainerPerStage()
timestamps()
timeout(time: 30, unit: 'MINUTES')
}
triggers {
......@@ -35,128 +48,74 @@ pipeline {
addCiMessage: true,
addVoteOnMergeRequest: true,
acceptMergeRequestOnSuccess: false,
cancelPendingBuildsOnUpdate: true
)
}
libraries {
lib("android-pipeline-steps")
}
stages {
stage("Pre") {
steps {
updateGitlabCommitStatus name: "Job", state: "running"
updateGitlabCommitStatus name: "Pre", state: "running"
library "android-pipeline-steps"
}
post {
failure {
updateGitlabCommitStatus name: "Pre", state: "failed"
}
success {
updateGitlabCommitStatus name: "Pre", state: "success"
}
}
}
stage("Compile") {
steps {
updateGitlabCommitStatus name: "Compile", state: "running"
sh "./gradlew compileReleaseSources"
}
post {
failure {
updateGitlabCommitStatus name: "Compile", state: "failed"
}
success {
updateGitlabCommitStatus name: "Compile", state: "success"
gitlabCommitStatus("Compile") {
sh "./gradlew compileReleaseSources"
}
}
}
stage("Lint") {
steps {
updateGitlabCommitStatus name: "Lint", state: "running"
sh "./gradlew spotlessApply lint"
sh "./gradlew lintRelease --continue"
androidLint pattern: "**/lint-results-*.xml"
publishHTML([
allowMissing : false,
alwaysLinkToLastBuild: true,
keepAll : false,
reportDir : "$WORKSPACE/app/build/reports/",
reportFiles : "lint-results-release.html",
reportName : "HTML Report",
reportTitles : ""
])
}
post {
failure {
updateGitlabCommitStatus name: "Lint", state: "failed"
}
success {
updateGitlabCommitStatus name: "Lint", state: "success"
gitlabCommitStatus("Lint") {
sh "./gradlew spotlessCheck lint"
androidLint pattern: "**/lint-results-*.xml"
publishHTML([
allowMissing : false,
alwaysLinkToLastBuild: true,
keepAll : false,
reportDir : "$WORKSPACE/app/build/reports/",
reportFiles : "lint-results-release.html",
reportName : "HTML Report",
reportTitles : ""
])
}
}
}
stage("Unit tests") {
steps {
updateGitlabCommitStatus name: "Unit tests", state: "running"
sh "./gradlew testReleaseUnitTest --info"
junit "**/test-results/**/*.xml"
publishHTML([
allowMissing : false,
alwaysLinkToLastBuild: true,
keepAll : false,
reportDir : "$WORKSPACE/app/build/reports/tests/testReleaseUnitTest",
reportFiles : "index.html",
reportName : "Junit test report",
reportTitles : ""
])
}
post {
failure {
updateGitlabCommitStatus name: "Unit tests", state: "failed"
}
success {
updateGitlabCommitStatus name: "Unit tests", state: "success"
gitlabCommitStatus("Unit tests") {
sh "./gradlew testReleaseUnitTest --info"
junit "**/test-results/**/*.xml"
publishHTML([
allowMissing : false,
alwaysLinkToLastBuild: true,
keepAll : false,
reportDir : "$WORKSPACE/app/build/reports/tests/testReleaseUnitTest",
reportFiles : "index.html",
reportName : "Junit test report",
reportTitles : ""
])
}
}
}
stage("Instrumented tests") {
parallel {
stage("on min SDK level") {
steps {
sh "echo ${env.MIN_SDK_IMAGE}"
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "running"
withAvd(hardwareProfile: "Nexus 5X", systemImage: env.MIN_SDK_IMAGE, headless: true) {
sh "./gradlew clean connectedDebugAndroidTest"
}
}
post {
failure {
updateGitlabCommitStatus name: "Instrumented tests on min SDK image", state: "failed"
}
success {
updateGitlabCommitStatus name: "Instrumented tests on min SDK image", state: "success"
}
stage("on min SDK level") {
steps {
gitlabCommitStatus("Instrumented tests on min SDK image") {
withAvd(hardwareProfile: "Nexus 5X", systemImage: env.MIN_SDK_IMAGE, headless: true) {
sh "./gradlew clean connectedCheck"
}
}
}
}
stage("on max SDK level") {
steps {
sh "echo ${env.MAX_SDK_IMAGE}"
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "running"
withAvd(hardwareProfile: "Nexus 5X", systemImage: env.MAX_SDK_IMAGE, headless: true) {
sh "./gradlew clean connectedDebugAndroidTest"
}
}
post {
failure {
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "failed"
}
success {
updateGitlabCommitStatus name: "Instrumented tests on target SDK image", state: "success"
}
stage("on max SDK level") {
steps {
gitlabCommitStatus("Instrumented tests on max SDK image") {
withAvd(hardwareProfile: "Nexus 5X", systemImage: env.MAX_SDK_IMAGE, headless: true) {
sh "./gradlew clean connectedCheck"
}
}
}
......@@ -171,16 +130,4 @@ pipeline {
// }
// }
}
post {
always {
sh "chown -R jenkins ${env.WORKSPACE}"
}
failure {
updateGitlabCommitStatus name: "Job", state: "failed"
}
success {
updateGitlabCommitStatus name: "Job", state: "success"
}
}
}
\ No newline at end of file
/build
/src/main/assets/*.properties
/release/
/src/main/java/intellij.gdsl
......@@ -16,6 +16,7 @@ android {
versionCode 12
versionName "1.2.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": schema_location]
......@@ -96,13 +97,6 @@ spotless {
"max_line_length" : 120,
])
}
format "xml", {
target fileTree(".") {
include "**/*.xml"
exclude "**/build/**"
}
eclipseWtp("xml").configFile "$rootDir/freshrss.xmlformat.prefs".toString()
}
}
dependencies {
......@@ -110,16 +104,15 @@ dependencies {
def activity_version = "1.1.0"
def fragment_version = "1.2.1"
def lifecycle_version = "2.2.0"
def room_version = '2.2.3'
def room_version = "2.2.3"
def roomigrant_version = "0.1.7"
def fuel_version = "2.0.1"
def jackson_version = '2.10.2'
def espresso_version = "3.2.0"
def test_runnner_version = "1.2.0"
def promise_version = "3.3.0"
def android_navigation = "1.0.0"
def jsoup_version = "1.12.1"
def acraVersion = "5.1.3"
def jsoup_version = '1.12.2'
def acraVersion = '5.5.0'
def autoservice_version = "1.0-rc6"
// Linter
......@@ -138,7 +131,7 @@ dependencies {
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation "androidx.core:core-ktx:1.1.0"
implementation 'androidx.core:core-ktx:1.2.0'
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.preference:preference-ktx:1.1.0"
......@@ -161,6 +154,7 @@ dependencies {
// Room & roomigrant
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
androidTestImplementation "androidx.room:room-testing:$room_version"
......@@ -192,9 +186,11 @@ dependencies {
implementation "ch.acra:acra-notification:$acraVersion"
// Tests
testImplementation 'junit:junit:4.13'
androidTestImplementation "androidx.test:runner:$test_runnner_version"
testImplementation "junit:junit:4.13"
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "com.github.javafaker:javafaker:1.0.2"
// Debug
debugImplementation "com.facebook.stetho:stetho:1.5.1"
......
package fr.chenry.android.freshrss
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTestActivity {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("fr.chenry.android.freshrss", appContext.packageName)
}
}
......@@ -5,14 +5,9 @@ import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
abstract class FreshRSSDabatabaseBaseTest {
open val testDB get() = "migration-test"
@get:Rule
......@@ -22,23 +17,17 @@ abstract class FreshRSSDabatabaseBaseTest {
FrameworkSQLiteOpenHelperFactory()
)
val instrumentation get() = InstrumentationRegistry.getInstrumentation()
val assets get() = instrumentation.context.assets
val context get() = instrumentation.context
val targetContext get() = instrumentation.targetContext
private lateinit var _database: SQLiteDatabase
val database: SQLiteDatabase
get() {
if(!::_database.isInitialized || !_database.isOpen) {
_database = SQLiteDatabase.openDatabase(
targetContext.getDatabasePath(testDB).absolutePath,
null, SQLiteDatabase.OPEN_READWRITE
)
}
return _database
}
val instrumentation inline get() = InstrumentationRegistry.getInstrumentation()
val context inline get() = instrumentation.targetContext
val assets inline get() = instrumentation.context.assets
val database: SQLiteDatabase by lazy {
SQLiteDatabase.openDatabase(
context.getDatabasePath(testDB).absolutePath,
null, SQLiteDatabase.OPEN_READWRITE
)
}
fun createDatabaseFromScratch(targetVersion: Int) {
fun createDatabaseFromScratch(targetVersion: Int = FreshRSSDabatabase_Migrations.build().size) {
helper.createDatabase(testDB, 1).apply {
execSQLWithImports(this, "migration-test-1-before")
migrateDatabase(this, 1, targetVersion)
......@@ -60,7 +49,7 @@ abstract class FreshRSSDabatabaseBaseTest {
}
}
fun execSQLWithImports(sqlMgr: SupportSQLiteDatabase, name: String) {
private fun execSQLWithImports(sqlMgr: SupportSQLiteDatabase, name: String) {
val importRegex = "^--\\s*import\\s*:\\s*(\\S+).*$"
assets.open("database/${name.replace(".sql", "")}.sql").reader().readLines().forEach {sql ->
if(sql.matches(importRegex.toRegex())) {
......
package fr.chenry.android.freshrss.store.database
import androidx.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
......@@ -9,7 +10,7 @@ import org.junit.runner.RunWith
class FreshRSSDabatabaseMigrationsTest : FreshRSSDabatabaseBaseTest() {
@Test
fun allMigrations() {
createDatabaseFromScratch(FreshRSSDabatabase_Migrations.build().size)
createDatabaseFromScratch()
}
@Test
......@@ -42,7 +43,7 @@ class FreshRSSDabatabaseMigrationsTest : FreshRSSDabatabaseBaseTest() {
val query = database.rawQuery("SELECT * FROM subscriptions", arrayOf())
assertEquals(5, query.count)
assertEquals(arrayOf("id", "title", "unreadCount", "imageBitmap", "iconUrl"), query.columnNames)
assertArrayEquals(arrayOf("id", "title", "unreadCount", "imageBitmap", "iconUrl"), query.columnNames)
assertEquals(true, query.moveToFirst())
......@@ -51,7 +52,6 @@ class FreshRSSDabatabaseMigrationsTest : FreshRSSDabatabaseBaseTest() {
val bindArgs = arrayOf("54f869cf-a764-4cb5-a77e-c46d8ca798e4")
database.execSQL("UPDATE subscriptions SET iconUrl = 'https://example.com/icon' WHERE id = ?", bindArgs)
database.close()
database.rawQuery("SELECT * FROM subscriptions WHERE id = ?", bindArgs).let {
it.moveToFirst()
assertEquals("https://example.com/icon", it.getString(4))
......
package fr.chenry.android.freshrss.store.database
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.utils.SubscriptionsTestUtils
import fr.chenry.android.freshrss.utils.TestUtils
import kotlinx.coroutines.runBlocking
import org.junit.*
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FreshRSSDabatabaseTest: FreshRSSDabatabaseBaseTest() {
private val dbName = "${this::class.qualifiedName!!}.db"
private lateinit var db: FreshRSSDabatabase
@Before
fun setUp() {
db = Room.databaseBuilder(context, FreshRSSDabatabase::class.java, dbName)
.addMigrations(*FreshRSSDabatabase_Migrations.build())
.build()
}
@After
fun tearDown() {
context.deleteDatabase(dbName)
}
@Test
fun syncSubscriptionsFromScratch() {
runBlocking {
val subscriptions = SubscriptionsTestUtils.createSubscriptions(10)
.sortedBy {it.id}.map {it.id to it}.toMap()
db.syncSubscriptions(subscriptions)
val result = db.getAllSubcriptionsIds().sorted().toTypedArray()
assertArrayEquals(subscriptions.keys.toTypedArray(), result)
}
}
@Test
fun syncSubscriptionsWithUpdates() {
runBlocking {
val subscriptions = SubscriptionsTestUtils.createSubscriptions(10)
val subscriptionsMap = subscriptions.sortedBy {it.id}.map {it.id to it}.toMap(LinkedHashMap())
db.syncSubscriptions(subscriptionsMap)
val oldSubscription = subscriptions[5]
val newSubscription = Subscription(
oldSubscription.id,
TestUtils.harryPotter().character(),
"${oldSubscription.iconUrl}/icon",
oldSubscription.unreadCount + 20,
oldSubscription.subscriptionCategories + TestUtils.harryPotter().character().split("\\s+".toRegex()),
oldSubscription.newestArticleDate.plusDays(3)
)
subscriptionsMap[newSubscription.id] = newSubscription
db.syncSubscriptions(subscriptionsMap)
val expected = oldSubscription.copy(
title = newSubscription.title,
iconUrl = newSubscription.iconUrl,
subscriptionCategories = newSubscription.subscriptionCategories
)
val actual = db.getSubcriptionsById(oldSubscription.id).blockingFirst().firstOrNull()
assertEquals(expected, actual)
}
}
@Test
fun syncSubscriptionsWithDeletions() {
runBlocking {
val subscriptions = SubscriptionsTestUtils.createSubscriptions(10)
val subscriptionsMap = subscriptions.sortedBy {it.id}.map {it.id to it}.toMap(LinkedHashMap())
db.syncSubscriptions(subscriptionsMap)
val subscriptionToRemove = subscriptions[5]
subscriptionsMap.remove(subscriptionToRemove.id)
assertEquals(9, subscriptionsMap.size)
db.syncSubscriptions(subscriptionsMap)
val actual = db.getAllSubcriptionsIds()
val expected = subscriptions.filter {it.id != subscriptionToRemove.id}.map {it.id}.sorted()
assertEquals(expected, actual)
}
}
}
package fr.chenry.android.freshrss.utils
import fr.chenry.android.freshrss.store.database.models.Subscription
import org.joda.time.LocalDateTime
import java.util.concurrent.TimeUnit
object SubscriptionsTestUtils {
fun createSubscription(
id: String = TestUtils.idNumber().valid(),
title: String = TestUtils.lebowski().character(),
iconUrl: String = TestUtils.internet().url(),
unreadCount: Int = TestUtils.number().numberBetween(0, 100),
subscriptionCategories: List<String> = listOf(),
newestArticleDate: LocalDateTime = LocalDateTime(
TestUtils.jodaDate().past(15,TimeUnit.DAYS).millis
)
) = Subscription(id, title, iconUrl, unreadCount, subscriptionCategories, newestArticleDate)
fun createSubscriptions(number: Int) = (1..number).map {createSubscription()}
}
package fr.chenry.android.freshrss.utils
import com.github.javafaker.DateAndTime
import com.github.javafaker.Faker
import org.joda.time.DateTime
import java.util.Locale
import java.util.concurrent.TimeUnit
class JodaDateAndTime internal constructor(private val faker: Faker){
/**
* Generates a future date from now. Note that there is a 1 second slack to avoid generating a past date.
*
* @param atMost
* at most this amount of time ahead from now exclusive.
* @param unit
* the time unit.
* @return a future date from now.
*/
fun future(atMost: Int, unit: TimeUnit): DateTime {
val aBitLaterThanNow = DateTime(DateTime.now().plusSeconds(1))
return future(atMost, unit, aBitLaterThanNow)
}
/**
* Generates a future date from now, with a minimum time.
*
* @param atMost
* at most this amount of time ahead from now exclusive.
* @param minimum
* the minimum amount of time in the future from now.
* @param unit
* the time unit.
* @return a future date from now.
*/
fun future(atMost: Int, minimum: Int, unit: TimeUnit): DateTime {
val now = DateTime()
val minimumDateTime = DateTime(DateTime.now().millis + unit.toMillis(minimum.toLong()))
return future(atMost - minimum, unit, minimumDateTime)
}
/**
* Generates a future date relative to the `referenceDateTime`.
*
* @param atMost
* at most this amount of time ahead to the `referenceDateTime` exclusive.
* @param unit
* the time unit.
* @param referenceDateTime
* the future date relative to this date.
* @return a future date relative to `referenceDateTime`.
*/
fun future(atMost: Int, unit: TimeUnit, referenceDateTime: DateTime): DateTime {
val upperBound = unit.toMillis(atMost.toLong())
var futureMillis = referenceDateTime.millis
futureMillis += 1 + faker.random().nextLong(upperBound - 1)
return DateTime(futureMillis)
}
/**
* Generates a past date from now. Note that there is a 1 second slack added.
*
* @param atMost
* at most this amount of time earlier from now exclusive.
* @param unit
* the time unit.
* @return a past date from now.
*/
fun past(atMost: Int, unit: TimeUnit): DateTime {
val aBitEarlierThanNow = DateTime(DateTime.now().millis - 1000)
return past(atMost, unit, aBitEarlierThanNow)
}