mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Implement JsFileStorage using IndexedDB
This commit is contained in:
@@ -94,7 +94,6 @@ suspend fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log
|
|||||||
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"
|
||||||
println(filename)
|
|
||||||
val migrationFile = fileOpener.openResourceFile(filename)
|
val migrationFile = fileOpener.openResourceFile(filename)
|
||||||
for (line in migrationFile.lines()) {
|
for (line in migrationFile.lines()) {
|
||||||
if (line.isEmpty()) continue
|
if (line.isEmpty()) continue
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ interface FileOpener {
|
|||||||
*
|
*
|
||||||
* The path is relative to the assets folder. For example, to open
|
* The path is relative to the assets folder. For example, to open
|
||||||
* assets/main/migrations/09.sql you should provide migrations/09.sql
|
* assets/main/migrations/09.sql you should provide migrations/09.sql
|
||||||
* as the filename.
|
* as the path.
|
||||||
|
*
|
||||||
|
* This function always succeed, even if the file does not exist.
|
||||||
*/
|
*/
|
||||||
fun openResourceFile(filename: String): ResourceFile
|
fun openResourceFile(path: String): ResourceFile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file which was not shipped with the application, such as
|
* Opens a file which was not shipped with the application, such as
|
||||||
@@ -36,26 +38,54 @@ interface FileOpener {
|
|||||||
*
|
*
|
||||||
* The path is relative to the user folder. For example, if the application
|
* The path is relative to the user folder. For example, if the application
|
||||||
* stores the user data at /home/user/.loop/ and you wish to open the file
|
* stores the user data at /home/user/.loop/ and you wish to open the file
|
||||||
* /home/user/.loop/crash.log, you should provide crash.log as the filename.
|
* /home/user/.loop/crash.log, you should provide crash.log as the path.
|
||||||
|
*
|
||||||
|
* This function always succeed, even if the file does not exist.
|
||||||
*/
|
*/
|
||||||
fun openUserFile(filename: String): UserFile
|
fun openUserFile(path: String): UserFile
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a file that was created after the application was installed, as a
|
* Represents a file that was created after the application was installed, as a
|
||||||
* result of some user action, such as databases and logs. These files can be
|
* result of some user action, such as databases and logs.
|
||||||
* deleted.
|
|
||||||
*/
|
*/
|
||||||
interface UserFile {
|
interface UserFile {
|
||||||
fun delete()
|
/**
|
||||||
fun exists(): Boolean
|
* Deletes the user file. If the file does not exist, nothing happens.
|
||||||
|
*/
|
||||||
|
suspend fun delete()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the file exists.
|
||||||
|
*/
|
||||||
|
suspend fun exists(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the lines of the file. If the file does not exist, throws an
|
||||||
|
* exception.
|
||||||
|
*/
|
||||||
|
suspend fun lines(): List<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a file that was shipped with the application, such as migration
|
* Represents a file that was shipped with the application, such as migration
|
||||||
* files or translations. These files cannot be deleted.
|
* files or database templates.
|
||||||
*/
|
*/
|
||||||
interface ResourceFile {
|
interface ResourceFile {
|
||||||
fun copyTo(dest: UserFile)
|
/**
|
||||||
|
* Copies the resource file to the specified user file. If the user file
|
||||||
|
* already exists, it is replaced. If not, a new file is created.
|
||||||
|
*/
|
||||||
|
suspend fun copyTo(dest: UserFile)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the lines of the resource file. If the file does not exist,
|
||||||
|
* throws an exception.
|
||||||
|
*/
|
||||||
suspend fun lines(): List<String>
|
suspend fun lines(): List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the file exists.
|
||||||
|
*/
|
||||||
|
suspend fun exists(): Boolean
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package org.isoron
|
package org.isoron
|
||||||
|
|
||||||
open class BaseTest {
|
val resolver = DependencyResolver()
|
||||||
val resolver = DependencyResolver()
|
|
||||||
val fileOpener = resolver.getFileOpener()
|
open class BaseTest
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@ import org.isoron.platform.gui.*
|
|||||||
import org.isoron.platform.io.*
|
import org.isoron.platform.io.*
|
||||||
|
|
||||||
expect class DependencyResolver() {
|
expect class DependencyResolver() {
|
||||||
fun getFileOpener(): FileOpener
|
suspend fun getFileOpener(): FileOpener
|
||||||
suspend 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)
|
||||||
|
|||||||
@@ -24,12 +24,29 @@ import kotlin.test.*
|
|||||||
|
|
||||||
class FilesTest() : BaseTest() {
|
class FilesTest() : BaseTest() {
|
||||||
suspend fun testLines() {
|
suspend fun testLines() {
|
||||||
|
val fileOpener = resolver.getFileOpener()
|
||||||
|
|
||||||
|
assertFalse(fileOpener.openUserFile("non-existing.txt").exists())
|
||||||
|
assertFalse(fileOpener.openResourceFile("non-existing.txt").exists())
|
||||||
|
|
||||||
val hello = fileOpener.openResourceFile("hello.txt")
|
val hello = fileOpener.openResourceFile("hello.txt")
|
||||||
var lines = hello.lines()
|
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 helloCopy = fileOpener.openUserFile("hello-copy.txt")
|
||||||
|
hello.copyTo(helloCopy)
|
||||||
|
lines = helloCopy.lines()
|
||||||
|
assertEquals("Hello World!", lines[0])
|
||||||
|
assertEquals("This is a resource.", lines[1])
|
||||||
|
|
||||||
|
assertTrue(helloCopy.exists())
|
||||||
|
helloCopy.delete()
|
||||||
|
assertFalse(helloCopy.exists())
|
||||||
|
|
||||||
|
|
||||||
val migration = fileOpener.openResourceFile("migrations/012.sql")
|
val migration = fileOpener.openResourceFile("migrations/012.sql")
|
||||||
|
assertTrue(migration.exists())
|
||||||
lines = migration.lines()
|
lines = migration.lines()
|
||||||
assertEquals("delete from Score", lines[0])
|
assertEquals("delete from Score", lines[0])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ class JsPreparedStatement(val stmt: dynamic) : PreparedStatement {
|
|||||||
|
|
||||||
class JsDatabase(val db: dynamic) : Database {
|
class JsDatabase(val db: dynamic) : Database {
|
||||||
override fun prepareStatement(sql: String): PreparedStatement {
|
override fun prepareStatement(sql: String): PreparedStatement {
|
||||||
println(sql)
|
|
||||||
return JsPreparedStatement(db.prepare(sql))
|
return JsPreparedStatement(db.prepare(sql))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,31 +20,111 @@
|
|||||||
package org.isoron.platform.io
|
package org.isoron.platform.io
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.w3c.dom.events.*
|
|
||||||
import org.w3c.xhr.*
|
import org.w3c.xhr.*
|
||||||
import kotlin.js.*
|
import kotlin.js.*
|
||||||
|
|
||||||
class JsFileOpener : FileOpener {
|
class JsFileStorage {
|
||||||
override fun openUserFile(filename: String): UserFile {
|
private val indexedDB = eval("indexedDB")
|
||||||
return JsUserFile(filename)
|
private var db: dynamic = null
|
||||||
|
|
||||||
|
private val DB_NAME = "Main"
|
||||||
|
private val OS_NAME = "Files"
|
||||||
|
|
||||||
|
suspend fun init() {
|
||||||
|
console.log("Initializing JsFileStorage...")
|
||||||
|
Promise<Int> { resolve, reject ->
|
||||||
|
val req = indexedDB.open(DB_NAME, 2)
|
||||||
|
req.onerror = { reject(Exception("could not open IndexedDB")) }
|
||||||
|
req.onupgradeneeded = {
|
||||||
|
console.log("Creating document store for JsFileStorage...")
|
||||||
|
req.result.createObjectStore(OS_NAME)
|
||||||
|
}
|
||||||
|
req.onsuccess = {
|
||||||
|
console.log("JsFileStorage is ready.")
|
||||||
|
db = req.result
|
||||||
|
resolve(0)
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openResourceFile(filename: String): ResourceFile {
|
suspend fun delete(path: String) {
|
||||||
return JsResourceFile(filename)
|
Promise<Int> { resolve, reject ->
|
||||||
|
val transaction = db.transaction(OS_NAME, "readwrite")
|
||||||
|
val os = transaction.objectStore(OS_NAME)
|
||||||
|
val req = os.delete(path)
|
||||||
|
req.onerror = { reject(Exception("could not delete $path")) }
|
||||||
|
req.onsuccess = { resolve(0) }
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(path: String, content: String) {
|
||||||
|
Promise<Int> { resolve, reject ->
|
||||||
|
val transaction = db.transaction(OS_NAME, "readwrite")
|
||||||
|
val os = transaction.objectStore(OS_NAME)
|
||||||
|
val req = os.put(content, path)
|
||||||
|
req.onerror = { reject(Exception("could not put $path")) }
|
||||||
|
req.onsuccess = { resolve(0) }
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(path: String): String {
|
||||||
|
return Promise<String> { resolve, reject ->
|
||||||
|
val transaction = db.transaction(OS_NAME, "readonly")
|
||||||
|
val os = transaction.objectStore(OS_NAME)
|
||||||
|
val req = os.get(path)
|
||||||
|
req.onerror = { reject(Exception("could not get $path")) }
|
||||||
|
req.onsuccess = { resolve(req.result) }
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun exists(path: String): Boolean {
|
||||||
|
return Promise<Boolean> { resolve, reject ->
|
||||||
|
val transaction = db.transaction(OS_NAME, "readonly")
|
||||||
|
val os = transaction.objectStore(OS_NAME)
|
||||||
|
val req = os.count(path)
|
||||||
|
req.onerror = { reject(Exception("could not count $path")) }
|
||||||
|
req.onsuccess = { resolve(req.result > 0) }
|
||||||
|
}.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsUserFile(filename: String) : UserFile {
|
class JsFileOpener(val fileStorage: JsFileStorage) : FileOpener {
|
||||||
override fun delete() {
|
|
||||||
TODO()
|
override fun openUserFile(path: String): UserFile {
|
||||||
|
return JsUserFile(fileStorage, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exists(): Boolean {
|
override fun openResourceFile(path: String): ResourceFile {
|
||||||
TODO()
|
return JsResourceFile(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsUserFile(val fs: JsFileStorage,
|
||||||
|
val filename: String) : UserFile {
|
||||||
|
override suspend fun lines(): List<String> {
|
||||||
|
return fs.get(filename).lines()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete() {
|
||||||
|
fs.delete(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exists(): Boolean {
|
||||||
|
return fs.exists(filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsResourceFile(val filename: String) : ResourceFile {
|
class JsResourceFile(val filename: String) : ResourceFile {
|
||||||
|
override suspend fun exists(): Boolean {
|
||||||
|
return Promise<Boolean> { resolve, reject ->
|
||||||
|
val xhr = XMLHttpRequest()
|
||||||
|
xhr.open("GET", "/assets/$filename", true)
|
||||||
|
xhr.onload = { resolve(xhr.status.toInt() != 404) }
|
||||||
|
xhr.onerror = { reject(Exception()) }
|
||||||
|
xhr.send()
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun lines(): List<String> {
|
override suspend fun lines(): List<String> {
|
||||||
return Promise<List<String>> { resolve, reject ->
|
return Promise<List<String>> { resolve, reject ->
|
||||||
val xhr = XMLHttpRequest()
|
val xhr = XMLHttpRequest()
|
||||||
@@ -55,7 +135,8 @@ class JsResourceFile(val filename: String) : ResourceFile {
|
|||||||
}.await()
|
}.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun copyTo(dest: UserFile) {
|
override suspend fun copyTo(dest: UserFile) {
|
||||||
TODO()
|
val fs = (dest as JsUserFile).fs
|
||||||
|
fs.put(dest.filename, lines().joinToString("\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,16 @@ import org.w3c.dom.*
|
|||||||
import kotlin.browser.*
|
import kotlin.browser.*
|
||||||
|
|
||||||
actual class DependencyResolver {
|
actual class DependencyResolver {
|
||||||
actual fun getFileOpener(): FileOpener = JsFileOpener()
|
|
||||||
|
var fs: JsFileStorage? = null
|
||||||
|
|
||||||
|
actual suspend fun getFileOpener(): FileOpener {
|
||||||
|
if (fs == null) {
|
||||||
|
fs = JsFileStorage()
|
||||||
|
fs?.init()
|
||||||
|
}
|
||||||
|
return JsFileOpener(fs!!)
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun getDatabase(): Database {
|
actual suspend fun getDatabase(): Database {
|
||||||
val nativeDB = eval("new SQL.Database()")
|
val nativeDB = eval("new SQL.Database()")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import kotlin.test.*
|
|||||||
|
|
||||||
class JsAsyncTests {
|
class JsAsyncTests {
|
||||||
@Test
|
@Test
|
||||||
fun testLines() = GlobalScope.promise { FilesTest().testLines() }
|
fun testFiles() = GlobalScope.promise { FilesTest().testLines() }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testDatabase() = GlobalScope.promise { DatabaseTest().testUsage() }
|
fun testDatabase() = GlobalScope.promise { DatabaseTest().testUsage() }
|
||||||
|
|||||||
@@ -22,45 +22,54 @@ package org.isoron.platform.io
|
|||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
|
|
||||||
class JavaResourceFile(private val path: Path) : ResourceFile {
|
class JavaResourceFile(private val path: String) : ResourceFile {
|
||||||
override suspend fun lines(): List<String> {
|
private val javaPath: Path
|
||||||
return Files.readAllLines(path)
|
get() {
|
||||||
|
val mainPath = Paths.get("assets/main/$path")
|
||||||
|
val testPath = Paths.get("assets/test/$path")
|
||||||
|
if (Files.exists(mainPath)) return mainPath
|
||||||
|
else return testPath
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exists(): Boolean {
|
||||||
|
return Files.exists(javaPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun copyTo(dest: UserFile) {
|
override suspend fun lines(): List<String> {
|
||||||
Files.copy(path, (dest as JavaUserFile).path)
|
return Files.readAllLines(javaPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun copyTo(dest: UserFile) {
|
||||||
|
if (dest.exists()) dest.delete()
|
||||||
|
Files.copy(javaPath, (dest as JavaUserFile).path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stream(): InputStream {
|
fun stream(): InputStream {
|
||||||
return Files.newInputStream(path)
|
return Files.newInputStream(javaPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JavaUserFile(val path: Path) : UserFile {
|
class JavaUserFile(val path: Path) : UserFile {
|
||||||
override fun exists(): Boolean {
|
override suspend fun lines(): List<String> {
|
||||||
|
return Files.readAllLines(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exists(): Boolean {
|
||||||
return Files.exists(path)
|
return Files.exists(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete() {
|
override suspend fun delete() {
|
||||||
Files.delete(path)
|
Files.delete(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JavaFileOpener : FileOpener {
|
class JavaFileOpener : FileOpener {
|
||||||
override fun openUserFile(filename: String): UserFile {
|
override fun openUserFile(path: String): UserFile {
|
||||||
val path = Paths.get("/tmp/$filename")
|
val path = Paths.get("/tmp/$path")
|
||||||
return JavaUserFile(path)
|
return JavaUserFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openResourceFile(filename: String): ResourceFile {
|
override fun openResourceFile(path: String): ResourceFile {
|
||||||
val rootFolders = listOf("assets/main",
|
return JavaResourceFile(path)
|
||||||
"assets/test")
|
|
||||||
for (root in rootFolders) {
|
|
||||||
val path = Paths.get("$root/$filename")
|
|
||||||
if (Files.exists(path) && Files.isReadable(path)) {
|
|
||||||
return JavaResourceFile(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw RuntimeException("file not found")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ actual class DependencyResolver actual constructor() {
|
|||||||
val fileOpener = JavaFileOpener()
|
val fileOpener = JavaFileOpener()
|
||||||
val databaseOpener = JavaDatabaseOpener(log)
|
val databaseOpener = JavaDatabaseOpener(log)
|
||||||
|
|
||||||
actual fun getFileOpener(): FileOpener = fileOpener
|
actual suspend fun getFileOpener(): FileOpener = fileOpener
|
||||||
|
|
||||||
actual suspend fun getDatabase(): Database {
|
actual suspend fun getDatabase(): Database {
|
||||||
val dbFile = fileOpener.openUserFile("test.sqlite3")
|
val dbFile = fileOpener.openUserFile("test.sqlite3")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import org.junit.*
|
|||||||
|
|
||||||
class JavaAsyncTests {
|
class JavaAsyncTests {
|
||||||
@Test
|
@Test
|
||||||
fun testLines() = runBlocking { FilesTest().testLines() }
|
fun testFiles() = runBlocking { FilesTest().testLines() }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testDatabase() = runBlocking { DatabaseTest().testUsage() }
|
fun testDatabase() = runBlocking { DatabaseTest().testUsage() }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.isoron.uhabits
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
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.components.*
|
import org.isoron.uhabits.components.*
|
||||||
@@ -55,28 +56,30 @@ open class BaseViewTest {
|
|||||||
expectedPath: String,
|
expectedPath: String,
|
||||||
component: Component,
|
component: Component,
|
||||||
threshold: Double = 1e-3) {
|
threshold: Double = 1e-3) {
|
||||||
|
|
||||||
val actual = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
val actual = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
||||||
val canvas = JavaCanvas(actual)
|
val canvas = JavaCanvas(actual)
|
||||||
val expectedFile: JavaResourceFile
|
val expectedFile: JavaResourceFile
|
||||||
val actualPath = "/tmp/${expectedPath}"
|
val actualPath = "/tmp/${expectedPath}"
|
||||||
|
|
||||||
component.draw(canvas)
|
component.draw(canvas)
|
||||||
try {
|
expectedFile = JavaFileOpener().openResourceFile(expectedPath) as JavaResourceFile
|
||||||
expectedFile = JavaFileOpener().openResourceFile(expectedPath) as JavaResourceFile
|
|
||||||
} catch(e: RuntimeException) {
|
|
||||||
File(actualPath).parentFile.mkdirs()
|
|
||||||
ImageIO.write(actual, "png", File(actualPath))
|
|
||||||
//fail("Expected file is missing. Actual render saved to $actualPath")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val expected = ImageIO.read(expectedFile.stream())
|
runBlocking<Unit> {
|
||||||
val d = distance(actual, expected)
|
if (expectedFile.exists()) {
|
||||||
if (d >= threshold) {
|
val expected = ImageIO.read(expectedFile.stream())
|
||||||
File(actualPath).parentFile.mkdirs()
|
val d = distance(actual, expected)
|
||||||
ImageIO.write(actual, "png", File(actualPath))
|
if (d >= threshold) {
|
||||||
ImageIO.write(expected, "png", File(actualPath.replace(".png", ".expected.png")))
|
File(actualPath).parentFile.mkdirs()
|
||||||
//fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
|
ImageIO.write(actual, "png", File(actualPath))
|
||||||
|
ImageIO.write(expected, "png", File(actualPath.replace(".png", ".expected.png")))
|
||||||
|
//fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
File(actualPath).parentFile.mkdirs()
|
||||||
|
ImageIO.write(actual, "png", File(actualPath))
|
||||||
|
//fail("Expected file is missing. Actual render saved to $actualPath")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user