diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e7ef2e5b3..2bd30a09d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,6 +30,11 @@ sourceSets{ srcDirs("src") } } + test{ + kotlin{ + srcDirs("test") + } + } } compose.desktop { @@ -99,6 +104,17 @@ dependencies { implementation(libs.compottie) implementation(libs.kaml) + + testImplementation(kotlin("test")) + testImplementation(libs.mockitoKotlin) + testImplementation(libs.junitJupiter) + testImplementation(libs.junitJupiterParams) +} + +tasks.test { + useJUnitPlatform() + workingDir = file("build/test") + workingDir.mkdirs() } tasks.compileJava{ diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 78e07f34a..4690c6946 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -1364,10 +1364,10 @@ private File moveLikeSketchFolder(File pdeFile, String baseName) throws IOExcept * @param schemeUri the full URI, including pde:// */ public Editor handleScheme(String schemeUri) { -// var result = Schema.handleSchema(schemeUri, this); -// if (result != null) { -// return result; -// } + var result = Schema.handleSchema(schemeUri, this); + if (result != null) { + return result; + } String location = schemeUri.substring(6); if (location.length() > 0) { diff --git a/app/src/processing/app/Schema.kt b/app/src/processing/app/Schema.kt index 8ea12e7f6..3a269f7d3 100644 --- a/app/src/processing/app/Schema.kt +++ b/app/src/processing/app/Schema.kt @@ -53,7 +53,11 @@ class Schema { private fun handleSketchUrl(uri: URI): Editor?{ val url = File(uri.path.replace("/url/", "")) - val tempSketchFolder = File(Base.untitledFolder, url.nameWithoutExtension) + val rand = (1..6) + .map { (('a'..'z') + ('A'..'Z')).random() } + .joinToString("") + + val tempSketchFolder = File(File(Base.untitledFolder, rand), url.nameWithoutExtension) tempSketchFolder.mkdirs() val tempSketchFile = File(tempSketchFolder, "${tempSketchFolder.name}.pde") @@ -71,7 +75,7 @@ class Schema { ?.map { it.split("=") } ?.associate { URLDecoder.decode(it[0], StandardCharsets.UTF_8) to - URLDecoder.decode(it[1], StandardCharsets.UTF_8) + URLDecoder.decode(it[1], StandardCharsets.UTF_8) } ?: emptyMap() options["data"]?.let{ data -> @@ -81,7 +85,7 @@ class Schema { downloadFiles(uri, code, File(sketchFolder, "code")) } options["pde"]?.let{ pde -> - downloadFiles(uri, pde, sketchFolder) + downloadFiles(uri, pde, sketchFolder, "pde") } options["mode"]?.let{ mode -> val modeFile = File(sketchFolder, "sketch.properties") @@ -89,7 +93,7 @@ class Schema { } } - private fun downloadFiles(uri: URI, urlList: String, targetFolder: File){ + private fun downloadFiles(uri: URI, urlList: String, targetFolder: File, extension: String = ""){ Thread{ targetFolder.mkdirs() @@ -101,37 +105,31 @@ class Schema { val files = urlList.split(",") files.filter { it.isNotBlank() } - .map{ it.split(":", limit = 2) } - .map{ segments -> - if(segments.size == 2){ - if(segments[0].isBlank()){ - return@map listOf(null, segments[1]) - } - return@map segments - } - return@map listOf(null, segments[0]) + .map { + if (it.contains(":")) it + else "$it:$it" } + .map{ it.split(":", limit = 2) } .forEach { (name, content) -> + var target = File(targetFolder, name) + if(extension.isNotBlank() && target.extension != extension){ + target = File(targetFolder, "$name.$extension") + } try{ - // Try to decode the content as base64 val file = Base64.getDecoder().decode(content) - if(name == null){ + if(name.isBlank()){ Messages.err("Base64 files needs to start with a file name followed by a colon") return@forEach } - File(targetFolder, name).writeBytes(file) + target.writeBytes(file) }catch(_: IllegalArgumentException){ - // Assume it's a URL and download it - var url = URI.create(content) - if(url.host == null){ - url = URI.create("https://$base/$content") - } - if(url.scheme == null){ - url = URI.create("https://$content") - } - - val target = File(targetFolder, name ?: url.path.split("/").last()) - url.toURL().openStream().use { input -> + val url = URL(when{ + content.startsWith("https://") -> content + content.startsWith("http://") -> content.replace("http://", "https://") + URL("https://$content").path.isNotBlank() -> "https://$content" + else -> "https://$base/$content" + }) + url.openStream().use { input -> target.outputStream().use { output -> input.copyTo(output) } @@ -148,7 +146,7 @@ class Schema { ?.map { it.split("=") } ?.associate { URLDecoder.decode(it[0], StandardCharsets.UTF_8) to - URLDecoder.decode(it[1], StandardCharsets.UTF_8) + URLDecoder.decode(it[1], StandardCharsets.UTF_8) } ?: emptyMap() for ((key, value) in options){ diff --git a/app/test/processing/app/SchemaTest.kt b/app/test/processing/app/SchemaTest.kt new file mode 100644 index 000000000..009f77adf --- /dev/null +++ b/app/test/processing/app/SchemaTest.kt @@ -0,0 +1,113 @@ +package processing.app + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.ArgumentCaptor +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import java.io.File +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.Test + + +class SchemaTest { + private val base: Base = mock{ + + } + companion object { + val preferences: MockedStatic = mockStatic(Preferences::class.java) + } + + + @Test + fun testLocalFiles() { + val file = "/this/is/a/local/file" + Schema.handleSchema("pde://$file", base) + verify(base).handleOpen(file) + } + + @Test + fun testNewSketch() { + Schema.handleSchema("pde://sketch/new", base) + verify(base).handleNew() + } + + @OptIn(ExperimentalEncodingApi::class) + @Test + fun testBase64SketchAndExtraFiles() { + val sketch = """ + void setup(){ + + } + void draw(){ + + } + """.trimIndent() + + val base64 = Base64.encode(sketch.toByteArray()) + Schema.handleSchema("pde://sketch/base64/$base64?pde=AnotherFile:$base64", base) + val captor = ArgumentCaptor.forClass(String::class.java) + + verify(base).handleOpenUntitled(captor.capture()) + + val file = File(captor.value) + assert(file.exists()) + assert(file.readText() == sketch) + + val extra = file.parentFile.resolve("AnotherFile.pde") + assert(extra.exists()) + assert(extra.readText() == sketch) + file.parentFile.deleteRecursively() + } + + @Test + fun testURLSketch() { + Schema.handleSchema("pde://sketch/url/github.com/processing/processing-examples/raw/refs/heads/main/Basics/Arrays/Array/Array.pde", base) + + val captor = ArgumentCaptor.forClass(String::class.java) + verify(base).handleOpenUntitled(captor.capture()) + val output = File(captor.value) + assert(output.exists()) + assert(output.name == "Array.pde") + assert(output.extension == "pde") + assert(output.parentFile.name == "Array") + + output.parentFile.parentFile.deleteRecursively() + } + + @ParameterizedTest + @ValueSource(strings = [ + "Module.pde:https://github.com/processing/processing-examples/raw/refs/heads/main/Basics/Arrays/ArrayObjects/Module.pde", + "Module.pde", + "Module:Module.pde", + "Module:https://github.com/processing/processing-examples/raw/refs/heads/main/Basics/Arrays/ArrayObjects/Module.pde", + "Module.pde:github.com/processing/processing-examples/raw/refs/heads/main/Basics/Arrays/ArrayObjects/Module.pde" + ]) + fun testURLSketchWithFile(file: String){ + Schema.handleSchema("pde://sketch/url/github.com/processing/processing-examples/raw/refs/heads/main/Basics/Arrays/ArrayObjects/ArrayObjects.pde?pde=$file", base) + + val captor = ArgumentCaptor.forClass(String::class.java) + verify(base).handleOpenUntitled(captor.capture()) + + // wait for threads to resolve + Thread.sleep(1000) + + val output = File(captor.value) + assert(output.parentFile.name == "ArrayObjects") + assert(output.exists()) + assert(output.parentFile.resolve("Module.pde").exists()) + output.parentFile.parentFile.deleteRecursively() + } + + @Test + fun testPreferences() { + Schema.handleSchema("pde://preferences?test=value", base) + preferences.verify { + Preferences.set("test", "value") + Preferences.save() + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61703c19a..89d4602ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ kotlin = "2.0.20" compose-plugin = "1.7.1" jogl = "2.5.0" +jupiter = "5.12.0" [libraries] jogl = { module = "org.jogamp.jogl:jogl-all-main", version.ref = "jogl" } @@ -12,7 +13,10 @@ jnaplatform = { module = "net.java.dev.jna:jna-platform", version = "5.12.1" } compottie = { module = "io.github.alexzhirkevich:compottie", version = "2.0.0-rc02" } kaml = { module = "com.charleskorn.kaml:kaml", version = "0.65.0" } junit = { module = "junit:junit", version = "4.13.2" } +junitJupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "jupiter" } +junitJupiterParams = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "jupiter" } mockito = { module = "org.mockito:mockito-core", version = "4.11.0" } +mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.0" } antlr = { module = "org.antlr:antlr4", version = "4.7.2" } eclipseJDT = { module = "org.eclipse.jdt:org.eclipse.jdt.core", version = "3.16.0" } eclipseJDTCompiler = { module = "org.eclipse.jdt:org.eclipse.jdt.compiler.apt", version = "1.3.400" }