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 { commonMain {
dependencies { dependencies {
implementation kotlin('stdlib-common') implementation kotlin('stdlib-common')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.2.0-alpha-2'
} }
} }
@ -66,6 +67,7 @@ kotlin {
jvmMain { jvmMain {
dependencies { dependencies {
implementation kotlin('stdlib-jdk8') implementation kotlin('stdlib-jdk8')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha-2'
} }
} }
@ -80,6 +82,7 @@ kotlin {
jsMain { jsMain {
dependencies { dependencies {
implementation kotlin('stdlib-js') 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.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() val currentVersion = getVersion()
log.debug("Database", "Current database version: $currentVersion") log.debug("Database", "Current database version: $currentVersion")
@ -92,9 +92,11 @@ fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
begin() begin()
for (v in (currentVersion + 1)..newVersion) { 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) val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.readLines()) { for (line in migrationFile.lines()) {
if (line.isEmpty()) continue if (line.isEmpty()) continue
run(line) run(line)
} }

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

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

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

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

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

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

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

@ -25,52 +25,43 @@ import org.isoron.platform.io.*
import kotlin.test.* import kotlin.test.*
class HabitRepositoryTest() : BaseTest() { class HabitRepositoryTest() : BaseTest() {
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),
color = PaletteColor(3),
isArchived = false,
position = 0,
unit = "",
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
lateinit var repository: HabitRepository val original1 = Habit(id = 1,
lateinit private var original0: Habit name = "Exercise",
lateinit private var original1: Habit description = "Did you exercise for at least 20 minutes?",
lateinit private var original2: Habit frequency = Frequency(1, 2),
color = PaletteColor(4),
isArchived = false,
position = 1,
unit = "",
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
@BeforeTest val original2 = Habit(id = 2,
fun setUp() { name = "Learn Japanese",
original0 = Habit(id = 0, description = "Did you study Japanese today?",
name = "Wake up early", frequency = Frequency(1, 1),
description = "Did you wake up before 6am?", color = PaletteColor(3),
frequency = Frequency(1, 1), isArchived = false,
color = PaletteColor(3), position = 2,
isArchived = false, unit = "",
position = 0, target = 0.0,
unit = "", type = HabitType.BOOLEAN_HABIT)
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
original1 = Habit(id = 1, val repository = HabitRepository(db)
name = "Exercise",
description = "Did you exercise for at least 20 minutes?",
frequency = Frequency(1, 2),
color = PaletteColor(4),
isArchived = false,
position = 1,
unit = "",
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
original2 = Habit(id = 2,
name = "Learn Japanese",
description = "Did you study Japanese today?",
frequency = Frequency(1, 1),
color = PaletteColor(3),
isArchived = false,
position = 2,
unit = "",
target = 0.0,
type = HabitType.BOOLEAN_HABIT)
repository = HabitRepository(db)
}
@Test
fun testFindAll() {
var habits = repository.findAll() var habits = repository.findAll()
assertEquals(0, repository.nextId()) assertEquals(0, repository.nextId())
assertEquals(0, habits.size) assertEquals(0, habits.size)

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

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

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

@ -21,15 +21,18 @@ package org.isoron
import org.isoron.platform.gui.* import org.isoron.platform.gui.*
import org.isoron.platform.io.* import org.isoron.platform.io.*
import org.isoron.uhabits.*
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.browser.* import kotlin.browser.*
actual class DependencyResolver { actual class DependencyResolver {
actual fun getFileOpener(): FileOpener = JsFileOpener() actual fun getFileOpener(): FileOpener = JsFileOpener()
actual fun getDatabase(): Database { actual suspend fun getDatabase(): Database {
val db = eval("new SQL.Database()") val nativeDB = eval("new SQL.Database()")
return JsDatabase(db) val db = JsDatabase(nativeDB)
db.migrateTo(LOOP_DATABASE_VERSION, getFileOpener(), StandardLog())
return db
} }
actual fun createCanvas(width: Int, height: Int): Canvas { 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.* import java.nio.file.*
class JavaResourceFile(private val path: Path) : ResourceFile { class JavaResourceFile(private val path: Path) : ResourceFile {
override fun copyTo(dest: UserFile) { override suspend fun lines(): List<String> {
Files.copy(path, (dest as JavaUserFile).path) return Files.readAllLines(path)
} }
override fun readLines(): List<String> { override fun copyTo(dest: UserFile) {
return Files.readAllLines(path) Files.copy(path, (dest as JavaUserFile).path)
} }
fun stream(): InputStream { fun stream(): InputStream {

@ -19,6 +19,7 @@
package org.isoron package org.isoron
import kotlinx.coroutines.*
import org.isoron.platform.gui.* import org.isoron.platform.gui.*
import org.isoron.platform.io.* import org.isoron.platform.io.*
import org.isoron.uhabits.* import org.isoron.uhabits.*
@ -34,7 +35,7 @@ actual class DependencyResolver actual constructor() {
actual fun getFileOpener(): FileOpener = fileOpener actual fun getFileOpener(): FileOpener = fileOpener
actual fun getDatabase(): Database { actual suspend fun getDatabase(): Database {
val dbFile = fileOpener.openUserFile("test.sqlite3") val dbFile = fileOpener.openUserFile("test.sqlite3")
if (dbFile.exists()) dbFile.delete() if (dbFile.exists()) dbFile.delete()
val db = databaseOpener.open(dbFile) 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" "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": { "lcid": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",

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

Loading…
Cancel
Save