Make I/O asynchronous with coroutines; make all JS tests pass

pull/498/head
Alinson S. Xavier 7 years ago
parent 8972f2d03d
commit f310eaf7d9

@ -53,6 +53,7 @@ kotlin {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.2.0-alpha-2'
}
}
@ -66,6 +67,7 @@ kotlin {
jvmMain {
dependencies {
implementation kotlin('stdlib-jdk8')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha-2'
}
}
@ -80,6 +82,7 @@ kotlin {
jsMain {
dependencies {
implementation kotlin('stdlib-js')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.2.0-alpha-2'
}
}

@ -80,7 +80,7 @@ fun Database.getVersion() = queryInt("pragma user_version")
fun Database.setVersion(v: Int) = run("pragma user_version = $v")
fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
suspend fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
val currentVersion = getVersion()
log.debug("Database", "Current database version: $currentVersion")
@ -92,9 +92,11 @@ fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
begin()
for (v in (currentVersion + 1)..newVersion) {
val filename = sprintf("migrations/%03d.sql", v)
val sv = if(v < 10) "00$v" else if (v<100) "0$v" else "$v"
val filename = "migrations/$sv.sql"
println(filename)
val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.readLines()) {
for (line in migrationFile.lines()) {
if (line.isEmpty()) continue
run(line)
}

@ -56,6 +56,6 @@ interface UserFile {
* files or translations. These files cannot be deleted.
*/
interface ResourceFile {
fun readLines(): List<String>
fun copyTo(dest: UserFile)
suspend fun lines(): List<String>
}

@ -29,11 +29,11 @@ interface Log {
*/
class StandardLog : Log {
override fun info(tag: String, msg: String) {
println(sprintf("I/%-20s %s", tag, msg))
println("I/$tag $msg")
}
override fun debug(tag: String, msg: String) {
println(sprintf("D/%-20s %s", tag, msg))
println("D/$tag $msg")
}
}

@ -26,82 +26,82 @@ import org.isoron.uhabits.components.*
import org.isoron.uhabits.i18n.*
import org.isoron.uhabits.models.*
class Backend(databaseName: String,
databaseOpener: DatabaseOpener,
fileOpener: FileOpener,
localeHelper: LocaleHelper,
val log: Log,
val taskRunner: TaskRunner) {
val database: Database
val habitsRepository: HabitRepository
val checkmarkRepository: CheckmarkRepository
val habits = mutableMapOf<Int, Habit>()
val checkmarks = mutableMapOf<Habit, CheckmarkList>()
val scores = mutableMapOf<Habit, ScoreList>()
val mainScreenDataSource: MainScreenDataSource
val strings = localeHelper.getStringsForCurrentLocale()
val preferences: Preferences
var theme: Theme = LightTheme()
init {
val dbFile = fileOpener.openUserFile(databaseName)
if (!dbFile.exists()) {
val templateFile = fileOpener.openResourceFile("databases/template.db")
templateFile.copyTo(dbFile)
}
database = databaseOpener.open(dbFile)
database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
preferences = Preferences(PreferencesRepository(database))
habitsRepository = HabitRepository(database)
checkmarkRepository = CheckmarkRepository(database)
taskRunner.runInBackground {
habits.putAll(habitsRepository.findAll())
for ((key, habit) in habits) {
val checks = checkmarkRepository.findAll(key)
checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
checkmarks[habit]?.setManualCheckmarks(checks)
scores[habit] = ScoreList(checkmarks[habit]!!)
}
}
mainScreenDataSource = MainScreenDataSource(preferences,
habits,
checkmarks,
scores,
taskRunner)
}
fun createHabit(habit: Habit) {
val id = habitsRepository.nextId()
habit.id = id
habit.position = habits.size
habits[id] = habit
checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
habitsRepository.insert(habit)
mainScreenDataSource.requestData()
}
fun deleteHabit(id: Int) {
habits[id]?.let { habit ->
habitsRepository.delete(habit)
habits.remove(id)
mainScreenDataSource.requestData()
}
}
fun updateHabit(modified: Habit) {
habits[modified.id]?.let { existing ->
modified.position = existing.position
habitsRepository.update(modified)
}
}
}
//class Backend(databaseName: String,
// databaseOpener: DatabaseOpener,
// fileOpener: FileOpener,
// localeHelper: LocaleHelper,
// val log: Log,
// val taskRunner: TaskRunner) {
//
// val database: Database
//
// val habitsRepository: HabitRepository
//
// val checkmarkRepository: CheckmarkRepository
//
// val habits = mutableMapOf<Int, Habit>()
//
// val checkmarks = mutableMapOf<Habit, CheckmarkList>()
//
// val scores = mutableMapOf<Habit, ScoreList>()
//
// val mainScreenDataSource: MainScreenDataSource
//
// val strings = localeHelper.getStringsForCurrentLocale()
//
// val preferences: Preferences
//
// var theme: Theme = LightTheme()
//
// init {
// val dbFile = fileOpener.openUserFile(databaseName)
// if (!dbFile.exists()) {
// val templateFile = fileOpener.openResourceFile("databases/template.db")
// templateFile.copyTo(dbFile)
// }
// database = databaseOpener.open(dbFile)
// database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
// preferences = Preferences(PreferencesRepository(database))
// habitsRepository = HabitRepository(database)
// checkmarkRepository = CheckmarkRepository(database)
// taskRunner.runInBackground {
// habits.putAll(habitsRepository.findAll())
// for ((key, habit) in habits) {
// val checks = checkmarkRepository.findAll(key)
// checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
// checkmarks[habit]?.setManualCheckmarks(checks)
// scores[habit] = ScoreList(checkmarks[habit]!!)
// }
// }
// mainScreenDataSource = MainScreenDataSource(preferences,
// habits,
// checkmarks,
// scores,
// taskRunner)
// }
//
// fun createHabit(habit: Habit) {
// val id = habitsRepository.nextId()
// habit.id = id
// habit.position = habits.size
// habits[id] = habit
// checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
// habitsRepository.insert(habit)
// mainScreenDataSource.requestData()
// }
//
// fun deleteHabit(id: Int) {
// habits[id]?.let { habit ->
// habitsRepository.delete(habit)
// habits.remove(id)
// mainScreenDataSource.requestData()
// }
// }
//
// fun updateHabit(modified: Habit) {
// habits[modified.id]?.let { existing ->
// modified.position = existing.position
// habitsRepository.update(modified)
// }
// }
//}

@ -22,5 +22,4 @@ package org.isoron
open class BaseTest {
val resolver = DependencyResolver()
val fileOpener = resolver.getFileOpener()
val db = resolver.getDatabase()
}

@ -24,7 +24,7 @@ import org.isoron.platform.io.*
expect class DependencyResolver() {
fun getFileOpener(): FileOpener
fun getDatabase(): Database
suspend fun getDatabase(): Database
fun createCanvas(width: Int, height: Int): Canvas
fun exportCanvas(canvas: Canvas, filename: String)
}

@ -23,8 +23,8 @@ import org.isoron.*
import kotlin.test.*
class DatabaseTest() : BaseTest() {
@Test
fun testUsage() {
suspend fun testUsage() {
val db = resolver.getDatabase()
db.setVersion(0)
assertEquals(0, db.getVersion())

@ -23,15 +23,14 @@ import org.isoron.*
import kotlin.test.*
class FilesTest() : BaseTest() {
@Test
fun testReadLines() {
suspend fun testLines() {
val hello = fileOpener.openResourceFile("hello.txt")
var lines = hello.readLines()
var lines = hello.lines()
assertEquals("Hello World!", lines[0])
assertEquals("This is a resource.", lines[1])
val migration = fileOpener.openResourceFile("migrations/012.sql")
lines = migration.readLines()
lines = migration.lines()
assertEquals("delete from Score", lines[0])
}
}

@ -25,8 +25,8 @@ import kotlin.test.*
class CheckmarkRepositoryTest : BaseTest() {
@Test
fun testCRUD() {
suspend fun testCRUD() {
val db = resolver.getDatabase()
val habitA = 10
var checkmarksA = listOf(Checkmark(LocalDate(2019, 1, 15), 100),
Checkmark(LocalDate(2019, 1, 7), 500),

@ -25,15 +25,9 @@ import org.isoron.platform.io.*
import kotlin.test.*
class HabitRepositoryTest() : BaseTest() {
lateinit var repository: HabitRepository
lateinit private var original0: Habit
lateinit private var original1: Habit
lateinit private var original2: Habit
@BeforeTest
fun setUp() {
original0 = Habit(id = 0,
suspend fun testCRUD() {
val db = resolver.getDatabase()
val original0 = Habit(id = 0,
name = "Wake up early",
description = "Did you wake up before 6am?",
frequency = Frequency(1, 1),
@ -44,7 +38,7 @@ class HabitRepositoryTest() : BaseTest() {
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
original1 = Habit(id = 1,
val original1 = Habit(id = 1,
name = "Exercise",
description = "Did you exercise for at least 20 minutes?",
frequency = Frequency(1, 2),
@ -55,7 +49,7 @@ class HabitRepositoryTest() : BaseTest() {
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
original2 = Habit(id = 2,
val original2 = Habit(id = 2,
name = "Learn Japanese",
description = "Did you study Japanese today?",
frequency = Frequency(1, 1),
@ -66,11 +60,8 @@ class HabitRepositoryTest() : BaseTest() {
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
repository = HabitRepository(db)
}
val repository = HabitRepository(db)
@Test
fun testFindAll() {
var habits = repository.findAll()
assertEquals(0, repository.nextId())
assertEquals(0, habits.size)

@ -23,8 +23,8 @@ import org.isoron.*
import kotlin.test.*
class PreferencesRepositoryTest : BaseTest() {
@Test
fun testUsage() {
suspend fun testUsage() {
val db = resolver.getDatabase()
val prefs = PreferencesRepository(db)
assertEquals("default", prefs.getString("non_existing_key", "default"))

@ -29,6 +29,7 @@ class JsPreparedStatement(val stmt: dynamic) : PreparedStatement {
}
override fun finalize() {
stmt.free()
}
override fun getInt(index: Int): Int {

@ -19,8 +19,10 @@
package org.isoron.platform.io
import kotlinx.coroutines.*
import org.w3c.dom.events.*
import org.w3c.xhr.*
import kotlin.js.*
class JsFileOpener : FileOpener {
override fun openUserFile(filename: String): UserFile {
@ -34,21 +36,26 @@ class JsFileOpener : FileOpener {
class JsUserFile(filename: String) : UserFile {
override fun delete() {
TODO()
}
override fun exists(): Boolean {
return false
TODO()
}
}
class JsResourceFile(val filename: String) : ResourceFile {
override fun readLines(): List<String> {
override suspend fun lines(): List<String> {
return Promise<List<String>> { resolve, reject ->
val xhr = XMLHttpRequest()
xhr.open("GET", "/assets/$filename", false)
xhr.open("GET", "/assets/$filename", true)
xhr.onload = { resolve(xhr.responseText.lines()) }
xhr.onerror = { reject(Exception()) }
xhr.send()
return xhr.responseText.lines()
}.await()
}
override fun copyTo(dest: UserFile) {
TODO()
}
}

@ -21,15 +21,18 @@ package org.isoron
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.uhabits.*
import org.w3c.dom.*
import kotlin.browser.*
actual class DependencyResolver {
actual fun getFileOpener(): FileOpener = JsFileOpener()
actual fun getDatabase(): Database {
val db = eval("new SQL.Database()")
return JsDatabase(db)
actual suspend fun getDatabase(): Database {
val nativeDB = eval("new SQL.Database()")
val db = JsDatabase(nativeDB)
db.migrateTo(LOOP_DATABASE_VERSION, getFileOpener(), StandardLog())
return db
}
actual fun createCanvas(width: Int, height: Int): Canvas {

@ -0,0 +1,42 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron
import kotlinx.coroutines.*
import org.isoron.platform.io.*
import org.isoron.uhabits.models.*
import kotlin.test.*
class JsAsyncTests {
@Test
fun testLines() = GlobalScope.promise { FilesTest().testLines() }
@Test
fun testDatabase() = GlobalScope.promise { DatabaseTest().testUsage() }
@Test
fun testCheckmarkRepository() = GlobalScope.promise { CheckmarkRepositoryTest().testCRUD() }
@Test
fun testHabitRepository() = GlobalScope.promise { HabitRepositoryTest().testCRUD() }
@Test
fun testPreferencesRepository() = GlobalScope.promise { PreferencesRepositoryTest().testUsage() }
}

@ -23,12 +23,12 @@ import java.io.*
import java.nio.file.*
class JavaResourceFile(private val path: Path) : ResourceFile {
override fun copyTo(dest: UserFile) {
Files.copy(path, (dest as JavaUserFile).path)
override suspend fun lines(): List<String> {
return Files.readAllLines(path)
}
override fun readLines(): List<String> {
return Files.readAllLines(path)
override fun copyTo(dest: UserFile) {
Files.copy(path, (dest as JavaUserFile).path)
}
fun stream(): InputStream {

@ -19,6 +19,7 @@
package org.isoron
import kotlinx.coroutines.*
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.uhabits.*
@ -34,7 +35,7 @@ actual class DependencyResolver actual constructor() {
actual fun getFileOpener(): FileOpener = fileOpener
actual fun getDatabase(): Database {
actual suspend fun getDatabase(): Database {
val dbFile = fileOpener.openUserFile("test.sqlite3")
if (dbFile.exists()) dbFile.delete()
val db = databaseOpener.open(dbFile)

@ -0,0 +1,42 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron
import kotlinx.coroutines.*
import org.isoron.platform.io.*
import org.isoron.uhabits.models.*
import org.junit.*
class JavaAsyncTests {
@Test
fun testLines() = runBlocking { FilesTest().testLines() }
@Test
fun testDatabase() = runBlocking { DatabaseTest().testUsage() }
@Test
fun testCheckmarkRepository() = runBlocking { CheckmarkRepositoryTest().testCRUD() }
@Test
fun testHabitRepository() = runBlocking { HabitRepositoryTest().testCRUD() }
@Test
fun testPreferencesRepository() = runBlocking { PreferencesRepositoryTest().testUsage() }
}

@ -3615,6 +3615,11 @@
"kotlin": "1.3.21"
}
},
"kotlinx-coroutines-core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/kotlinx-coroutines-core/-/kotlinx-coroutines-core-1.1.1.tgz",
"integrity": "sha512-RnxF8HVQlMmLQcmJXSZQowR9WpsoeslY6ogqDovb/2HumkkaUBlJuR4eiXwX0DDnoAq8mGPvncl1lRAhK2+ovg=="
},
"lcid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",

@ -20,6 +20,7 @@
"babel-preset-react": "^6.24.1",
"kotlin": "^1.3.21",
"kotlin-test": "^1.3.21",
"kotlinx-coroutines-core": "^1.1.1",
"sql.js": "^0.5.0"
},
"devDependencies": {

Loading…
Cancel
Save