From 5ea19c9475a257e64af86555ae72019728fbf929 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 13 Apr 2019 09:20:37 -0500 Subject: [PATCH] Restore Backend class; replace TaskRunner by Kotlin Coroutines --- .../org/isoron/platform/concurrency/Tasks.kt | 67 -------- .../kotlin/org/isoron/platform/io/Database.kt | 12 +- .../org/isoron/uhabits/backend/Backend.kt | 156 +++++++++++------- .../uhabits/backend/MainScreenDataSource.kt | 47 +++--- .../kotlin/org/isoron/DependencyResolver.kt | 30 ---- .../org/isoron/platform/gui/CanvasTest.kt | 18 +- .../org/isoron/platform/io/DatabaseTest.kt | 6 +- .../org/isoron/platform/io/FilesTest.kt | 4 +- .../uhabits/models/CheckmarkRepositoryTest.kt | 6 +- .../uhabits/models/HabitRepositoryTest.kt | 5 +- .../models/PreferencesRepositoryTest.kt | 8 +- .../platform/concurrency/UIDispatcher.kt} | 15 +- .../kotlin/org/isoron/DependencyResolver.kt | 50 ------ .../kotlin/org/isoron/IosAsyncTests.kt | 17 +- .../jsTest/kotlin/org/isoron/JsAsyncTests.kt | 42 ----- .../org/isoron/platform/JsAsyncTests.kt | 70 ++++++++ .../JsCanvasTest.kt} | 32 +--- .../org/isoron/platform/io/JavaDatabase.kt | 2 +- .../kotlin/org/isoron/JavaAsyncTests.kt | 34 +++- .../JavaCanvasTest.kt} | 30 +--- ios/Application/AppDelegate.swift | 39 +++-- .../Frontend/MainScreenController.swift | 14 +- ios/Application/Platform/IosDatabase.swift | 2 +- ios/Tests/Platform/IosDatabaseTest.swift | 4 - ios/uhabits.xcodeproj/project.pbxproj | 10 +- web/src/test/index.html | 2 +- 26 files changed, 315 insertions(+), 407 deletions(-) delete mode 100644 core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt delete mode 100644 core/src/commonTest/kotlin/org/isoron/DependencyResolver.kt rename core/src/{commonTest/kotlin/org/isoron/BaseTest.kt => iosMain/kotlin/org/isoron/platform/concurrency/UIDispatcher.kt} (68%) delete mode 100644 core/src/iosTest/kotlin/org/isoron/DependencyResolver.kt delete mode 100644 core/src/jsTest/kotlin/org/isoron/JsAsyncTests.kt create mode 100644 core/src/jsTest/kotlin/org/isoron/platform/JsAsyncTests.kt rename core/src/jsTest/kotlin/org/isoron/{DependencyResolver.kt => platform/JsCanvasTest.kt} (61%) rename core/src/jvmTest/kotlin/org/isoron/{DependencyResolver.kt => platform/JavaCanvasTest.kt} (60%) diff --git a/core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt b/core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt deleted file mode 100644 index 8f518d2b2..000000000 --- a/core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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 - - 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() - - 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) -} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt b/core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt index 14faec0a7..d48b51ebd 100644 --- a/core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt @@ -19,6 +19,8 @@ package org.isoron.platform.io +import org.isoron.uhabits.* + interface PreparedStatement { fun step(): StepResult fun finalize() @@ -63,7 +65,7 @@ fun Database.queryInt(sql: String): Int { fun Database.nextId(tableName: String): Int { 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) stmt.finalize() return result + 1 @@ -80,7 +82,9 @@ fun Database.getVersion() = queryInt("pragma user_version") 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() log.debug("Database", "Current database version: $currentVersion") @@ -89,10 +93,10 @@ suspend fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log if (currentVersion > newVersion) throw RuntimeException("database produced by future version of the application") - + begin() 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 migrationFile = fileOpener.openResourceFile(filename) for (line in migrationFile.lines()) { diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt index a14c66b38..029cac4a2 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt @@ -19,86 +19,118 @@ package org.isoron.uhabits.backend +import kotlinx.coroutines.* import org.isoron.platform.concurrency.* import org.isoron.platform.io.* import org.isoron.uhabits.* import org.isoron.uhabits.components.* import org.isoron.uhabits.i18n.* import org.isoron.uhabits.models.* +import kotlin.coroutines.* -class Backend(databaseName: String, - databaseOpener: DatabaseOpener, - fileOpener: FileOpener, - localeHelper: LocaleHelper, + +open class BackendScope(private val ctx: CoroutineContext, + private val log: Log) : CoroutineScope { + + 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 taskRunner: TaskRunner) { - -// private val database: Database -// -// private val habitsRepository: HabitRepository -// -// private val checkmarkRepository: CheckmarkRepository -// -// private val habits = mutableMapOf() -// -// private val checkmarks = mutableMapOf() -// -// private val scores = mutableMapOf() - - val mainScreenDataSource: MainScreenDataSource? = null - val strings = localeHelper.getStringsForCurrentLocale() - val preferences: Preferences? = null + private val crCtx: CoroutineContext + ) : CoroutineScope by BackendScope(crCtx, log) { + + + private lateinit var database: Database + private lateinit var habitsRepository: HabitRepository + private lateinit var checkmarkRepository: CheckmarkRepository + lateinit var preferences: Preferences + + lateinit var mainScreenDataSource: MainScreenDataSource + + private val habits = mutableMapOf() + private val checkmarks = mutableMapOf() + private val scores = mutableMapOf() + + var strings = localeHelper.getStringsForCurrentLocale() 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) + val observable = Observable() + + fun init() { + launch { + initDatabase() + initRepositories() + initDataSources() + observable.notifyListeners { it.onReady() } + } + } + + private fun initRepositories() { + preferences = Preferences(PreferencesRepository(database)) + habitsRepository = HabitRepository(database) + checkmarkRepository = CheckmarkRepository(database) + habits.putAll(habitsRepository.findAll()) + log.info("Backend", "${habits.size} habits loaded") + 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]!!) + } + } + + 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) { -// 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() + 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() -// } + 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) -// } + habits[modified.id]?.let { existing -> + modified.position = existing.position + habitsRepository.update(modified) + } + } + + interface Listener { + fun onReady() } } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt index 5243fda0c..9cadb8cb3 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt @@ -27,8 +27,7 @@ import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED class MainScreenDataSource(val preferences: Preferences, val habits: MutableMap, val checkmarks: MutableMap, - val scores: MutableMap, - val taskRunner: TaskRunner) { + val scores: MutableMap) { val maxNumberOfButtons = 60 private val today = LocalDate(2019, 3, 30) /* TODO */ @@ -44,36 +43,32 @@ class MainScreenDataSource(val preferences: Preferences, } fun requestData() { - taskRunner.runInBackground { - var filtered = habits.values.toList() + var filtered = habits.values.toList() - if (!preferences.showArchived) { - filtered = filtered.filter { !it.isArchived } - } + if (!preferences.showArchived) { + filtered = filtered.filter { !it.isArchived } + } - val checkmarks = filtered.associate { habit -> - val allValues = checkmarks.getValue(habit).getUntil(today) - if (allValues.size <= maxNumberOfButtons) habit to allValues - else habit to allValues.subList(0, maxNumberOfButtons) - } + val checkmarks = filtered.associate { habit -> + val allValues = checkmarks.getValue(habit).getUntil(today) + if (allValues.size <= maxNumberOfButtons) habit to allValues + else habit to allValues.subList(0, maxNumberOfButtons) + } - if (!preferences.showCompleted) { - filtered = filtered.filter { habit -> - (habit.type == HabitType.BOOLEAN_HABIT && checkmarks.getValue(habit)[0].value == UNCHECKED) || - (habit.type == HabitType.NUMERICAL_HABIT && checkmarks.getValue(habit)[0].value * 1000 < habit.target) - } + if (!preferences.showCompleted) { + filtered = filtered.filter { habit -> + (habit.type == HabitType.BOOLEAN_HABIT && checkmarks.getValue(habit)[0].value == UNCHECKED) || + (habit.type == HabitType.NUMERICAL_HABIT && checkmarks.getValue(habit)[0].value * 1000 < habit.target) } + } - val scores = filtered.associate { habit -> - habit to scores[habit]!!.getAt(today) - } + val scores = filtered.associate { habit -> + habit to scores[habit]!!.getAt(today) + } - taskRunner.runInForeground { - observable.notifyListeners { listener -> - val data = Data(filtered, scores, checkmarks) - listener.onDataChanged(data) - } - } + observable.notifyListeners { listener -> + val data = Data(filtered, scores, checkmarks) + listener.onDataChanged(data) } } } \ No newline at end of file diff --git a/core/src/commonTest/kotlin/org/isoron/DependencyResolver.kt b/core/src/commonTest/kotlin/org/isoron/DependencyResolver.kt deleted file mode 100644 index af81ce9f7..000000000 --- a/core/src/commonTest/kotlin/org/isoron/DependencyResolver.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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) -} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/org/isoron/platform/gui/CanvasTest.kt b/core/src/commonTest/kotlin/org/isoron/platform/gui/CanvasTest.kt index 6198ea60d..8c00765e5 100644 --- a/core/src/commonTest/kotlin/org/isoron/platform/gui/CanvasTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/platform/gui/CanvasTest.kt @@ -19,14 +19,9 @@ package org.isoron.platform.gui -import org.isoron.* -import kotlin.test.* - -class CanvasTest() : BaseTest() { - - @Test - fun testDrawing() { - val canvas = resolver.createCanvas(500, 400) +class CanvasTest(val platform: Platform) { + fun run() { + val canvas = platform.createCanvas(500, 400) canvas.setColor(Color(0x303030)) canvas.fillRect(0.0, 0.0, 500.0, 400.0) @@ -66,6 +61,11 @@ class CanvasTest() : BaseTest() { canvas.setFont(Font.FONT_AWESOME) 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) } } \ No newline at end of file diff --git a/core/src/commonTest/kotlin/org/isoron/platform/io/DatabaseTest.kt b/core/src/commonTest/kotlin/org/isoron/platform/io/DatabaseTest.kt index a9101995b..dedb078e9 100644 --- a/core/src/commonTest/kotlin/org/isoron/platform/io/DatabaseTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/platform/io/DatabaseTest.kt @@ -19,12 +19,10 @@ package org.isoron.platform.io -import org.isoron.* import kotlin.test.* -class DatabaseTest() : BaseTest() { - suspend fun testUsage() { - val db = resolver.getDatabase() +class DatabaseTest(val db: Database) { + fun testUsage() { db.setVersion(0) assertEquals(0, db.getVersion()) diff --git a/core/src/commonTest/kotlin/org/isoron/platform/io/FilesTest.kt b/core/src/commonTest/kotlin/org/isoron/platform/io/FilesTest.kt index 4958ade2c..38bde3408 100644 --- a/core/src/commonTest/kotlin/org/isoron/platform/io/FilesTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/platform/io/FilesTest.kt @@ -22,10 +22,8 @@ package org.isoron.platform.io import org.isoron.* import kotlin.test.* -class FilesTest() : BaseTest() { +class FilesTest(val fileOpener: FileOpener) { suspend fun testLines() { - val fileOpener = resolver.getFileOpener() - assertFalse(fileOpener.openUserFile("non-existing-usr.txt").exists(), "non-existing-usr.txt shouldn't exist") diff --git a/core/src/commonTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt b/core/src/commonTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt index 57f9d94ea..90c929078 100644 --- a/core/src/commonTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt @@ -20,13 +20,13 @@ package org.isoron.uhabits.models import org.isoron.* +import org.isoron.platform.io.* import org.isoron.platform.time.* import kotlin.test.* -class CheckmarkRepositoryTest : BaseTest() { - suspend fun testCRUD() { - val db = resolver.getDatabase() +class CheckmarkRepositoryTest(val db: Database) { + fun testCRUD() { val habitA = 10 var checkmarksA = listOf(Checkmark(LocalDate(2019, 1, 15), 100), Checkmark(LocalDate(2019, 1, 7), 500), diff --git a/core/src/commonTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt b/core/src/commonTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt index 09060b189..d7b72a316 100644 --- a/core/src/commonTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt @@ -24,9 +24,8 @@ import org.isoron.platform.gui.* import org.isoron.platform.io.* import kotlin.test.* -class HabitRepositoryTest() : BaseTest() { - suspend fun testCRUD() { - val db = resolver.getDatabase() +class HabitRepositoryTest(val db: Database) { + fun testCRUD() { val original0 = Habit(id = 0, name = "Wake up early", description = "Did you wake up before 6am?", diff --git a/core/src/commonTest/kotlin/org/isoron/uhabits/models/PreferencesRepositoryTest.kt b/core/src/commonTest/kotlin/org/isoron/uhabits/models/PreferencesRepositoryTest.kt index 4a3f70839..4f1849730 100644 --- a/core/src/commonTest/kotlin/org/isoron/uhabits/models/PreferencesRepositoryTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/uhabits/models/PreferencesRepositoryTest.kt @@ -19,14 +19,12 @@ package org.isoron.uhabits.models -import org.isoron.* +import org.isoron.platform.io.* import kotlin.test.* -class PreferencesRepositoryTest : BaseTest() { - suspend fun testUsage() { - val db = resolver.getDatabase() +class PreferencesRepositoryTest(val db: Database) { + fun testUsage() { val prefs = PreferencesRepository(db) - assertEquals("default", prefs.getString("non_existing_key", "default")) prefs.putString("ringtone_path", "/tmp") assertEquals("/tmp", prefs.getString("ringtone_path", "none")) diff --git a/core/src/commonTest/kotlin/org/isoron/BaseTest.kt b/core/src/iosMain/kotlin/org/isoron/platform/concurrency/UIDispatcher.kt similarity index 68% rename from core/src/commonTest/kotlin/org/isoron/BaseTest.kt rename to core/src/iosMain/kotlin/org/isoron/platform/concurrency/UIDispatcher.kt index d90dfbf90..d4aece983 100644 --- a/core/src/commonTest/kotlin/org/isoron/BaseTest.kt +++ b/core/src/iosMain/kotlin/org/isoron/platform/concurrency/UIDispatcher.kt @@ -17,8 +17,17 @@ * with this program. If not, see . */ -package org.isoron +package org.isoron.platform.concurrency -val resolver = DependencyResolver() +import kotlinx.coroutines.* +import platform.darwin.* +import kotlin.coroutines.* -open class BaseTest \ No newline at end of file +class UIDispatcher : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + val queue = dispatch_get_main_queue() + dispatch_async(queue) { + block.run() + } + } +} \ No newline at end of file diff --git a/core/src/iosTest/kotlin/org/isoron/DependencyResolver.kt b/core/src/iosTest/kotlin/org/isoron/DependencyResolver.kt deleted file mode 100644 index 0568c1d9c..000000000 --- a/core/src/iosTest/kotlin/org/isoron/DependencyResolver.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -@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 - val filePath = paths.first().URLByAppendingPathComponent("IosCanvasTest.png")!!.path!! - val data = UIImagePNGRepresentation(image)!! - data.writeToFile(filePath, false) - } -} \ No newline at end of file diff --git a/core/src/iosTest/kotlin/org/isoron/IosAsyncTests.kt b/core/src/iosTest/kotlin/org/isoron/IosAsyncTests.kt index aec40d079..12b02f9a8 100644 --- a/core/src/iosTest/kotlin/org/isoron/IosAsyncTests.kt +++ b/core/src/iosTest/kotlin/org/isoron/IosAsyncTests.kt @@ -21,22 +21,11 @@ package org.isoron import kotlinx.coroutines.* import org.isoron.platform.io.* -import org.isoron.uhabits.models.* import kotlin.test.* class IosAsyncTests { @Test - fun testFiles() = 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() } + fun testFiles() = runBlocking { + FilesTest(IosFileOpener()).testLines() + } } \ No newline at end of file diff --git a/core/src/jsTest/kotlin/org/isoron/JsAsyncTests.kt b/core/src/jsTest/kotlin/org/isoron/JsAsyncTests.kt deleted file mode 100644 index a6a1644f1..000000000 --- a/core/src/jsTest/kotlin/org/isoron/JsAsyncTests.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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() } -} \ No newline at end of file diff --git a/core/src/jsTest/kotlin/org/isoron/platform/JsAsyncTests.kt b/core/src/jsTest/kotlin/org/isoron/platform/JsAsyncTests.kt new file mode 100644 index 000000000..9925da221 --- /dev/null +++ b/core/src/jsTest/kotlin/org/isoron/platform/JsAsyncTests.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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() + } +} \ No newline at end of file diff --git a/core/src/jsTest/kotlin/org/isoron/DependencyResolver.kt b/core/src/jsTest/kotlin/org/isoron/platform/JsCanvasTest.kt similarity index 61% rename from core/src/jsTest/kotlin/org/isoron/DependencyResolver.kt rename to core/src/jsTest/kotlin/org/isoron/platform/JsCanvasTest.kt index 9f0d480db..8f003324d 100644 --- a/core/src/jsTest/kotlin/org/isoron/DependencyResolver.kt +++ b/core/src/jsTest/kotlin/org/isoron/platform/JsCanvasTest.kt @@ -17,41 +17,25 @@ * with this program. If not, see . */ -package org.isoron +package org.isoron.platform import org.isoron.platform.gui.* -import org.isoron.platform.io.* -import org.isoron.uhabits.* import org.w3c.dom.* import kotlin.browser.* +import kotlin.test.* -actual class DependencyResolver { - - 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 { +class JsCanvasTest : CanvasTest.Platform { + override fun createCanvas(width: Int, height: Int): Canvas { val canvasElement = document.getElementById("canvas") as HTMLCanvasElement canvasElement.style.width = "${width}px" canvasElement.style.height = "${height}px" return HtmlCanvas(canvasElement) } - actual fun exportCanvas(canvas: Canvas, filename: String) { + override fun exportCanvas(canvas: Canvas, filename: String) { // do nothing } + + @Test + fun testDrawing() = CanvasTest(this).run() } \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt b/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt index 8a562b0ea..df99f0693 100644 --- a/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt +++ b/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt @@ -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 { val platformFile = file as JavaUserFile val conn = DriverManager.getConnection("jdbc:sqlite:${platformFile.path}") diff --git a/core/src/jvmTest/kotlin/org/isoron/JavaAsyncTests.kt b/core/src/jvmTest/kotlin/org/isoron/JavaAsyncTests.kt index 5159a13a5..32ad8b063 100644 --- a/core/src/jvmTest/kotlin/org/isoron/JavaAsyncTests.kt +++ b/core/src/jvmTest/kotlin/org/isoron/JavaAsyncTests.kt @@ -21,22 +21,46 @@ package org.isoron import kotlinx.coroutines.* import org.isoron.platform.io.* +import org.isoron.uhabits.* import org.isoron.uhabits.models.* import org.junit.* 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 - fun testFiles() = runBlocking { FilesTest().testLines() } + fun testFiles() = runBlocking { + FilesTest(fileOpener).testLines() + } @Test - fun testDatabase() = runBlocking { DatabaseTest().testUsage() } + fun testDatabase() = runBlocking { + DatabaseTest(getDatabase()).testUsage() + } @Test - fun testCheckmarkRepository() = runBlocking { CheckmarkRepositoryTest().testCRUD() } + fun testCheckmarkRepository() = runBlocking { + CheckmarkRepositoryTest(getDatabase()).testCRUD() + } @Test - fun testHabitRepository() = runBlocking { HabitRepositoryTest().testCRUD() } + fun testHabitRepository() = runBlocking { + HabitRepositoryTest(getDatabase()).testCRUD() + } @Test - fun testPreferencesRepository() = runBlocking { PreferencesRepositoryTest().testUsage() } + fun testPreferencesRepository() = runBlocking { + PreferencesRepositoryTest(getDatabase()).testUsage() + } } \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/DependencyResolver.kt b/core/src/jvmTest/kotlin/org/isoron/platform/JavaCanvasTest.kt similarity index 60% rename from core/src/jvmTest/kotlin/org/isoron/DependencyResolver.kt rename to core/src/jvmTest/kotlin/org/isoron/platform/JavaCanvasTest.kt index bc20dae1f..334500c9e 100644 --- a/core/src/jvmTest/kotlin/org/isoron/DependencyResolver.kt +++ b/core/src/jvmTest/kotlin/org/isoron/platform/JavaCanvasTest.kt @@ -17,39 +17,25 @@ * with this program. If not, see . */ -package org.isoron +package org.isoron.platform -import kotlinx.coroutines.* import org.isoron.platform.gui.* -import org.isoron.platform.io.* -import org.isoron.uhabits.* +import org.junit.* import java.awt.image.* import java.io.* import javax.imageio.* -actual class DependencyResolver actual constructor() { - - 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 { +class JavaCanvasTest : CanvasTest.Platform { + override fun createCanvas(width: Int, height: Int): Canvas { val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) 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 ImageIO.write(javaCanvas.image, "png", File("/tmp/$filename")) } + + @Test + fun testDrawing() = CanvasTest(this).run() } \ No newline at end of file diff --git a/ios/Application/AppDelegate.swift b/ios/Application/AppDelegate.swift index cb3d8f114..74c0c8a26 100644 --- a/ios/Application/AppDelegate.swift +++ b/ios/Application/AppDelegate.swift @@ -19,27 +19,36 @@ import UIKit -@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - +@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, BackendListener { + var window: UIWindow? - var backend = Backend(databaseName: "dev.db", - databaseOpener: IosDatabaseOpener(withLog: StandardLog()), - fileOpener: IosFileOpener(), - localeHelper: IosLocaleHelper(NSLocale.preferredLanguages), - log: StandardLog(), - taskRunner: SequentialTaskRunner()) + var nav: UINavigationController? + let log = StandardLog() + var backend: Backend? func application(_ application: UIApplication, 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) - if let window = window { - let nav = UINavigationController() - nav.viewControllers = [MainScreenController(withBackend: backend)] - window.backgroundColor = UIColor.white - window.rootViewController = nav - window.makeKeyAndVisible() - } + nav = UINavigationController() + window?.backgroundColor = UIColor.white + window?.rootViewController = nav + window?.makeKeyAndVisible() + return true } + + func onReady() { + nav?.viewControllers = [MainScreenController(withBackend: backend!)] + } } diff --git a/ios/Application/Frontend/MainScreenController.swift b/ios/Application/Frontend/MainScreenController.swift index 2d2b52b3e..2485caead 100644 --- a/ios/Application/Frontend/MainScreenController.swift +++ b/ios/Application/Frontend/MainScreenController.swift @@ -100,7 +100,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener var preferences: Preferences var theme: Theme var nButtons = 3 - var strings: Strings + var strings = Strings() required init?(coder aDecoder: NSCoder) { fatalError() @@ -109,9 +109,9 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener init(withBackend backend:Backend) { self.backend = backend self.strings = backend.strings - self.dataSource = backend.mainScreenDataSource! + self.dataSource = backend.mainScreenDataSource self.theme = backend.theme - self.preferences = backend.preferences! + self.preferences = backend.preferences super.init(nibName: nil, bundle: nil) self.dataSource.observable.addListener(listener: self) self.dataSource.requestData() @@ -189,7 +189,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener @objc func onMoreActionsClicked() { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - + if preferences.showArchived { alert.addAction(UIAlertAction(title: strings.hide_archived, style: .default) { (action: UIAlertAction) -> Void in @@ -203,7 +203,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener self.dataSource.requestData() }) } - + if preferences.showCompleted { alert.addAction(UIAlertAction(title: strings.hide_completed, style: .default) { (action: UIAlertAction) -> Void in @@ -217,7 +217,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener self.dataSource.requestData() }) } - + if preferences.nightMode { alert.addAction(UIAlertAction(title: strings.day_mode, style: .default) { (action: UIAlertAction) -> Void in @@ -229,7 +229,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener self.preferences.nightMode = true }) } - + alert.addAction(UIAlertAction(title: strings.help, style: .default) { (action: UIAlertAction) -> Void in if let link = URL(string: "http://loophabits.org/faq") { diff --git a/ios/Application/Platform/IosDatabase.swift b/ios/Application/Platform/IosDatabase.swift index cd5954661..bd43aa13f 100644 --- a/ios/Application/Platform/IosDatabase.swift +++ b/ios/Application/Platform/IosDatabase.swift @@ -124,7 +124,7 @@ class IosDatabaseOpener : NSObject, DatabaseOpener { } func open(file: UserFile) -> Database { - let dbPath = (file as! IosUserFile).path + let dbPath = (file as! IosFile).path let version = String(cString: sqlite3_libversion()) log.info(tag: "IosDatabaseOpener", msg: "SQLite \(version)") diff --git a/ios/Tests/Platform/IosDatabaseTest.swift b/ios/Tests/Platform/IosDatabaseTest.swift index e1ed7de15..3a2d9b93f 100644 --- a/ios/Tests/Platform/IosDatabaseTest.swift +++ b/ios/Tests/Platform/IosDatabaseTest.swift @@ -26,9 +26,6 @@ class IosDatabaseTest: XCTestCase { let fileOpener = IosFileOpener() let dbFile = fileOpener.openUserFile(path: "test.sqlite3") - if dbFile.exists() { - dbFile.delete() - } let db = databaseOpener.open(file: dbFile) var stmt = db.prepareStatement(sql: "drop table if exists demo") @@ -68,6 +65,5 @@ class IosDatabaseTest: XCTestCase { stmt.finalize() db.close() - dbFile.delete() } } diff --git a/ios/uhabits.xcodeproj/project.pbxproj b/ios/uhabits.xcodeproj/project.pbxproj index 535ff2f47..4855f75bf 100644 --- a/ios/uhabits.xcodeproj/project.pbxproj +++ b/ios/uhabits.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* 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 */; }; 006EFE50225432B8008464E0 /* AboutScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006EFE4F225432B8008464E0 /* AboutScreenController.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 */; }; 00C0C6BF22465F65003D8AF0 /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BC22465F65003D8AF0 /* migrations */; }; 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, ); }; }; 00C0C6CE2246EFB3003D8AF0 /* IosExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6CD2246EFB3003D8AF0 /* IosExtensions.swift */; }; 00C0C6D122470705003D8AF0 /* IosCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D022470705003D8AF0 /* IosCanvas.swift */; }; @@ -94,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 00C0C6CB2246E543003D8AF0 /* main.framework in Frameworks */, + 001592642260AE0F00D2814F /* main.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -498,6 +498,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = Application/BridgingHeader.h; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -525,6 +526,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = Application/BridgingHeader.h; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -547,6 +549,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits"; @@ -570,6 +574,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits"; diff --git a/web/src/test/index.html b/web/src/test/index.html index a76c88840..7ac2480f1 100644 --- a/web/src/test/index.html +++ b/web/src/test/index.html @@ -12,8 +12,8 @@ +
-