Restore Backend class; replace TaskRunner by Kotlin Coroutines

pull/498/head
Alinson S. Xavier 7 years ago
parent b0cedde0a9
commit 5ea19c9475

@ -1,67 +0,0 @@
/*
* 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.platform.concurrency
/**
* A TaskRunner provides the ability of running tasks in different queues. The
* class is also observable, and notifies listeners when new tasks are started
* or finished.
*
* Two queues are available: a foreground queue and a background queue. These
* two queues may run in parallel, depending on the hardware. Multiple tasks
* submitted to the same queue, however, always run sequentially, in the order
* they were enqueued.
*/
interface TaskRunner {
val listeners: MutableList<Listener>
val activeTaskCount: Int
fun runInBackground(task: () -> Unit)
fun runInForeground(task: () -> Unit)
interface Listener {
fun onTaskStarted()
fun onTaskFinished()
}
}
/**
* Sequential implementation of TaskRunner. Both background and foreground
* queues run in the same thread, so they block each other.
*/
class SequentialTaskRunner : TaskRunner {
override val listeners = mutableListOf<TaskRunner.Listener>()
override var activeTaskCount = 0
override fun runInBackground(task: () -> Unit) {
activeTaskCount += 1
for (l in listeners) l.onTaskStarted()
task()
activeTaskCount -= 1
for (l in listeners) l.onTaskFinished()
}
override fun runInForeground(task: () -> Unit) = runInBackground(task)
}

@ -19,6 +19,8 @@
package org.isoron.platform.io package org.isoron.platform.io
import org.isoron.uhabits.*
interface PreparedStatement { interface PreparedStatement {
fun step(): StepResult fun step(): StepResult
fun finalize() fun finalize()
@ -63,7 +65,7 @@ fun Database.queryInt(sql: String): Int {
fun Database.nextId(tableName: String): Int { fun Database.nextId(tableName: String): Int {
val stmt = prepareStatement("select seq from sqlite_sequence where name='$tableName'") val stmt = prepareStatement("select seq from sqlite_sequence where name='$tableName'")
if(stmt.step() == StepResult.ROW) { if (stmt.step() == StepResult.ROW) {
val result = stmt.getInt(0) val result = stmt.getInt(0)
stmt.finalize() stmt.finalize()
return result + 1 return result + 1
@ -80,7 +82,9 @@ 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")
suspend 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,7 +96,7 @@ suspend fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log
begin() begin()
for (v in (currentVersion + 1)..newVersion) { for (v in (currentVersion + 1)..newVersion) {
val sv = if(v < 10) "00$v" else if (v<100) "0$v" else "$v" val sv = if (v < 10) "00$v" else if (v < 100) "0$v" else "$v"
val filename = "migrations/$sv.sql" val filename = "migrations/$sv.sql"
val migrationFile = fileOpener.openResourceFile(filename) val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.lines()) { for (line in migrationFile.lines()) {

@ -19,86 +19,118 @@
package org.isoron.uhabits.backend package org.isoron.uhabits.backend
import kotlinx.coroutines.*
import org.isoron.platform.concurrency.* import org.isoron.platform.concurrency.*
import org.isoron.platform.io.* import org.isoron.platform.io.*
import org.isoron.uhabits.* import org.isoron.uhabits.*
import org.isoron.uhabits.components.* 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.*
import kotlin.coroutines.*
class Backend(databaseName: String,
databaseOpener: DatabaseOpener, open class BackendScope(private val ctx: CoroutineContext,
fileOpener: FileOpener, private val log: Log) : CoroutineScope {
localeHelper: LocaleHelper,
private val job = Job()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
log.info("Coroutine", throwable.toString())
}
override val coroutineContext: CoroutineContext
get() = ctx + job + exceptionHandler
}
class Backend(private val databaseName: String,
private val databaseOpener: DatabaseOpener,
private val fileOpener: FileOpener,
private val localeHelper: LocaleHelper,
private val log: Log, private val log: Log,
private val taskRunner: TaskRunner) { private val crCtx: CoroutineContext
) : CoroutineScope by BackendScope(crCtx, log) {
// private val database: Database
//
// private val habitsRepository: HabitRepository private lateinit var database: Database
// private lateinit var habitsRepository: HabitRepository
// private val checkmarkRepository: CheckmarkRepository private lateinit var checkmarkRepository: CheckmarkRepository
// lateinit var preferences: Preferences
// private val habits = mutableMapOf<Int, Habit>()
// lateinit var mainScreenDataSource: MainScreenDataSource
// private val checkmarks = mutableMapOf<Habit, CheckmarkList>()
// private val habits = mutableMapOf<Int, Habit>()
// private val scores = mutableMapOf<Habit, ScoreList>() private val checkmarks = mutableMapOf<Habit, CheckmarkList>()
private val scores = mutableMapOf<Habit, ScoreList>()
val mainScreenDataSource: MainScreenDataSource? = null
val strings = localeHelper.getStringsForCurrentLocale() var strings = localeHelper.getStringsForCurrentLocale()
val preferences: Preferences? = null
var theme: Theme = LightTheme() var theme: Theme = LightTheme()
init { val observable = Observable<Listener>()
// val dbFile = fileOpener.openUserFile(databaseName)
// if (!dbFile.exists()) { fun init() {
// val templateFile = fileOpener.openResourceFile("databases/template.db") launch {
// templateFile.copyTo(dbFile) initDatabase()
// } initRepositories()
// database = databaseOpener.open(dbFile) initDataSources()
// database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) observable.notifyListeners { it.onReady() }
// preferences = Preferences(PreferencesRepository(database)) }
// habitsRepository = HabitRepository(database) }
// checkmarkRepository = CheckmarkRepository(database)
// taskRunner.runInBackground { private fun initRepositories() {
// habits.putAll(habitsRepository.findAll()) preferences = Preferences(PreferencesRepository(database))
// for ((key, habit) in habits) { habitsRepository = HabitRepository(database)
// val checks = checkmarkRepository.findAll(key) checkmarkRepository = CheckmarkRepository(database)
// checkmarks[habit] = CheckmarkList(habit.frequency, habit.type) habits.putAll(habitsRepository.findAll())
// checkmarks[habit]?.setManualCheckmarks(checks) log.info("Backend", "${habits.size} habits loaded")
// scores[habit] = ScoreList(checkmarks[habit]!!) for ((key, habit) in habits) {
// } val checks = checkmarkRepository.findAll(key)
// } checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
// mainScreenDataSource = MainScreenDataSource(preferences, checkmarks[habit]?.setManualCheckmarks(checks)
// habits, scores[habit] = ScoreList(checkmarks[habit]!!)
// checkmarks, }
// scores, }
// taskRunner)
private fun initDataSources() {
mainScreenDataSource =
MainScreenDataSource(preferences, habits, checkmarks, scores)
}
private suspend fun initDatabase() {
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)
} }
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)
// } }
}
interface Listener {
fun onReady()
} }
} }

@ -27,8 +27,7 @@ import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
class MainScreenDataSource(val preferences: Preferences, class MainScreenDataSource(val preferences: Preferences,
val habits: MutableMap<Int, Habit>, val habits: MutableMap<Int, Habit>,
val checkmarks: MutableMap<Habit, CheckmarkList>, val checkmarks: MutableMap<Habit, CheckmarkList>,
val scores: MutableMap<Habit, ScoreList>, val scores: MutableMap<Habit, ScoreList>) {
val taskRunner: TaskRunner) {
val maxNumberOfButtons = 60 val maxNumberOfButtons = 60
private val today = LocalDate(2019, 3, 30) /* TODO */ private val today = LocalDate(2019, 3, 30) /* TODO */
@ -44,7 +43,6 @@ class MainScreenDataSource(val preferences: Preferences,
} }
fun requestData() { fun requestData() {
taskRunner.runInBackground {
var filtered = habits.values.toList() var filtered = habits.values.toList()
if (!preferences.showArchived) { if (!preferences.showArchived) {
@ -68,12 +66,9 @@ class MainScreenDataSource(val preferences: Preferences,
habit to scores[habit]!!.getAt(today) habit to scores[habit]!!.getAt(today)
} }
taskRunner.runInForeground {
observable.notifyListeners { listener -> observable.notifyListeners { listener ->
val data = Data(filtered, scores, checkmarks) val data = Data(filtered, scores, checkmarks)
listener.onDataChanged(data) listener.onDataChanged(data)
} }
} }
}
}
} }

@ -1,30 +0,0 @@
/*
* 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 org.isoron.platform.gui.*
import org.isoron.platform.io.*
expect class DependencyResolver() {
suspend fun getFileOpener(): FileOpener
suspend fun getDatabase(): Database
fun createCanvas(width: Int, height: Int): Canvas
fun exportCanvas(canvas: Canvas, filename: String)
}

@ -19,14 +19,9 @@
package org.isoron.platform.gui package org.isoron.platform.gui
import org.isoron.* class CanvasTest(val platform: Platform) {
import kotlin.test.* fun run() {
val canvas = platform.createCanvas(500, 400)
class CanvasTest() : BaseTest() {
@Test
fun testDrawing() {
val canvas = resolver.createCanvas(500, 400)
canvas.setColor(Color(0x303030)) canvas.setColor(Color(0x303030))
canvas.fillRect(0.0, 0.0, 500.0, 400.0) canvas.fillRect(0.0, 0.0, 500.0, 400.0)
@ -66,6 +61,11 @@ class CanvasTest() : BaseTest() {
canvas.setFont(Font.FONT_AWESOME) canvas.setFont(Font.FONT_AWESOME)
canvas.drawText(FontAwesome.CHECK, 250.0, 300.0) canvas.drawText(FontAwesome.CHECK, 250.0, 300.0)
resolver.exportCanvas(canvas, "CanvasTest.png") platform.exportCanvas(canvas, "CanvasTest.png")
}
interface Platform {
fun createCanvas(width: Int, height: Int): Canvas
fun exportCanvas(canvas: Canvas, filename: String)
} }
} }

@ -19,12 +19,10 @@
package org.isoron.platform.io package org.isoron.platform.io
import org.isoron.*
import kotlin.test.* import kotlin.test.*
class DatabaseTest() : BaseTest() { class DatabaseTest(val db: Database) {
suspend fun testUsage() { fun testUsage() {
val db = resolver.getDatabase()
db.setVersion(0) db.setVersion(0)
assertEquals(0, db.getVersion()) assertEquals(0, db.getVersion())

@ -22,10 +22,8 @@ package org.isoron.platform.io
import org.isoron.* import org.isoron.*
import kotlin.test.* import kotlin.test.*
class FilesTest() : BaseTest() { class FilesTest(val fileOpener: FileOpener) {
suspend fun testLines() { suspend fun testLines() {
val fileOpener = resolver.getFileOpener()
assertFalse(fileOpener.openUserFile("non-existing-usr.txt").exists(), assertFalse(fileOpener.openUserFile("non-existing-usr.txt").exists(),
"non-existing-usr.txt shouldn't exist") "non-existing-usr.txt shouldn't exist")

@ -20,13 +20,13 @@
package org.isoron.uhabits.models package org.isoron.uhabits.models
import org.isoron.* import org.isoron.*
import org.isoron.platform.io.*
import org.isoron.platform.time.* import org.isoron.platform.time.*
import kotlin.test.* import kotlin.test.*
class CheckmarkRepositoryTest : BaseTest() { class CheckmarkRepositoryTest(val db: Database) {
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),

@ -24,9 +24,8 @@ import org.isoron.platform.gui.*
import org.isoron.platform.io.* import org.isoron.platform.io.*
import kotlin.test.* import kotlin.test.*
class HabitRepositoryTest() : BaseTest() { class HabitRepositoryTest(val db: Database) {
suspend fun testCRUD() { fun testCRUD() {
val db = resolver.getDatabase()
val original0 = Habit(id = 0, val original0 = Habit(id = 0,
name = "Wake up early", name = "Wake up early",
description = "Did you wake up before 6am?", description = "Did you wake up before 6am?",

@ -19,14 +19,12 @@
package org.isoron.uhabits.models package org.isoron.uhabits.models
import org.isoron.* import org.isoron.platform.io.*
import kotlin.test.* import kotlin.test.*
class PreferencesRepositoryTest : BaseTest() { class PreferencesRepositoryTest(val db: Database) {
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"))
prefs.putString("ringtone_path", "/tmp") prefs.putString("ringtone_path", "/tmp")
assertEquals("/tmp", prefs.getString("ringtone_path", "none")) assertEquals("/tmp", prefs.getString("ringtone_path", "none"))

@ -17,8 +17,17 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron package org.isoron.platform.concurrency
val resolver = DependencyResolver() import kotlinx.coroutines.*
import platform.darwin.*
import kotlin.coroutines.*
open class BaseTest class UIDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
val queue = dispatch_get_main_queue()
dispatch_async(queue) {
block.run()
}
}
}

@ -1,50 +0,0 @@
/*
* 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/>.
*/
@file:Suppress("UNCHECKED_CAST")
package org.isoron
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import platform.CoreGraphics.*
import platform.Foundation.*
import platform.UIKit.*
actual class DependencyResolver {
actual suspend fun getFileOpener(): FileOpener {
return IosFileOpener()
}
actual suspend fun getDatabase(): Database = TODO()
actual fun createCanvas(width: Int, height: Int): Canvas {
UIGraphicsBeginImageContext(CGSizeMake(width=500.0, height=600.0))
return IosCanvas()
}
actual fun exportCanvas(canvas: Canvas, filename: String): Unit {
val image = UIGraphicsGetImageFromCurrentImageContext()!!
val manager = NSFileManager.defaultManager
val paths = manager.URLsForDirectory(NSDocumentDirectory, NSUserDomainMask) as List<NSURL>
val filePath = paths.first().URLByAppendingPathComponent("IosCanvasTest.png")!!.path!!
val data = UIImagePNGRepresentation(image)!!
data.writeToFile(filePath, false)
}
}

@ -21,22 +21,11 @@ package org.isoron
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.isoron.platform.io.* import org.isoron.platform.io.*
import org.isoron.uhabits.models.*
import kotlin.test.* import kotlin.test.*
class IosAsyncTests { class IosAsyncTests {
@Test @Test
fun testFiles() = runBlocking { FilesTest().testLines() } fun testFiles() = runBlocking {
FilesTest(IosFileOpener()).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() }
} }

@ -1,42 +0,0 @@
/*
* 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 testFiles() = 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() }
}

@ -0,0 +1,70 @@
/*
* 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.*
import org.isoron.uhabits.models.*
import kotlin.test.*
class JsAsyncTests {
var fs: JsFileStorage? = null
suspend fun getFileOpener(): FileOpener {
if (fs == null) {
fs = JsFileStorage()
fs?.init()
}
return JsFileOpener(fs!!)
}
suspend fun getDatabase(): Database {
val nativeDB = eval("new SQL.Database()")
val db = JsDatabase(nativeDB)
db.migrateTo(LOOP_DATABASE_VERSION, getFileOpener(), StandardLog())
return db
}
@Test
fun testFiles() = GlobalScope.promise {
FilesTest(getFileOpener()).testLines()
}
@Test
fun testDatabase() = GlobalScope.promise {
DatabaseTest(getDatabase()).testUsage()
}
@Test
fun testCheckmarkRepository() = GlobalScope.promise {
CheckmarkRepositoryTest(getDatabase()).testCRUD()
}
@Test
fun testHabitRepository() = GlobalScope.promise {
HabitRepositoryTest(getDatabase()).testCRUD()
}
@Test
fun testPreferencesRepository() = GlobalScope.promise {
PreferencesRepositoryTest(getDatabase()).testUsage()
}
}

@ -17,41 +17,25 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron package org.isoron.platform
import org.isoron.platform.gui.* import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.uhabits.*
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.browser.* import kotlin.browser.*
import kotlin.test.*
actual class DependencyResolver { class JsCanvasTest : CanvasTest.Platform {
override fun createCanvas(width: Int, height: Int): Canvas {
var fs: JsFileStorage? = null
actual suspend fun getFileOpener(): FileOpener {
if (fs == null) {
fs = JsFileStorage()
fs?.init()
}
return JsFileOpener(fs!!)
}
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 {
val canvasElement = document.getElementById("canvas") as HTMLCanvasElement val canvasElement = document.getElementById("canvas") as HTMLCanvasElement
canvasElement.style.width = "${width}px" canvasElement.style.width = "${width}px"
canvasElement.style.height = "${height}px" canvasElement.style.height = "${height}px"
return HtmlCanvas(canvasElement) return HtmlCanvas(canvasElement)
} }
actual fun exportCanvas(canvas: Canvas, filename: String) { override fun exportCanvas(canvas: Canvas, filename: String) {
// do nothing // do nothing
} }
@Test
fun testDrawing() = CanvasTest(this).run()
} }

@ -92,7 +92,7 @@ class JavaDatabase(private var conn: Connection,
} }
} }
class JavaDatabaseOpener(private val log: Log) : DatabaseOpener { class JavaDatabaseOpener(val log: Log) : DatabaseOpener {
override fun open(file: UserFile): Database { override fun open(file: UserFile): Database {
val platformFile = file as JavaUserFile val platformFile = file as JavaUserFile
val conn = DriverManager.getConnection("jdbc:sqlite:${platformFile.path}") val conn = DriverManager.getConnection("jdbc:sqlite:${platformFile.path}")

@ -21,22 +21,46 @@ package org.isoron
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.isoron.platform.io.* import org.isoron.platform.io.*
import org.isoron.uhabits.*
import org.isoron.uhabits.models.* import org.isoron.uhabits.models.*
import org.junit.* import org.junit.*
class JavaAsyncTests { class JavaAsyncTests {
val log = StandardLog()
val fileOpener = JavaFileOpener()
val databaseOpener = JavaDatabaseOpener(log)
suspend fun getDatabase(): Database {
val dbFile = fileOpener.openUserFile("test.sqlite3")
if (dbFile.exists()) dbFile.delete()
val db = databaseOpener.open(dbFile)
db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
return db
}
@Test @Test
fun testFiles() = runBlocking { FilesTest().testLines() } fun testFiles() = runBlocking {
FilesTest(fileOpener).testLines()
}
@Test @Test
fun testDatabase() = runBlocking { DatabaseTest().testUsage() } fun testDatabase() = runBlocking {
DatabaseTest(getDatabase()).testUsage()
}
@Test @Test
fun testCheckmarkRepository() = runBlocking { CheckmarkRepositoryTest().testCRUD() } fun testCheckmarkRepository() = runBlocking {
CheckmarkRepositoryTest(getDatabase()).testCRUD()
}
@Test @Test
fun testHabitRepository() = runBlocking { HabitRepositoryTest().testCRUD() } fun testHabitRepository() = runBlocking {
HabitRepositoryTest(getDatabase()).testCRUD()
}
@Test @Test
fun testPreferencesRepository() = runBlocking { PreferencesRepositoryTest().testUsage() } fun testPreferencesRepository() = runBlocking {
PreferencesRepositoryTest(getDatabase()).testUsage()
}
} }

@ -17,39 +17,25 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron package org.isoron.platform
import kotlinx.coroutines.*
import org.isoron.platform.gui.* import org.isoron.platform.gui.*
import org.isoron.platform.io.* import org.junit.*
import org.isoron.uhabits.*
import java.awt.image.* import java.awt.image.*
import java.io.* import java.io.*
import javax.imageio.* import javax.imageio.*
actual class DependencyResolver actual constructor() { class JavaCanvasTest : CanvasTest.Platform {
override fun createCanvas(width: Int, height: Int): Canvas {
val log = StandardLog()
val fileOpener = JavaFileOpener()
val databaseOpener = JavaDatabaseOpener(log)
actual suspend fun getFileOpener(): FileOpener = fileOpener
actual suspend fun getDatabase(): Database {
val dbFile = fileOpener.openUserFile("test.sqlite3")
if (dbFile.exists()) dbFile.delete()
val db = databaseOpener.open(dbFile)
db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
return db
}
actual fun createCanvas(width: Int, height: Int): Canvas {
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
return JavaCanvas(image, pixelScale = 1.0) return JavaCanvas(image, pixelScale = 1.0)
} }
actual fun exportCanvas(canvas: Canvas, filename: String) { override fun exportCanvas(canvas: Canvas, filename: String) {
val javaCanvas = canvas as JavaCanvas val javaCanvas = canvas as JavaCanvas
ImageIO.write(javaCanvas.image, "png", File("/tmp/$filename")) ImageIO.write(javaCanvas.image, "png", File("/tmp/$filename"))
} }
@Test
fun testDrawing() = CanvasTest(this).run()
} }

@ -19,27 +19,36 @@
import UIKit import UIKit
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, BackendListener {
var window: UIWindow? var window: UIWindow?
var backend = Backend(databaseName: "dev.db", var nav: UINavigationController?
databaseOpener: IosDatabaseOpener(withLog: StandardLog()), let log = StandardLog()
fileOpener: IosFileOpener(), var backend: Backend?
localeHelper: IosLocaleHelper(NSLocale.preferredLanguages),
log: StandardLog(),
taskRunner: SequentialTaskRunner())
func application(_ application: UIApplication, func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
backend = Backend(databaseName: "uhabits.db",
databaseOpener: IosDatabaseOpener(withLog: log),
fileOpener: IosFileOpener(),
localeHelper: IosLocaleHelper(NSLocale.preferredLanguages),
log: log,
crCtx: UIDispatcher())
backend?.observable.addListener(listener: self)
backend?.doInit()
window = UIWindow(frame: UIScreen.main.bounds) window = UIWindow(frame: UIScreen.main.bounds)
if let window = window { nav = UINavigationController()
let nav = UINavigationController() window?.backgroundColor = UIColor.white
nav.viewControllers = [MainScreenController(withBackend: backend)] window?.rootViewController = nav
window.backgroundColor = UIColor.white window?.makeKeyAndVisible()
window.rootViewController = nav
window.makeKeyAndVisible()
}
return true return true
} }
func onReady() {
nav?.viewControllers = [MainScreenController(withBackend: backend!)]
}
} }

@ -100,7 +100,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener
var preferences: Preferences var preferences: Preferences
var theme: Theme var theme: Theme
var nButtons = 3 var nButtons = 3
var strings: Strings var strings = Strings()
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError() fatalError()
@ -109,9 +109,9 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener
init(withBackend backend:Backend) { init(withBackend backend:Backend) {
self.backend = backend self.backend = backend
self.strings = backend.strings self.strings = backend.strings
self.dataSource = backend.mainScreenDataSource! self.dataSource = backend.mainScreenDataSource
self.theme = backend.theme self.theme = backend.theme
self.preferences = backend.preferences! self.preferences = backend.preferences
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.dataSource.observable.addListener(listener: self) self.dataSource.observable.addListener(listener: self)
self.dataSource.requestData() self.dataSource.requestData()

@ -124,7 +124,7 @@ class IosDatabaseOpener : NSObject, DatabaseOpener {
} }
func open(file: UserFile) -> Database { func open(file: UserFile) -> Database {
let dbPath = (file as! IosUserFile).path let dbPath = (file as! IosFile).path
let version = String(cString: sqlite3_libversion()) let version = String(cString: sqlite3_libversion())
log.info(tag: "IosDatabaseOpener", msg: "SQLite \(version)") log.info(tag: "IosDatabaseOpener", msg: "SQLite \(version)")

@ -26,9 +26,6 @@ class IosDatabaseTest: XCTestCase {
let fileOpener = IosFileOpener() let fileOpener = IosFileOpener()
let dbFile = fileOpener.openUserFile(path: "test.sqlite3") let dbFile = fileOpener.openUserFile(path: "test.sqlite3")
if dbFile.exists() {
dbFile.delete()
}
let db = databaseOpener.open(file: dbFile) let db = databaseOpener.open(file: dbFile)
var stmt = db.prepareStatement(sql: "drop table if exists demo") var stmt = db.prepareStatement(sql: "drop table if exists demo")
@ -68,6 +65,5 @@ class IosDatabaseTest: XCTestCase {
stmt.finalize() stmt.finalize()
db.close() db.close()
dbFile.delete()
} }
} }

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
001592642260AE0F00D2814F /* main.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; };
006EFE4E2252EA2B008464E0 /* IosLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006EFE4D2252EA2B008464E0 /* IosLocale.swift */; }; 006EFE4E2252EA2B008464E0 /* IosLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006EFE4D2252EA2B008464E0 /* IosLocale.swift */; };
006EFE50225432B8008464E0 /* AboutScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006EFE4F225432B8008464E0 /* AboutScreenController.swift */; }; 006EFE50225432B8008464E0 /* AboutScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006EFE4F225432B8008464E0 /* AboutScreenController.swift */; };
00A5B42822009F590024E00C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42722009F590024E00C /* AppDelegate.swift */; }; 00A5B42822009F590024E00C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42722009F590024E00C /* AppDelegate.swift */; };
@ -18,7 +19,6 @@
00C0C6BE22465F65003D8AF0 /* databases in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BB22465F65003D8AF0 /* databases */; }; 00C0C6BE22465F65003D8AF0 /* databases in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BB22465F65003D8AF0 /* databases */; };
00C0C6BF22465F65003D8AF0 /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BC22465F65003D8AF0 /* migrations */; }; 00C0C6BF22465F65003D8AF0 /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BC22465F65003D8AF0 /* migrations */; };
00C0C6CA2246E543003D8AF0 /* main.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; }; 00C0C6CA2246E543003D8AF0 /* main.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; };
00C0C6CB2246E543003D8AF0 /* main.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; };
00C0C6CC2246E550003D8AF0 /* main.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 00C0C6CC2246E550003D8AF0 /* main.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
00C0C6CE2246EFB3003D8AF0 /* IosExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6CD2246EFB3003D8AF0 /* IosExtensions.swift */; }; 00C0C6CE2246EFB3003D8AF0 /* IosExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6CD2246EFB3003D8AF0 /* IosExtensions.swift */; };
00C0C6D122470705003D8AF0 /* IosCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D022470705003D8AF0 /* IosCanvas.swift */; }; 00C0C6D122470705003D8AF0 /* IosCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D022470705003D8AF0 /* IosCanvas.swift */; };
@ -94,7 +94,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00C0C6CB2246E543003D8AF0 /* main.framework in Frameworks */, 001592642260AE0F00D2814F /* main.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -498,6 +498,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = Application/BridgingHeader.h; SWIFT_OBJC_BRIDGING_HEADER = Application/BridgingHeader.h;
SWIFT_PRECOMPILE_BRIDGING_HEADER = NO;
SWIFT_VERSION = 4.2; SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -525,6 +526,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = Application/BridgingHeader.h; SWIFT_OBJC_BRIDGING_HEADER = Application/BridgingHeader.h;
SWIFT_PRECOMPILE_BRIDGING_HEADER = NO;
SWIFT_VERSION = 4.2; SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -547,6 +549,8 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests; PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_PRECOMPILE_BRIDGING_HEADER = NO;
SWIFT_VERSION = 4.2; SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits";
@ -570,6 +574,8 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests; PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_PRECOMPILE_BRIDGING_HEADER = NO;
SWIFT_VERSION = 4.2; SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits";

@ -12,8 +12,8 @@
</style> </style>
</head> </head>
<body> <body>
<canvas id="canvas" width=500 height=400 style="display: none"></canvas>
<div id="mocha"></div> <div id="mocha"></div>
<canvas id="canvas" style="width: 500px; height: 400px; display: none;"></canvas>
<script src="../lib/mocha.js"></script> <script src="../lib/mocha.js"></script>
<script>mocha.setup('bdd')</script> <script>mocha.setup('bdd')</script>
<script src="../test.js"></script> <script src="../test.js"></script>

Loading…
Cancel
Save