Processing Plugin tests & Refactor

This commit is contained in:
Stef Tervelde
2025-07-08 10:26:16 +02:00
parent b64505d476
commit 7379166bc4
6 changed files with 228 additions and 57 deletions

View File

@@ -7,6 +7,7 @@ plugins{
repositories {
mavenCentral()
maven("https://jogamp.org/deployment/maven")
}
dependencies{
@@ -16,9 +17,11 @@ dependencies{
implementation(libs.kotlinGradlePlugin)
implementation(libs.kotlinComposePlugin)
testImplementation(project(":core"))
testImplementation(libs.junit)
}
// TODO: CI/CD for publishing the plugin to the Gradle Plugin Portal
gradlePlugin{
plugins{
create("processing"){

View File

@@ -17,6 +17,7 @@ This task stores the resulting information in a file that can be used later to r
*/
abstract class LibrariesTask : DefaultTask() {
// TODO: Allow multiple directories
@InputDirectory
@Optional
val librariesDirectory: DirectoryProperty = project.objects.directoryProperty()
@@ -41,6 +42,9 @@ abstract class LibrariesTask : DefaultTask() {
fun execute() {
if (!librariesDirectory.isPresent) {
logger.error("Libraries directory is not set. Libraries will not be imported.")
val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream())
meta.writeObject(arrayListOf<Library>())
meta.close()
return
}
val libraries = librariesDirectory.get().asFile

View File

@@ -7,6 +7,7 @@ import org.gradle.work.InputChanges
import processing.mode.java.preproc.PdePreprocessor
import java.io.File
import java.io.ObjectOutputStream
import java.io.Serializable
import java.util.concurrent.Callable
import java.util.jar.JarFile
import javax.inject.Inject
@@ -14,7 +15,7 @@ import javax.inject.Inject
// TODO: Generate sourcemaps
/*
* The PDETask is the main task that processes the .pde files and generates the Java source code
* The PDETask is the main task that processes the .pde files and generates the Java source code through the PdePreprocessor.
*/
abstract class PDETask : SourceTask() {
@get:InputFiles
@@ -24,21 +25,13 @@ abstract class PDETask : SourceTask() {
open val stableSources: FileCollection = project.files(Callable<Any> { this.source })
@OutputDirectory
val outputDirectory = project.objects.directoryProperty()
@get:Input
@get:Optional
var workingDir: String? = null
val outputDirectory: DirectoryProperty = project.objects.directoryProperty()
@get:Input
var sketchName: String = "processing"
@get:Input
@get:Optional
var sketchBook: String? = null
@OutputFile
val sketchMetaData = project.objects.fileProperty()
val sketchMetaData: RegularFileProperty = project.objects.fileProperty()
init{
outputDirectory.convention(project.layout.buildDirectory.dir("generated/pde"))
@@ -49,18 +42,16 @@ abstract class PDETask : SourceTask() {
val sketchName: String,
val sketchRenderer: String?,
val importStatements: List<String>
) : java.io.Serializable
) : Serializable
@TaskAction
fun execute() {
// TODO: Allow pre-processor to run on individual files (future)
// TODO: Only compare file names from both defined roots (e.g. sketch.pde and folder/sketch.pde should both be included)
// Using stableSources since we can only run the pre-processor on the full set of sources
val combined = stableSources
.files
.groupBy { it.name }
.map { entry ->
// TODO: Select by which one is in the unsaved folder
entry.value.maxByOrNull { it.lastModified() }!!
}
.joinToString("\n"){
@@ -74,6 +65,8 @@ abstract class PDETask : SourceTask() {
.build()
.write(javaFile, combined)
// TODO: Save the edits to meta files
javaFile.flush()
javaFile.close()
@@ -87,10 +80,4 @@ abstract class PDETask : SourceTask() {
metaFile.writeObject(sketchMeta)
metaFile.close()
}
@get:Inject
open val deleter: Deleter
get() {
throw UnsupportedOperationException("Decorator takes care of injection")
}
}

View File

@@ -17,7 +17,6 @@ import java.net.Socket
import java.util.*
import javax.inject.Inject
// TODO: CI/CD for publishing the plugin
class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFactory) : Plugin<Project> {
override fun apply(project: Project) {
val sketchName = project.layout.projectDirectory.asFile.name.replace(Regex("[^a-zA-Z0-9_]"), "_")
@@ -32,6 +31,7 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
// TODO: Setup sketchbook when using as a standalone plugin, use the Java Preferences
val sketchbook = project.findProperty("processing.sketchbook") as String?
val settings = project.findProperty("processing.settings") as String?
// Apply the Java plugin to the Project
project.plugins.apply(JavaPlugin::class.java)
@@ -46,13 +46,12 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
project.tasks.findByName("wrapper")?.enabled = false
}
// Add the compose plugin to wrap the sketch in an executable
project.plugins.apply("org.jetbrains.compose")
// Add kotlin support
project.plugins.apply("org.jetbrains.kotlin.jvm")
// Add jetpack compose support
project.plugins.apply("org.jetbrains.kotlin.plugin.compose")
// Add the compose plugin to wrap the sketch in an executable
project.plugins.apply("org.jetbrains.compose")
// Add the Processing core library (within Processing from the internal maven repo and outside from the internet)
project.dependencies.add("implementation", "$processingGroup:core:${processingVersion}")
@@ -129,8 +128,7 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
}
project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.all { sourceSet ->
// For each java source set (mostly main) add a new source set for the PDE files
project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.first().let{ sourceSet ->
val pdeSourceSet = objectFactory.newInstance(
DefaultPDESourceDirectorySet::class.java,
objectFactory.sourceDirectorySet("${sourceSet.name}.pde", "${sourceSet.name} Processing Source")
@@ -142,11 +140,16 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
srcDir("$workingDir/unsaved")
}
sourceSet.allSource.source(pdeSourceSet)
sourceSet.java.srcDir(project.layout.projectDirectory).apply {
include("**/*.java")
exclude("${project.layout.buildDirectory.asFile.get()}/**/*")
}
val librariesTaskName = sourceSet.getTaskName("scanLibraries", "PDE")
val librariesScan = project.tasks.register(librariesTaskName, LibrariesTask::class.java) { task ->
task.description = "Scans the libraries in the sketchbook"
task.librariesDirectory.set(sketchbook?.let { File(it, "libraries") })
// TODO: Save the libraries metadata to settings folder to share between sketches
}
val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE")
@@ -154,31 +157,20 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact
task.description = "Processes the ${sourceSet.name} PDE"
task.source = pdeSourceSet
task.sketchName = sketchName
task.workingDir = workingDir
task.sketchBook = sketchbook
// Set the output of the pre-processor as the input for the java compiler
sourceSet.java.srcDir(task.outputDirectory)
task.doLast {
// Copy java files from the root to the generated directory
project.copy { copyTask ->
copyTask.from(project.layout.projectDirectory){ from ->
from.include("*.java")
}
copyTask.into(task.outputDirectory)
}
}
}
val depsTaskName = sourceSet.getTaskName("addLegacyDependencies", "PDE")
project.tasks.register(depsTaskName, DependenciesTask::class.java){ task ->
task.librariesMetaData
task.dependsOn(pdeTask, librariesScan)
// TODO: Save the libraries metadata to settings folder to share between sketches
}
project.tasks.named(
sourceSet.compileJavaTaskName
) { task ->
// Make sure that the PDE task runs before the java compilation task
project.tasks.named(sourceSet.compileJavaTaskName) { task ->
task.dependsOn(pdeTaskName, depsTaskName)
}
}

View File

@@ -1,18 +1,198 @@
import org.gradle.api.Task
import org.gradle.testfixtures.ProjectBuilder
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import java.lang.management.ManagementFactory
import java.net.URLClassLoader
class ProcessingPluginTest{
// TODO: Write tests
// TODO: Test on multiple platforms since there is meaningful differences between the platforms
@Test
fun testPluginAddsSketchTask(){
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("org.processing.java")
// TODO: Test on multiple platforms since there are meaningful differences between the platforms
data class TemporaryProcessingSketchResult(
val buildResult: BuildResult,
val sketchFolder: File,
val classLoader: ClassLoader
)
assert(project.tasks.getByName("sketch") is Task)
fun createTemporaryProcessingSketch(vararg arguments: String, configure: (sketchFolder: File) -> Unit): TemporaryProcessingSketchResult{
val directory = TemporaryFolder()
directory.create()
val sketchFolder = directory.newFolder("sketch")
directory.newFile("sketch/build.gradle.kts").writeText("""
plugins {
id("org.processing.java")
}
""".trimIndent())
directory.newFile("sketch/settings.gradle.kts")
configure(sketchFolder)
val buildResult = GradleRunner.create()
.withProjectDir(sketchFolder)
.withArguments(*arguments)
.withPluginClasspath()
.withDebug(true)
.build()
val classDir = sketchFolder.resolve("build/classes/java/main")
val classLoader = URLClassLoader(arrayOf(classDir.toURI().toURL()), this::class.java.classLoader)
return TemporaryProcessingSketchResult(
buildResult,
sketchFolder,
classLoader
)
}
@Test
fun testSinglePDE(){
val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
sketchFolder.resolve("sketch.pde").writeText("""
void setup(){
size(100, 100);
}
void draw(){
println("Hello World");
}
""".trimIndent())
}
val sketchClass = classLoader.loadClass("sketch")
assert(sketchClass != null) {
"Class sketch not found"
}
assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) {
"Method setup not found in class sketch"
}
assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) {
"Method draw not found in class sketch"
}
}
@Test
fun testMultiplePDE(){
val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
sketchFolder.resolve("sketch.pde").writeText(""")
void setup(){
size(100, 100);
}
void draw(){
otherFunction();
}
""".trimIndent())
sketchFolder.resolve("sketch2.pde").writeText("""
void otherFunction(){
println("Hi");
}
""".trimIndent())
}
val sketchClass = classLoader.loadClass("sketch")
assert(sketchClass != null) {
"Class sketch not found"
}
assert(sketchClass?.methods?.find { method -> method.name == "otherFunction" } != null) {
"Method otherFunction not found in class sketch"
}
}
@Test
fun testJavaSourceFile(){
val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
sketchFolder.resolve("sketch.pde").writeText("""
void setup(){
size(100, 100);
}
void draw(){
println("Hello World");
}
""".trimIndent())
sketchFolder.resolve("extra.java").writeText("""
class SketchJava {
public void javaMethod() {
System.out.println("Hello from Java");
}
}
""".trimIndent())
}
val sketchJavaClass = classLoader.loadClass("SketchJava")
assert(sketchJavaClass != null) {
"Class SketchJava not found"
}
assert(sketchJavaClass?.methods?.find { method -> method.name == "javaMethod" } != null) {
"Method javaMethod not found in class SketchJava"
}
}
@Test
fun testWithUnsavedSource(){
val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
sketchFolder.resolve("sketch.pde").writeText("""
void setup(){
size(100, 100);
}
void draw(){
println("Hello World");
}
""".trimIndent())
sketchFolder.resolve("../unsaved").mkdirs()
sketchFolder.resolve("../unsaved/sketch.pde").writeText("""
void setup(){
size(100, 100);
}
void draw(){
println("Hello World");
}
void newMethod(){
println("This is an unsaved method");
}
""".trimIndent())
sketchFolder.resolve("gradle.properties").writeText(""")
processing.workingDir = ${sketchFolder.parentFile.absolutePath}
""".trimIndent())
}
val sketchClass = classLoader.loadClass("sketch")
assert(sketchClass != null) {
"Class sketch not found"
}
assert(sketchClass?.methods?.find { method -> method.name == "newMethod" } != null) {
"Method otherFunction not found in class sketch"
}
}
}
fun isDebuggerAttached(): Boolean {
val runtimeMxBean = ManagementFactory.getRuntimeMXBean()
val inputArguments = runtimeMxBean.inputArguments
return inputArguments.any {
it.contains("-agentlib:jdwp")
}
}
fun openFolderInFinder(folder: File) {
if (!folder.exists() || !folder.isDirectory) {
println("Invalid directory: ${folder.absolutePath}")
return
}
val process = ProcessBuilder("open", folder.absolutePath)
.inheritIO()
.start()
process.waitFor()
}