diff --git a/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt b/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt deleted file mode 100644 index ef6f3a2..0000000 --- a/app/src/androidTest/java/me/zobrist/tichucounter/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package me.zobrist.tichucounter - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -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 ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("me.zobrist.tichucounter", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt b/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt new file mode 100644 index 0000000..7620a3b --- /dev/null +++ b/app/src/androidTest/java/me/zobrist/tichucounter/RepositoryInstrumentedTest.kt @@ -0,0 +1,280 @@ +package me.zobrist.tichucounter + +import android.content.Context +import androidx.compose.runtime.collectAsState +import androidx.lifecycle.asLiveData +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import me.zobrist.tichucounter.data.AppDatabase +import me.zobrist.tichucounter.data.GameDao +import me.zobrist.tichucounter.data.GameWithScores +import me.zobrist.tichucounter.data.RoundDao +import me.zobrist.tichucounter.repository.GameRepository +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.util.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class RepositoryInstrumentedTest { + private lateinit var gameDao: GameDao + private lateinit var roundDao: RoundDao + private lateinit var repository: GameRepository + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java).build() + roundDao = db.roundDao() + gameDao = db.gameDao() + + repository = GameRepository(gameDao, roundDao) + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun gameInitialisation() = runTest { + repository.getActiveGameFlow().take(1).collect { + assertEquals("TeamA", it.game.nameA) + assertEquals("TeamB", it.game.nameB) + assertTrue(it.game.active) + assertEquals(0, it.rounds.count()) + } + } + + @Test + @Throws(Exception::class) + fun modifyNames() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + repository.updateActiveTeamName(nameA ="aaa") + + repository.getActiveGameFlow().take(1).collect { + assertEquals("aaa", it.game.nameA) + assertEquals("TeamB", it.game.nameB) + } + + repository.updateActiveTeamName(nameB ="bbb") + + repository.getActiveGameFlow().take(1).collect { + assertEquals("aaa", it.game.nameA) + assertEquals("bbb", it.game.nameB) + } + + + } + + + + @Test + @Throws(Exception::class) + fun newGame() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + + + repository.getAllWithRoundFlow().take(1).collect() { it -> + assertEquals(6, it.count()) + + var uid: Long = 1 + it.forEach {game -> + assertEquals(uid++, game.game.uid) + assertEquals(0, game.rounds.count()) + } + } + } + + @Test + @Throws(Exception::class) + fun setActive() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + + + repository.getAllWithRoundFlow().take(1).collect() { it -> + val filtered = it.filter { it.game.active } + assertEquals(1, filtered.count()) + assertEquals(6, filtered.first().game.uid) + } + + repository.setActive(2) + + repository.getAllWithRoundFlow().take(1).collect() { it -> + val filtered = it.filter { it.game.active } + assertEquals(1, filtered.count()) + assertEquals(2, filtered.first().game.uid) + } + } + + @Test + @Throws(Exception::class) + fun addRoundToActiveGame() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + + + repository.addRoundToActiveGame(1,1) + repository.addRoundToActiveGame(2,2) + repository.addRoundToActiveGame(3,3) + repository.addRoundToActiveGame(4,4) + repository.addRoundToActiveGame(5,5) + repository.addRoundToActiveGame(6,6) + + + repository.getAllWithRoundFlow().take(1).collect() { it -> + val filtered = it.filter { it.rounds.isNotEmpty() } + assertEquals(1, filtered.count()) + assertEquals(6, filtered.first().rounds.count()) + } + } + + @Test + @Throws(Exception::class) + fun lastRound() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + repository.newGame() + + assertNull(repository.getLastRound()) + + repository.addRoundToActiveGame(1,1) + repository.addRoundToActiveGame(2,2) + repository.addRoundToActiveGame(3,3) + repository.addRoundToActiveGame(4,4) + repository.addRoundToActiveGame(5,5) + repository.addRoundToActiveGame(6,6) + + var lastRound = repository.getLastRound() + assertEquals(6, lastRound?.scoreA) + assertEquals(6, lastRound?.scoreB) + + repository.deleteLastRound() + + lastRound = repository.getLastRound() + assertEquals(5, lastRound?.scoreA) + assertEquals(5, lastRound?.scoreB) + + repository.deleteLastRound() + repository.deleteLastRound() + repository.deleteLastRound() + repository.deleteLastRound() + repository.deleteLastRound() + + assertNull(repository.getLastRound()) + + // No error thrown + repository.deleteLastRound() + } + + @Test + @Throws(Exception::class) + fun deleteInactive() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + for (i in 1..6) { + repository.newGame() + repository.addRoundToActiveGame(1,1) + repository.addRoundToActiveGame(2,2) + repository.addRoundToActiveGame(3,3) + repository.addRoundToActiveGame(4,4) + repository.addRoundToActiveGame(5,5) + repository.addRoundToActiveGame(6,6) + } + assertEquals(6 * 6, roundDao.getAll().count()) + + repository.deleteAllInactive() + + // Consists of two transactions. Delete games then delete rounds. + repository.getAllWithRoundFlow().take(1).collect() { it -> + assertEquals(1, it.count()) + assertEquals(6, it.first().rounds.count()) + } + assertEquals(6, roundDao.getAll().count()) + } + + @Test + @Throws(Exception::class) + fun deleteById() = runTest { + + repository.getActiveGameFlow().take(1).collect { + } + + for (i in 1..6) { + repository.newGame() + repository.addRoundToActiveGame(1,1) + repository.addRoundToActiveGame(2,2) + repository.addRoundToActiveGame(3,3) + repository.addRoundToActiveGame(4,4) + repository.addRoundToActiveGame(5,5) + repository.addRoundToActiveGame(6,6) + } + + // Non existing Id + repository.deleteGame(10) + + repository.getAllWithRoundFlow().take(1).collect() { it -> + assertEquals(7, it.count()) + } + + // Non existing Id + val toDelete: Long = 3 + repository.deleteGame(toDelete) + + repository.getAllWithRoundFlow().take(1).collect() { it -> + assertEquals(6, it.count()) + assertEquals(0, it.count { it.game.uid == toDelete }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt b/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt index 5f4a9c3..74cad1c 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/DaoBase.kt @@ -1,9 +1,6 @@ package me.zobrist.tichucounter.data -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Update +import androidx.room.* @Dao interface DaoBase { diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt index 00a9855..634b3cb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt @@ -12,7 +12,7 @@ interface GameDao : DaoBase { fun getAll(): Flow> @Transaction - @Query("SELECT * FROM game where uid ") + @Query("SELECT * FROM game") fun getGamesWithRounds(): Flow> @Transaction @@ -20,10 +20,13 @@ interface GameDao : DaoBase { fun getActiveWithRounds(): Flow @Query("SELECT * FROM game WHERE uid is :gameId") - fun getGameById(gameId: Long): Flow + fun getGameById(gameId: Long): Game @Query("SELECT * FROM game WHERE active is 1") - fun getActive(): Flow + fun getActiveAsFlow(): Flow + + @Query("SELECT * FROM game WHERE active is 1") + fun getActive(): Game? @Query("UPDATE game SET active = 1 WHERE uid is :gameId;") diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt index 44d2d54..463f6ac 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt @@ -5,13 +5,14 @@ import androidx.room.Entity import androidx.room.Relation import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round +import java.util.* @Entity data class GameWithScores( - @Embedded val game: Game, + @Embedded val game: Game = Game(), @Relation( parentColumn = "uid", entityColumn = "gameId" ) - val rounds: List + val rounds: List = emptyList() ) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt index e70a2de..314c8af 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt @@ -6,10 +6,10 @@ import java.util.* @Entity data class Game( - var active: Boolean, - var nameA: String, - var nameB: String, - val created: Date, - var modified: Date, + var active: Boolean = true, + var nameA: String = "TeamA", + var nameB: String = "TeamB", + val created: Date = Date(), + var modified: Date = Date(), @PrimaryKey(autoGenerate = true) override val uid: Long = 0 ) : IEntity \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt index 5716266..47082b6 100644 --- a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt +++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt @@ -1,11 +1,7 @@ package me.zobrist.tichucounter.repository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import me.zobrist.tichucounter.data.GameDao import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.RoundDao @@ -19,20 +15,15 @@ class GameRepository @Inject constructor( private val roundDao: RoundDao ) { - private var _activeGame: Game? = null - - val activeGame: Game - get() { - return _activeGame!! - } + private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date()) init { CoroutineScope(Dispatchers.IO).launch { - gameDao.getActive().collect { + gameDao.getActiveAsFlow().collect { if (it == null) { - gameDao.insert(Game(true, "TeamA", "TeamB", Date(), Date())) + newGame() } else { - _activeGame = it + activeGame = it } } } @@ -40,16 +31,25 @@ class GameRepository @Inject constructor( suspend fun newGame() { withContext(Dispatchers.IO) { - val id = - gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date())) + val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date())) setActive(id) } } - suspend fun updateGame(game: Game) { - game.modified = Date() + suspend fun updateActiveTeamName(nameA: String? = null, nameB: String? = null) { + + val newA = nameA ?: activeGame.nameA + val newB = nameB ?: activeGame.nameB + + if(newA == activeGame.nameA && newB == activeGame.nameB) { + return + } + + activeGame.modified = Date() + activeGame.nameA = newA + activeGame.nameB = newB withContext(Dispatchers.IO) { - gameDao.update(game) + gameDao.update(activeGame) } } @@ -92,11 +92,10 @@ class GameRepository @Inject constructor( suspend fun deleteGame(uid: Long) { withContext(Dispatchers.IO) { try { - gameDao.getGameById(uid).take(1).collect { - gameDao.delete(it) - val rounds = roundDao.getAllForGame(it.uid) - roundDao.delete(rounds) - } + val game = gameDao.getGameById(uid) + gameDao.delete(game) + val rounds = roundDao.getAllForGame(game.uid) + roundDao.delete(rounds) } catch (_: NullPointerException) { } } @@ -107,9 +106,8 @@ class GameRepository @Inject constructor( try { gameDao.getAll().take(1).collect { games -> - val activeId = games.first { it.active }.uid - val gamesToDelete = games.filter { !it.active } - val roundsToDelete = roundDao.getAll().filter { it.gameId != activeId } + val gamesToDelete = games.filter { it.uid != activeGame.uid } + val roundsToDelete = roundDao.getAll().filter { it.gameId != activeGame.uid } gameDao.delete(gamesToDelete) roundDao.delete(roundsToDelete) @@ -119,8 +117,8 @@ class GameRepository @Inject constructor( } } - fun getActiveGameFlow(): Flow { - return gameDao.getActiveWithRounds() + fun getActiveGameFlow(): Flow { + return gameDao.getActiveWithRounds().filter { it != null }.map { it!! } } fun getAllWithRoundFlow(): Flow> { diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt index 53fde44..5905bf0 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt @@ -34,17 +34,15 @@ class MainViewModel @Inject constructor( gameRepository.getActiveGameFlow().collect { - activeGameHasRounds = it?.rounds?.isNotEmpty() == true + activeGameHasRounds = it.rounds.isNotEmpty() == true - if (it != null) { - isUndoActionActive = it.rounds.isNotEmpty() + isUndoActionActive = it.rounds.isNotEmpty() - if (expectedRoundCount != it.rounds.count()) { - redoRounds.clear() - } - - expectedRoundCount = it.rounds.count() + if (expectedRoundCount != it.rounds.count()) { + redoRounds.clear() } + + expectedRoundCount = it.rounds.count() } } } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index e6ae019..38164e1 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -245,17 +245,13 @@ class CounterViewModel @Inject constructor( override fun updateNameA(value: String) { viewModelScope.launch { - val game = gameRepository.activeGame - game.nameA = value - gameRepository.updateGame(game) + gameRepository.updateActiveTeamName(nameA = value) } } override fun updateNameB(value: String) { viewModelScope.launch { - val game = gameRepository.activeGame - game.nameB = value - gameRepository.updateGame(game) + gameRepository.updateActiveTeamName(nameB = value) } }