- room database migration in progress

This commit is contained in:
2023-09-08 14:59:54 +01:00
parent 1258afc4d0
commit 220afa04cf
24 changed files with 695 additions and 188 deletions

View File

@@ -1,5 +1,6 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-kapt'
def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD")
def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD") def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD")
@@ -17,6 +18,12 @@ android {
versionName "2.1" versionName "2.1"
testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
} }
signingConfigs { signingConfigs {
release { release {
@@ -33,6 +40,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
sourceSets {
androidTest.assets.srcDirs +=
files("$projectDir/schemas".toString())
}
useLibrary 'android.test.mock' useLibrary 'android.test.mock'
} }
@@ -77,6 +88,8 @@ dependencies {
/ * Room database * / / * Room database * /
runtimeOnly "androidx.room:room-runtime:$ROOM_VERSION" runtimeOnly "androidx.room:room-runtime:$ROOM_VERSION"
implementation "androidx.room:room-ktx:$ROOM_VERSION" implementation "androidx.room:room-ktx:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION"
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
/ *Kodein Dependency Injection * / / *Kodein Dependency Injection * /
implementation "org.kodein.di:kodein-di-generic-jvm:$KODEIN_VERSION" implementation "org.kodein.di:kodein-di-generic-jvm:$KODEIN_VERSION"
implementation "org.kodein.di:kodein-di-framework-android-x:$KODEIN_VERSION" implementation "org.kodein.di:kodein-di-framework-android-x:$KODEIN_VERSION"

View File

@@ -1,17 +1,19 @@
package com.appttude.h_mal.farmr.application package com.appttude.h_mal.farmr.application
import androidx.room.Room
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.base.BaseApplication import com.appttude.h_mal.farmr.base.BaseApplication
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.Shift
class TestAppClass : BaseApplication() { class TestAppClass : BaseApplication() {
private val idlingResources = CountingIdlingResource("Data_loader") private val idlingResources = CountingIdlingResource("Data_loader")
lateinit var database: LegacyDatabase lateinit var database: AppDatabase
lateinit var preferenceProvider: PreferenceProvider lateinit var preferenceProvider: PreferenceProvider
override fun onCreate() { override fun onCreate() {
@@ -19,9 +21,9 @@ class TestAppClass : BaseApplication() {
IdlingRegistry.getInstance().register(idlingResources) IdlingRegistry.getInstance().register(idlingResources)
} }
override fun createDatabase(): LegacyDatabase { override fun createDatabase(): AppDatabase {
database = database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver) .build()
return database return database
} }
@@ -30,9 +32,9 @@ class TestAppClass : BaseApplication() {
return preferenceProvider return preferenceProvider
} }
fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift) fun addToDatabase(shift: Shift) = database.getShiftDao().upsertFullShift(shift.convertToShiftEntity())
fun addShiftsToDatabase(shifts: List<Shift>) = shifts.forEach { addToDatabase(it) } fun addShiftsToDatabase(shifts: List<Shift>) = shifts.map { it.convertToShiftEntity() }.let { database.getShiftDao().upsertListOfFullShift(it) }
fun clearDatabase() = database.deleteAllShiftsInDatabase() fun clearDatabase() = database.getShiftDao().deleteAllShifts()
fun cleanPrefs() = preferenceProvider.clearPrefs() fun cleanPrefs() = preferenceProvider.clearPrefs()
} }

View File

@@ -0,0 +1,68 @@
package com.appttude.h_mal.farmr.data.room
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.testing.MigrationTestHelper
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME
import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.ui.utils.getShifts
import org.junit.Rule
import org.junit.Test
import java.io.IOException
class RoomMigrationTest {
private val TEST_DB = "migration-test"
@get:Rule
val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName
)
@Test
@Throws(IOException::class)
fun migrationFrom2To3_containsCorrectData() {
// Create the database in version 4
val db = testHelper.createDatabase(TEST_DB, 4)
// Insert some data
getShifts().forEach {
db.insert(
ShiftsContract.ShiftsEntry.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, ContentValues().apply {
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE, it.type.type)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, it.description)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE, it.date)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN, it.timeIn ?: "00:00")
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT, it.timeOut ?: "00:00")
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION, it.duration ?: 0.00f)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK, it.breakMins ?: 0)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT, it.units ?: 0.00f)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE, it.rateOfPay)
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY, it.totalPay)
})
}
//Prepare for the next version
db.close()
// Re-open the database with version 5 and provide MIGRATION_4_5
// and MIGRATION_4_5 as the migration process.
testHelper.runMigrationsAndValidate(
TEST_DB, 5,
true, MIGRATION_4_5
)
// MigrationTestHelper automatically verifies the schema
//changes, but not the data validity
// Validate that the data was migrated properly.
val dbUser: User = getMigratedRoomDatabase().userDao().getUser()
assertEquals(dbUser.getId(), USER.getId())
assertEquals(dbUser.getUserName(), USER.getUserName())
// The date was missing in version 2, so it should be null in
//version 3
assertEquals(dbUser.getDate(), null)
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Application
import com.appttude.h_mal.farmr.data.RepositoryImpl import com.appttude.h_mal.farmr.data.RepositoryImpl
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
@@ -26,6 +27,6 @@ abstract class BaseApplication() : Application(), KodeinAware {
bind() from provider { ApplicationViewModelFactory(instance()) } bind() from provider { ApplicationViewModelFactory(instance()) }
} }
abstract fun createDatabase(): LegacyDatabase abstract fun createDatabase(): AppDatabase
abstract fun createPrefs(): PreferenceProvider abstract fun createPrefs(): PreferenceProvider
} }

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.farmr.data package com.appttude.h_mal.farmr.data
import androidx.lifecycle.LiveData
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Sortable
@@ -8,8 +10,8 @@ import com.appttude.h_mal.farmr.model.Sortable
interface Repository { interface Repository {
fun insertShiftIntoDatabase(shift: Shift): Boolean fun insertShiftIntoDatabase(shift: Shift): Boolean
fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean
fun readShiftsFromDatabase(): List<ShiftObject>? fun readShiftsFromDatabase(): LiveData<List<ShiftEntity>>
fun readSingleShiftFromDatabase(id: Long): ShiftObject? fun readSingleShiftFromDatabase(id: Long): ShiftEntity?
fun deleteSingleShiftFromDatabase(id: Long): Boolean fun deleteSingleShiftFromDatabase(id: Long): Boolean
fun deleteAllShiftsFromDatabase(): Boolean fun deleteAllShiftsFromDatabase(): Boolean
fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?> fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?>

View File

@@ -1,50 +1,77 @@
package com.appttude.h_mal.farmr.data package com.appttude.h_mal.farmr.data
import androidx.lifecycle.LiveData
import androidx.room.Room
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Sortable
import com.appttude.h_mal.farmr.utils.dateStringIsValid
import com.appttude.h_mal.farmr.utils.timeStringIsValid
class RepositoryImpl( class RepositoryImpl(
private val legacyDatabase: LegacyDatabase, roomDatabase: AppDatabase,
private val preferenceProvider: PreferenceProvider private val preferenceProvider: PreferenceProvider
): Repository { ): Repository {
private val shiftDao = roomDatabase.getShiftDao()
override fun insertShiftIntoDatabase(shift: Shift): Boolean { override fun insertShiftIntoDatabase(shift: Shift): Boolean {
return legacyDatabase.insertShiftDataIntoDatabase(shift) != null val shiftEntity = shift.convertToShiftEntity()
return shiftDao.upsertFullShift(shiftEntity) > 0
} }
override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean { override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean {
return legacyDatabase.updateShiftDataIntoDatabase( if (shift.description.isBlank() || shift.description.trim().length < 3) {
id = id, throw IllegalArgumentException("description required")
typeString = shift.type.type, }
descriptionString = shift.description, if (!shift.date.dateStringIsValid()) {
dateString = shift.date, throw IllegalArgumentException("date required")
timeInString = shift.timeIn ?: "", }
timeOutString = shift.timeOut ?: "", shift.timeIn?.takeIf { !it.timeStringIsValid() }?.let {
duration = shift.duration ?: 0f, throw IllegalArgumentException("time in required")
breaks = shift.breakMins ?: 0, }
units = shift.units ?: 0f, shift.timeOut?.takeIf { !it.timeStringIsValid() }?.let {
payRate = shift.rateOfPay, throw IllegalArgumentException("time out required")
totalPay = shift.totalPay }
) == 1 shift.breakMins?.takeIf { it < 0 }?.let {
throw IllegalArgumentException("break required")
}
if (shift.timeIn != null || shift.timeOut != null) {
if (shift.duration == null) throw IllegalArgumentException("Duration required")
}
shift.units?.takeIf { it < 0 }?.let {
throw IllegalArgumentException("Units required")
}
if (shift.rateOfPay < 0) {
throw IllegalArgumentException("Rate of pay required")
}
if (shift.totalPay < 0) {
throw IllegalArgumentException("Total pay required")
}
val shiftEntity = shift.convertToShiftEntity(id)
return shiftDao.upsertFullShift(shiftEntity) > 0
} }
override fun readShiftsFromDatabase(): List<ShiftObject>? { override fun readShiftsFromDatabase(): LiveData<List<ShiftEntity>> {
return legacyDatabase.readShiftsFromDatabase() return shiftDao.getAllFullShift()
} }
override fun readSingleShiftFromDatabase(id: Long): ShiftObject? { override fun readSingleShiftFromDatabase(id: Long): ShiftEntity? {
return legacyDatabase.readSingleShiftWithId(id) return shiftDao.getCurrentFullShiftSingle(id)
} }
override fun deleteSingleShiftFromDatabase(id: Long): Boolean { override fun deleteSingleShiftFromDatabase(id: Long): Boolean {
return legacyDatabase.deleteSingleShift(id) == 1 return shiftDao.deleteShift(id) == 1
} }
override fun deleteAllShiftsFromDatabase(): Boolean { override fun deleteAllShiftsFromDatabase(): Boolean {
return legacyDatabase.deleteAllShiftsInDatabase() > 0 return shiftDao.deleteAllShifts() > 0
} }
override fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?> { override fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?> {

View File

@@ -33,10 +33,10 @@ class ShiftsDbHelper(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAM
} }
companion object { companion object {
private const val DATABASE_NAME = "shifts.db" const val DATABASE_NAME = "shifts.db"
private const val DATABASE_VERSION = 4 private const val DATABASE_VERSION = 4
private const val DEFAULT_TEXT = "Hourly" private const val DEFAULT_TEXT = "Hourly"
private const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " (" const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " ("
+ ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " TEXT NOT NULL, " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " TEXT NOT NULL, "
+ ShiftsEntry.COLUMN_SHIFT_DATE + " DATE NOT NULL, " + ShiftsEntry.COLUMN_SHIFT_DATE + " DATE NOT NULL, "
+ ShiftsEntry.COLUMN_SHIFT_TIME_IN + " TIME NOT NULL, " + ShiftsEntry.COLUMN_SHIFT_TIME_IN + " TIME NOT NULL, "

View File

@@ -0,0 +1,51 @@
package com.appttude.h_mal.farmr.data.room
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5
@Database(
entities = [ShiftEntity::class],
version = 5,
exportSchema = true
)
@TypeConverters(DateConverter::class, TimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun getShiftDao(): ShiftDao
companion object {
@Volatile
private var instance: AppDatabase? = null
private val LOCK = Any()
// create an instance of room database or use previously created instance
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also {
instance = it
}
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
).addMigrations(MIGRATION_4_5)
.addTypeConverter(DateConverter())
.addTypeConverter(TimeConverter())
.build()
}
}

View File

@@ -0,0 +1,37 @@
package com.appttude.h_mal.farmr.data.room
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.DeleteTable
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
@Dao
interface ShiftDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertFullShift(item: ShiftEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsertListOfFullShift(items: List<ShiftEntity>)
@Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1")
fun getCurrentFullShift(shiftId: Long): LiveData<ShiftEntity>
@Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1")
fun getCurrentFullShiftSingle(shiftId: Long): ShiftEntity?
@Query("SELECT * FROM shifts")
fun getAllFullShift(): LiveData<List<ShiftEntity>>
@Query("DELETE FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId")
fun deleteShift(shiftId: Long): Int
@Query("DELETE FROM shifts")
fun deleteAllShifts(): Int
}

View File

@@ -0,0 +1,19 @@
package com.appttude.h_mal.farmr.data.room.converters
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import java.sql.Date
@ProvidedTypeConverter
class DateConverter {
@TypeConverter
fun toDate(dateString: String): Date {
// Convert yyyy-MM-dd into date
return Date.valueOf(dateString)
}
@TypeConverter
fun fromDate(date: Date): String {
return date.toString()
}
}

View File

@@ -0,0 +1,23 @@
package com.appttude.h_mal.farmr.data.room.converters
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import java.sql.Date
import java.sql.Time
import java.time.Instant
@ProvidedTypeConverter
class TimeConverter {
@TypeConverter
fun toTime(timeString: String?): Time {
// Convert HH:mm into date
timeString?.let { return Time.valueOf("$timeString:00") }
return Time.valueOf("00:00:00")
}
@TypeConverter
fun fromTime(time: Time): String {
// Return string in format of HH:mm
return time.toString().substring(0,5)
}
}

View File

@@ -0,0 +1,34 @@
package com.appttude.h_mal.farmr.data.room.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.TABLE_NAME
import com.appttude.h_mal.farmr.model.ShiftType
import java.sql.Date
import java.sql.Time
@Entity(tableName = TABLE_NAME)
data class ShiftEntity(
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) val description: String,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DATE, typeAffinity = ColumnInfo.UNDEFINED) val date: Date,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_IN, typeAffinity = ColumnInfo.TEXT) val timeIn: Time,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_OUT, typeAffinity = ColumnInfo.TEXT) val timeOut: Time,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_BREAK) val breakMins: Int? = 0,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DURATION, typeAffinity = ColumnInfo.REAL, defaultValue = "0") val duration: Float = 0f,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TYPE) val type: String,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_UNIT, typeAffinity = ColumnInfo.REAL) val units: Float? = 0f,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_PAYRATE, typeAffinity = ColumnInfo.REAL) val payRate: Float? = 0f,
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TOTALPAY, typeAffinity = ColumnInfo.REAL) val totalPay: Float? = 0f,
@PrimaryKey
@ColumnInfo(name = ShiftsEntry._ID) val id: Long? = 0,
) {
fun convertToShiftObject(): ShiftObject {
return ShiftObject(
id ?: 0, type, description, date.toString(), timeIn.toString(), timeOut.toString(), duration, breakMins ?: 0, units ?: 0f, payRate ?: 0f, totalPay ?: 0f
)
}
}

View File

@@ -0,0 +1,14 @@
package com.appttude.h_mal.farmr.data.room.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS '" + ShiftsDbHelper.SQL_CREATE_PRODUCTS_TABLE_2 + "'");
database.execSQL("ALTER TABLE `shifts` ADD COLUMN `date` TEXT NOT NULL")
// CREATE TABLE IF NOT EXISTS (`description` TEXT NOT NULL, , `timein` TEXT NOT NULL, `timeout` TEXT NOT NULL, `break` INTEGER, `duration` REAL NOT NULL DEFAULT 0, `shifttype` TEXT NOT NULL, `unit` REAL, `payrate` REAL, `totalpay` REAL, `_id` INTEGER, PRIMARY KEY(`_id`))
}
}

View File

@@ -1,13 +1,15 @@
package com.appttude.h_mal.farmr.di package com.appttude.h_mal.farmr.di
import androidx.room.RoomDatabase
import com.appttude.h_mal.farmr.base.BaseApplication import com.appttude.h_mal.farmr.base.BaseApplication
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
class ShiftApplication: BaseApplication() { class ShiftApplication: BaseApplication() {
override fun createDatabase(): LegacyDatabase { override fun createDatabase(): AppDatabase {
return LegacyDatabase(contentResolver) return AppDatabase(this)
} }
override fun createPrefs() = PreferenceProvider(this) override fun createPrefs() = PreferenceProvider(this)

View File

@@ -1,11 +0,0 @@
package com.appttude.h_mal.farmr.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class EntityItem(
@PrimaryKey(autoGenerate = false)
val id: String,
val shift: Shift
)

View File

@@ -1,5 +1,8 @@
package com.appttude.h_mal.farmr.model package com.appttude.h_mal.farmr.model
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.utils.calculateDuration import com.appttude.h_mal.farmr.utils.calculateDuration
import com.appttude.h_mal.farmr.utils.formatToTwoDp import com.appttude.h_mal.farmr.utils.formatToTwoDp
@@ -15,6 +18,39 @@ data class Shift(
val rateOfPay: Float, val rateOfPay: Float,
val totalPay: Float val totalPay: Float
) { ) {
fun convertToShiftEntity(): ShiftEntity {
val timeConverter = TimeConverter()
return ShiftEntity(
description = description,
type = type.type,
date = DateConverter().toDate(date),
timeIn = timeConverter.toTime(timeIn),
timeOut = timeConverter.toTime(timeOut),
duration = duration ?: 0f,
breakMins = breakMins ?: 0,
units = units ?: 0f,
payRate = rateOfPay,
totalPay = totalPay
)
}
fun convertToShiftEntity(id: Long): ShiftEntity {
val timeConverter = TimeConverter()
return ShiftEntity(
id = id,
description = description,
type = type.type,
date = DateConverter().toDate(date),
timeIn = timeConverter.toTime(timeIn),
timeOut = timeConverter.toTime(timeOut),
duration = duration ?: 0f,
breakMins = breakMins ?: 0,
units = units ?: 0f,
payRate = rateOfPay,
totalPay = totalPay
)
}
companion object { companion object {
// Invocation for Hourly // Invocation for Hourly
operator fun invoke( operator fun invoke(
@@ -60,6 +96,4 @@ data class Shift(
(units * rateOfPay).formatToTwoDp() (units * rateOfPay).formatToTwoDp()
) )
} }
} }

View File

@@ -13,6 +13,8 @@ import androidx.core.widget.doAfterTextChanged
import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BackPressedListener
import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.base.BaseFragment
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.model.Success
import com.appttude.h_mal.farmr.utils.ID import com.appttude.h_mal.farmr.utils.ID
@@ -31,6 +33,9 @@ import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel
class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item), class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item),
RadioGroup.OnCheckedChangeListener, BackPressedListener { RadioGroup.OnCheckedChangeListener, BackPressedListener {
private val dateConverter = DateConverter()
private val timeConverter = TimeConverter()
private lateinit var mHourlyRadioButton: RadioButton private lateinit var mHourlyRadioButton: RadioButton
private lateinit var mPieceRadioButton: RadioButton private lateinit var mPieceRadioButton: RadioButton
private lateinit var mLocationEditText: EditText private lateinit var mLocationEditText: EditText
@@ -126,41 +131,41 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
// Since we are editing a shift lets load the shift data into the views // Since we are editing a shift lets load the shift data into the views
viewModel.getCurrentShift(arguments!!.getLong(ID))?.run { viewModel.getCurrentShift(arguments!!.getLong(ID))?.run {
mLocationEditText.setText(description) mLocationEditText.setText(description)
mDateEditText.setText(date) mDateEditText.setText(dateConverter.fromDate(date))
// Set types // Set types
mType = ShiftType.getEnumByType(type) mType = ShiftType.getEnumByType(type)
mDescription = description mDescription = description
mDate = date mDate = dateConverter.fromDate(date)
mPayRate = rateOfPay mPayRate = payRate!!
when (ShiftType.getEnumByType(type)) { when (ShiftType.getEnumByType(type)) {
ShiftType.HOURLY -> { ShiftType.HOURLY -> {
mHourlyRadioButton.isChecked = true mHourlyRadioButton.isChecked = true
mPieceRadioButton.isChecked = false mPieceRadioButton.isChecked = false
mTimeInEditText.setText(timeIn) mTimeInEditText.setText(timeConverter.fromTime(timeIn))
mTimeOutEditText.setText(timeOut) mTimeOutEditText.setText(timeConverter.fromTime(timeOut))
mBreakEditText.setText(breakMins.toString()) mBreakEditText.setText(breakMins.toString())
val durationText = "${duration.formatToTwoDpString()} Hours" val durationText = "${duration.formatToTwoDpString()} Hours"
mDurationTextView.text = durationText mDurationTextView.text = durationText
// Set fields // Set fields
mTimeIn = timeIn mTimeIn = timeConverter.fromTime(timeIn)
mTimeOut = timeOut mTimeOut = timeConverter.fromTime(timeOut)
mBreaks = breakMins mBreaks = breakMins
} }
ShiftType.PIECE -> { ShiftType.PIECE -> {
mHourlyRadioButton.isChecked = false mHourlyRadioButton.isChecked = false
mPieceRadioButton.isChecked = true mPieceRadioButton.isChecked = true
mUnitEditText.setText(units.formatToTwoDpString()) mUnitEditText.setText(units?.formatToTwoDpString())
// Set piece rate units // Set piece rate units
mUnits = units mUnits = units
} }
} }
mPayRateEditText.setText(rateOfPay.formatAsCurrencyString()) mPayRateEditText.setText(payRate.formatAsCurrencyString())
mTotalPayTextView.text = totalPay.formatAsCurrencyString() mTotalPayTextView.text = totalPay?.formatAsCurrencyString()
calculateTotalPay() calculateTotalPay()
} }

View File

@@ -1,7 +1,6 @@
package com.appttude.h_mal.farmr.viewmodel package com.appttude.h_mal.farmr.viewmodel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
@@ -16,6 +15,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Sortable
@@ -37,25 +37,28 @@ class MainViewModel(
private val repository: Repository private val repository: Repository
) : ShiftViewModel(repository) { ) : ShiftViewModel(repository) {
private val _shiftLiveData = MutableLiveData<List<ShiftObject>>() private val shiftLiveData: LiveData<List<ShiftEntity>> = repository.readShiftsFromDatabase()
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
private var mSort: Sortable = Sortable.ID private var mSort: Sortable = Sortable.ID
private var mOrder: Order = Order.ASCENDING private var mOrder: Order = Order.ASCENDING
private val observer = Observer<List<ShiftObject>> { private val observer = Observer<List<ShiftEntity>> {
it?.let { it?.let { updateFiltrationAndPostResults(it) }
val result = it.applyFilters().sortList(mSort, mOrder)
onSuccess(result)
}
} }
init { init {
// Load shifts into live data when view model has been instantiated
refreshLiveData()
shiftLiveData.observeForever(observer) shiftLiveData.observeForever(observer)
} }
private fun updateFiltrationAndPostResults(entities: List<ShiftEntity>) {
val result = entities.mapToShiftObjects().applyFilters().sortList(mSort, mOrder)
onSuccess(result)
}
private fun List<ShiftEntity>.mapToShiftObjects(): List<ShiftObject> {
return map { i -> i.convertToShiftObject() }
}
private fun List<ShiftObject>.applyFilters(): List<ShiftObject> { private fun List<ShiftObject>.applyFilters(): List<ShiftObject> {
val filter = getFiltrationDetails() val filter = getFiltrationDetails()
@@ -75,14 +78,21 @@ class MainViewModel(
} }
} }
private fun comparedStringsContains(first: String?, second: String?): Boolean { /*
first?.let { * Check if string compareWith contains compareAgainst
(second?.contains(it))?.let { c -> return c } */
private fun comparedStringsContains(compareWith: String?, compareAgainst: String?): Boolean {
compareWith?.let {
(compareAgainst?.contains(it))?.let { c -> return c }
} }
return comparedStrings(first, second) return comparedStrings(compareWith, compareAgainst)
} }
/*
* check if date [compareWith] fall between two dates
* if fromDate or toDate is null then it will take today's date for comparison
*/
private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? { private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? {
val first = fromDate?.convertDateString() val first = fromDate?.convertDateString()
val second = toDate?.convertDateString() val second = toDate?.convertDateString()
@@ -96,7 +106,9 @@ class MainViewModel(
return compareDate.after(first) && compareDate.before(second) return compareDate.after(first) && compareDate.before(second)
} }
/*
* When view-model is cleared we stop observing any livedata
*/
override fun onCleared() { override fun onCleared() {
shiftLiveData.removeObserver(observer) shiftLiveData.removeObserver(observer)
super.onCleared() super.onCleared()
@@ -132,7 +144,7 @@ class MainViewModel(
var totalUnits = 0f var totalUnits = 0f
var totalPay = 0f var totalPay = 0f
var lines = 0 var lines = 0
_shiftLiveData.value?.applyFilters()?.forEach { shiftLiveData.value?.mapToShiftObjects()?.applyFilters()?.forEach {
lines += 1 lines += 1
totalDuration += it.duration totalDuration += it.duration
when (ShiftType.getEnumByType(it.type)) { when (ShiftType.getEnumByType(it.type)) {
@@ -156,16 +168,12 @@ class MainViewModel(
fun deleteShift(id: Long) { fun deleteShift(id: Long) {
if (!repository.deleteSingleShiftFromDatabase(id)) { if (!repository.deleteSingleShiftFromDatabase(id)) {
onError("Failed to delete shift") onError("Failed to delete shift")
} else {
refreshLiveData()
} }
} }
fun deleteAllShifts() { fun deleteAllShifts() {
if (!repository.deleteAllShiftsFromDatabase()) { if (!repository.deleteAllShiftsFromDatabase()) {
onError("Failed to delete all shifts from database") onError("Failed to delete all shifts from database")
} else {
refreshLiveData()
} }
} }
@@ -194,8 +202,12 @@ class MainViewModel(
return stringBuilder.toString() return stringBuilder.toString()
} }
/*
* After operations such as updating filtering or sorting
* we update the data we sent to the ui
*/
fun refreshLiveData() { fun refreshLiveData() {
repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) } shiftLiveData.value?.let { updateFiltrationAndPostResults(it) }
} }
fun clearFilters() { fun clearFilters() {
@@ -204,6 +216,9 @@ class MainViewModel(
refreshLiveData() refreshLiveData()
} }
/*
* Build a .csv file to be exported
*/
fun createExcelSheet(file: File): File? { fun createExcelSheet(file: File): File? {
val wbSettings = WorkbookSettings().apply { val wbSettings = WorkbookSettings().apply {
locale = Locale("en", "EN") locale = Locale("en", "EN")
@@ -232,7 +247,7 @@ class MainViewModel(
return null return null
} }
val sortAndOrder = getSortAndOrder() val sortAndOrder = getSortAndOrder()
val data = shiftLiveData.value!!.applyFilters() val data = shiftLiveData.value!!.mapToShiftObjects().applyFilters()
.sortList(sortAndOrder.first, sortAndOrder.second) .sortList(sortAndOrder.first, sortAndOrder.second)
var currentRow = 0 var currentRow = 0
val cells = data.map { shift -> val cells = data.map { shift ->

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.farmr.viewmodel package com.appttude.h_mal.farmr.viewmodel
import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.model.Success
@@ -17,6 +19,9 @@ class SubmissionViewModel(
private val repository: Repository private val repository: Repository
) : ShiftViewModel(repository) { ) : ShiftViewModel(repository) {
private val dateConverter = DateConverter()
private val timeConverter = TimeConverter()
fun insertHourlyShift( fun insertHourlyShift(
description: String, description: String,
date: String, date: String,
@@ -26,7 +31,7 @@ class SubmissionViewModel(
breakMins: Int?, breakMins: Int?,
) { ) {
// Validate inputs from the edit texts // Validate inputs from the edit texts
(description.length > 3).validateField { (description.trim().length > 3).validateField {
onError("Description length should be longer") onError("Description length should be longer")
return return
} }
@@ -53,7 +58,7 @@ class SubmissionViewModel(
val result = insertShiftIntoDatabase( val result = insertShiftIntoDatabase(
ShiftType.HOURLY, ShiftType.HOURLY,
description, description.trim(),
date, date,
rateOfPay.formatToTwoDp(), rateOfPay.formatToTwoDp(),
timeIn, timeIn,
@@ -73,7 +78,7 @@ class SubmissionViewModel(
rateOfPay: Float rateOfPay: Float
) { ) {
// Validate inputs from the edit texts // Validate inputs from the edit texts
(description.length > 3).validateField { (description.trim().length > 3).validateField {
onError("Description length should be longer") onError("Description length should be longer")
return return
} }
@@ -92,7 +97,7 @@ class SubmissionViewModel(
val result = insertShiftIntoDatabase( val result = insertShiftIntoDatabase(
type = ShiftType.PIECE, type = ShiftType.PIECE,
description = description, description = description.trim(),
date = date, date = date,
rateOfPay = rateOfPay.formatToTwoDp(), rateOfPay = rateOfPay.formatToTwoDp(),
null, null,
@@ -177,72 +182,72 @@ class SubmissionViewModel(
breakMins: Int? = null, breakMins: Int? = null,
units: Float? = null, units: Float? = null,
): Boolean { ): Boolean {
val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() val currentShift = repository.readSingleShiftFromDatabase(id)
?: throw IOException("Cannot update shift as it does not exist") ?: throw IOException("Cannot update shift as it does not exist")
val mDate = date ?: dateConverter.fromDate(currentShift.date)
val mTimeIn = timeIn ?: timeConverter.fromTime(currentShift.timeIn)
val mTimeOut = timeOut ?: timeConverter.fromTime(currentShift.timeIn)
val shift = when (type) { val shift = when (type) {
ShiftType.HOURLY -> { ShiftType.HOURLY -> {
// Shift type has changed so mandatory fields for hourly shift are now required as well // Shift type has changed so mandatory fields for hourly shift are now required as well
val insertTimeIn =
(timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted")
val insertTimeOut =
(timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted")
Shift( Shift(
description = description ?: currentShift.description, description = description ?: currentShift.description,
date = date ?: currentShift.date, date = mDate,
timeIn = insertTimeIn, timeIn = mTimeIn,
timeOut = insertTimeOut, timeOut = mTimeOut,
breakMins = breakMins ?: currentShift.breakMins, breakMins = breakMins ?: currentShift.breakMins,
rateOfPay = rateOfPay ?: currentShift.rateOfPay rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
) )
} }
ShiftType.PIECE -> { ShiftType.PIECE -> {
// Shift type has changed so mandatory fields for piece rate shift are now required as well // Shift type has changed so mandatory fields for piece rate shift are now required as well
val insertUnits = (units ?: currentShift.units)
?: throw IOException("Units must be inserted for piece rate shifts")
Shift( Shift(
description = description ?: currentShift.description, description = description ?: currentShift.description,
date = date ?: currentShift.date, date = mDate,
units = insertUnits, units = (units ?: currentShift.units) ?: 0f,
rateOfPay = rateOfPay ?: currentShift.rateOfPay rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
) )
} }
else -> { else -> {
if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) {
// Updates to description or date field // Updates to description or date field
currentShift.copy( Shift(
description = description ?: currentShift.description, ShiftType.getEnumByType(currentShift.type),
date = date ?: currentShift.date, description ?: currentShift.description,
mDate,
mTimeIn,
mTimeOut,
currentShift.duration,
currentShift.breakMins,
currentShift.units,
currentShift.payRate ?: 0f,
currentShift.totalPay ?: 0f
) )
} else { } else {
// Updating shifts where shift type has remained the same // Updating shifts where shift type has remained the same
when (currentShift.type) { when (ShiftType.getEnumByType(currentShift.type)) {
ShiftType.HOURLY -> { ShiftType.HOURLY -> {
val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException(
"No time in inserted"
)
val insertTimeOut = (timeOut ?: currentShift.timeOut)
?: throw IOException("No time out inserted")
Shift( Shift(
description = description ?: currentShift.description, description = description ?: currentShift.description,
date = date ?: currentShift.date, date = mDate,
timeIn = insertTimeIn, timeIn = mTimeIn,
timeOut = insertTimeOut, timeOut = mTimeOut,
breakMins = breakMins ?: currentShift.breakMins, breakMins = breakMins ?: currentShift.breakMins,
rateOfPay = rateOfPay ?: currentShift.rateOfPay rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
) )
} }
ShiftType.PIECE -> { ShiftType.PIECE -> {
val insertUnits = (units ?: currentShift.units) val insertUnits = (units ?: currentShift.units)
?: throw IOException("Units must be inserted for piece rate shifts")
Shift( Shift(
description = description ?: currentShift.description, description = description ?: currentShift.description,
date = date ?: currentShift.date, date = mDate,
units = insertUnits, units = (insertUnits) ?: 0f,
rateOfPay = rateOfPay ?: currentShift.rateOfPay rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
) )
} }
} }

View File

@@ -1,24 +1,38 @@
package com.appttude.h_mal.farmr.data package com.appttude.h_mal.farmr.data
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.data.room.AppDatabase
import com.appttude.h_mal.farmr.data.room.ShiftDao
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.Shift
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyLong
import java.io.IOException
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs import kotlin.test.assertIs
class RepositoryImplTest { class RepositoryImplTest {
@get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var repository: RepositoryImpl private lateinit var repository: RepositoryImpl
@MockK @RelaxedMockK
lateinit var db: LegacyDatabase lateinit var db: AppDatabase
@MockK @MockK
lateinit var prefs: PreferenceProvider lateinit var prefs: PreferenceProvider
@@ -32,20 +46,123 @@ class RepositoryImplTest {
@Test @Test
fun readDatabase_validResponse() { fun readDatabase_validResponse() {
// Arrange // Arrange
val elements = listOf<ShiftObject>( val liveData = mockk<LiveData<List<ShiftEntity>>>()
mockk { every { id } returns anyLong() },
mockk { every { id } returns anyLong() },
mockk { every { id } returns anyLong() },
mockk { every { id } returns anyLong() }
)
//Act //Act
every { db.readShiftsFromDatabase() } returns elements every { db.getShiftDao().getAllFullShift() } returns liveData
// Assert // Assert
val result = repository.readShiftsFromDatabase() val result = repository.readShiftsFromDatabase()
assertIs<List<ShiftObject>>(result) assertIs<LiveData<List<ShiftEntity>>>(result)
assertEquals(result.first().id, anyLong()) assertEquals(result, liveData)
}
@Test
fun updateShift_invalidDescription_validThrow() {
// Arrange
val id = anyLong()
val shift = mockk<Shift>()
//Act
val emptyDescExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedDescExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "DQ "
repository.updateShiftIntoDatabase(id, shift)
}
// Assert
assertEquals(emptyDescExceptionReturned.message, "description required")
assertEquals(untrimmedDescExceptionReturned.message, "description required")
}
@Test
fun updateShift_invalidDate_validThrow() {
// Arrange
val id = anyLong()
val shift = mockk<Shift>()
//Act
every { shift.description } returns "Valid desc"
val emptyDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.date } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "2022-03-02 "
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "02-03-2020"
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatDateExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
every { shift.description } returns "2022/03/02"
repository.updateShiftIntoDatabase(id, shift)
}
// Assert
assertEquals(emptyDateExceptionReturned.message, "date required")
assertEquals(untrimmedDateExceptionReturned.message, "date required")
assertEquals(wrongFormatDateExceptionReturned.message, "date required")
assertEquals(wrongFormatDateExceptionReturned2.message, "date required")
}
@Test
fun updateShift_invalidTimeInAndTimeOut_validThrow() {
// Arrange
val id = anyLong()
val shift = mockk<Shift>()
//Act
every { shift.description } returns "Valid desc"
every { shift.date } returns "2020-06-05"
val emptyTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns "14:04 "
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns "14 04"
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeInExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
every { shift.timeIn } returns "1404"
repository.updateShiftIntoDatabase(id, shift)
}
every { shift.timeIn } returns "14:00"
val emptyTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns ""
repository.updateShiftIntoDatabase(id, shift)
}
val untrimmedTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns "14:04 "
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns "14 04"
repository.updateShiftIntoDatabase(id, shift)
}
val wrongFormatTimeOutExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
every { shift.timeOut } returns "1404"
repository.updateShiftIntoDatabase(id, shift)
}
// Assert
assertEquals(emptyTimeInExceptionReturned.message, "time in required")
assertEquals(untrimmedTimeInExceptionReturned.message, "time in required")
assertEquals(wrongFormatTimeInExceptionReturned.message, "time in required")
assertEquals(wrongFormatTimeInExceptionReturned2.message, "time in required")
assertEquals(emptyTimeOutExceptionReturned.message, "time out required")
assertEquals(untrimmedTimeOutExceptionReturned.message, "time out required")
assertEquals(wrongFormatTimeOutExceptionReturned.message, "time out required")
assertEquals(wrongFormatTimeOutExceptionReturned2.message, "time out required")
} }
} }

View File

@@ -0,0 +1,26 @@
package com.appttude.h_mal.farmr.data.room.converters
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.sql.Time
class TimeConverterTest {
private val converter = TimeConverter()
@Test
fun toTime() {
val str = "16:04"
val time = converter.toTime(str)
assertEquals(time.toString(), "$str:00")
}
@Test
fun fromTime() {
val str = "16:04"
val time = converter.fromTime(Time.valueOf("16:04:00"))
assertEquals(time, str)
}
}

View File

@@ -2,7 +2,7 @@ package com.appttude.h_mal.farmr.utils
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -42,108 +42,108 @@ fun sleep(millis: Long = 1000) {
} }
fun getShifts() = listOf( fun getShifts() = listOf(
ShiftObject( ShiftEntity(
ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type,
"Day one", "Day one",
"2023-08-01", "2023-08-01",
"12:00", "12:00",
"13:00", "13:00",
1f,
ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(),
1f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
10f, 10f,
10f 10f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type, ),
ShiftEntity(
"Day two", "Day two",
"2023-08-02", "2023-08-02",
"12:00", "12:00",
"13:00", "13:00",
1f,
ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(),
1f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
10f, 10f,
10f 10f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type, ),
ShiftEntity(
"Day three", "Day three",
"2023-08-03", "2023-08-03",
"12:00", "12:00",
"13:00", "14:30",
1f,
30, 30,
2f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
10f, 10f,
5f 20f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.HOURLY.type, ),
ShiftEntity(
"Day four", "Day four",
"2023-08-04", "2023-08-04",
"12:00", "12:00",
"13:00", "14:30",
1f,
30, 30,
2f,
ShiftType.HOURLY.type,
ArgumentMatchers.anyFloat(), ArgumentMatchers.anyFloat(),
10f, 10f,
5f 20f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.PIECE.type, ),
ShiftEntity(
"Day five", "Day five",
"2023-08-05", "2023-08-05",
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f, 1f,
10f, 10f,
10f 10f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.PIECE.type, ),
ShiftEntity(
"Day six", "Day six",
"2023-08-06", "2023-08-06",
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f, 1f,
10f, 10f,
10f 10f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.PIECE.type, ),
ShiftEntity(
"Day seven", "Day seven",
"2023-08-07", "2023-08-07",
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f, 1f,
10f, 10f,
10f 10f,
),
ShiftObject(
ArgumentMatchers.anyLong(), ArgumentMatchers.anyLong(),
ShiftType.PIECE.type, ),
ShiftEntity(
"Day eight", "Day eight",
"2023-08-08", "2023-08-08",
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(), ArgumentMatchers.anyString(),
ArgumentMatchers.anyFloat(),
ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(),
ArgumentMatchers.anyFloat(),
ShiftType.PIECE.type,
1f, 1f,
10f, 10f,
10f 10f,
ArgumentMatchers.anyLong(),
), ),
) )

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.farmr.viewmodel
import android.os.Bundle import android.os.Bundle
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.utils.ID import com.appttude.h_mal.farmr.utils.ID
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@@ -16,7 +17,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
fun retrieveData_validBundleAndId_successfulRetrieval() { fun retrieveData_validBundleAndId_successfulRetrieval() {
// Arrange // Arrange
val id = anyLong() val id = anyLong()
val shift = mockk<ShiftObject>() val shift = mockk<ShiftEntity>()
val bundle = mockk<Bundle>() val bundle = mockk<Bundle>()
// Act // Act
@@ -25,7 +26,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
viewModel.retrieveData(bundle) viewModel.retrieveData(bundle)
// Assert // Assert
assertIs<ShiftObject>(retrieveCurrentData()) assertIs<ShiftEntity>(retrieveCurrentData())
assertEquals( assertEquals(
retrieveCurrentData(), retrieveCurrentData(),
shift shift
@@ -36,7 +37,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() { fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() {
// Arrange // Arrange
val id = anyLong() val id = anyLong()
val shift = mockk<ShiftObject>() val shift = mockk<ShiftEntity>()
val bundle = mockk<Bundle>() val bundle = mockk<Bundle>()
// Act // Act

View File

@@ -1,17 +1,23 @@
package com.appttude.h_mal.farmr.viewmodel package com.appttude.h_mal.farmr.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.prefs.DATE_IN import com.appttude.h_mal.farmr.data.prefs.DATE_IN
import com.appttude.h_mal.farmr.data.prefs.DATE_OUT import com.appttude.h_mal.farmr.data.prefs.DATE_OUT
import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION
import com.appttude.h_mal.farmr.data.prefs.TYPE import com.appttude.h_mal.farmr.data.prefs.TYPE
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.model.ViewState
import com.appttude.h_mal.farmr.utils.getOrAwaitValue import com.appttude.h_mal.farmr.utils.getOrAwaitValue
import com.appttude.h_mal.farmr.utils.getShifts import com.appttude.h_mal.farmr.utils.getShifts
import io.mockk.MockKAnnotations
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.mockk import io.mockk.mockk
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
import org.junit.Before import org.junit.Before
@@ -31,7 +37,10 @@ class MainViewModelTest {
@Before @Before
fun setUp() { fun setUp() {
repository = mockk() repository = mockk()
every { repository.readShiftsFromDatabase() }.returns(null)
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
every { repository.readShiftsFromDatabase() }.returns(liveData)
every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter())
viewModel = MainViewModel(repository) viewModel = MainViewModel(repository)
} }
@@ -45,47 +54,60 @@ class MainViewModelTest {
@Test @Test
fun getShiftsFromRepository_liveDataIsShown() { fun getShiftsFromRepository_liveDataIsShown() {
// Arrange // Arrange
val listOfShifts = anyList<ShiftObject>() val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val shifts = anyList<ShiftEntity>()
mutableLiveData.postValue(shifts)
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
// Act // Act
every { repository.readShiftsFromDatabase() }.returns(listOfShifts) every { repository.readShiftsFromDatabase() }.returns(liveData)
viewModel = MainViewModel(repository)
viewModel.refreshLiveData() viewModel.refreshLiveData()
// Assert // Assert
assertEquals(retrieveCurrentData(), listOfShifts) assertEquals(retrieveCurrentData(), shifts)
} }
@Test @Test
fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() { fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() {
// Arrange // Arrange
val listOfShifts = getShifts() val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val shifts = getShifts()
mutableLiveData.postValue(shifts)
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
// Act // Act
every { repository.readShiftsFromDatabase() }.returns(listOfShifts) every { repository.readShiftsFromDatabase() }.returns(liveData)
viewModel = MainViewModel(repository)
viewModel.refreshLiveData() viewModel.refreshLiveData()
val retrievedShifts = retrieveCurrentData() val retrievedShifts = retrieveCurrentData()
val description = viewModel.getInformation() val description = viewModel.getInformation()
// Assert // Assert
assertEquals(retrievedShifts, listOfShifts) assertEquals(retrievedShifts, shifts.map { it.convertToShiftObject() })
assertEquals( assertEquals(
description, "8 Shifts\n" + description, "8 Shifts\n" +
" (4 Hourly/4 Piece Rate)\n" + " (4 Hourly/4 Piece Rate)\n" +
"Total Hours: 4.0\n" + "Total Hours: 6.0\n" +
"Total Units: 4.0\n" + "Total Units: 4.0\n" +
"Total Pay: £70.00" "Total Pay: £100.00"
) )
} }
@Test @Test
fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() { fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() {
// Arrange // Arrange
val listOfShifts = getShifts() val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
val shifts = getShifts()
mutableLiveData.postValue(shifts)
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type } val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type }
// Act // Act
every { repository.readShiftsFromDatabase() }.returns(listOfShifts) every { repository.readShiftsFromDatabase() }.returns(liveData)
every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type)) every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type))
viewModel = MainViewModel(repository)
viewModel.refreshLiveData() viewModel.refreshLiveData()
val retrievedShifts = retrieveCurrentData() val retrievedShifts = retrieveCurrentData()
val description = viewModel.getInformation() val description = viewModel.getInformation()
@@ -96,18 +118,18 @@ class MainViewModelTest {
val descriptionAfterClearedFilter = viewModel.getInformation() val descriptionAfterClearedFilter = viewModel.getInformation()
// Assert // Assert
assertEquals(retrievedShifts, filteredShifts) assertEquals(retrievedShifts, filteredShifts.map { it.convertToShiftObject() })
assertEquals( assertEquals(
description, "4 Shifts\n" + description, "4 Shifts\n" +
"Total Hours: 4.0\n" + "Total Hours: 6.0\n" +
"Total Pay: £30.00" "Total Pay: £60.00"
) )
assertEquals( assertEquals(
descriptionAfterClearedFilter, "8 Shifts\n" + descriptionAfterClearedFilter, "8 Shifts\n" +
" (4 Hourly/4 Piece Rate)\n" + " (4 Hourly/4 Piece Rate)\n" +
"Total Hours: 4.0\n" + "Total Hours: 6.0\n" +
"Total Units: 4.0\n" + "Total Units: 4.0\n" +
"Total Pay: £70.00" "Total Pay: £100.00"
) )
} }