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" }