diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 36a2f7649..a5dd236df 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,7 +9,42 @@ on: jobs: build-and-test: - uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@30d03306cd2dbfade5083f5c6552afad5321d2bb + uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@05e7cd7ce0ea015acda8c10c1c39fb3a20a49e60 with: disable-spotless-pr: true + java-distribution: temurin + cache-rfg: true secrets: inherit + + sdl-gpu-tests: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + concurrency: + group: sdl-gpu-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + steps: + - name: Install a software Vulkan driver + run: | + sudo apt-get update -y + sudo apt-get install -y xvfb mesa-vulkan-drivers + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: | + 21 + 25 + - uses: gradle/actions/setup-gradle@v5 + - name: Run SDL-GPU tests + run: xvfb-run -a --server-args="-screen 0 1366x768x24" ./gradlew --build-cache --stacktrace --continue :sdl-gpu:glsmSdlTest :sdl-gpu:sdlGpuDeviceTest + - name: Check LWJGL did not extract natives to the shared temp dir + run: test ! -e "/tmp/lwjgl_$USER" + - name: Attach SDL-GPU test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: sdl-gpu-test-results + path: | + sdl-gpu/build/test-results/ + sdl-gpu/build/reports/tests/ + sdl-gpu/hs_err_pid*.log diff --git a/build.gradle.kts b/build.gradle.kts index 388b62652..7adb0a65d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,8 +89,11 @@ tasks.register("copyDependencies") { val embedOnly: Configuration by configurations val shadowImplementation: Configuration by configurations +val dependencyClasspath: FileCollection = sourceSets.main.get().runtimeClasspath - sourceSets.main.get().output + val downgradeEmbedOnlyForTest by tasks.registering(DowngradeFiles::class) { inputCollection = embedOnly + classpath = dependencyClasspath outputs.dir(temporaryDir) } @@ -104,6 +107,7 @@ val stripModuleInfoFromShadow by tasks.registering(Jar::class) { val downgradeShadowImplForTest by tasks.registering(DowngradeFiles::class) { inputCollection = files(stripModuleInfoFromShadow.map { it.archiveFile }) + classpath = dependencyClasspath outputs.dir(temporaryDir) } @@ -118,6 +122,7 @@ val glsmTestFixtures: Configuration by configurations.creating { val downgradeGlsmTestFixturesForTest by tasks.registering(DowngradeFiles::class) { inputCollection = glsmTestFixtures + classpath = dependencyClasspath outputs.dir(temporaryDir) } @@ -159,9 +164,25 @@ if (isMacOs && providers.gradleProperty("clientJvmVendor").isPresent) { } } +// Forge's asm-debug-all 5.0.3 cannot read the Java 25 class files ArchScan walks; this ASM has to resolve first. +val archScanAsm: Configuration by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +dependencies { + archScanAsm(libs.asm.tree.test) + archScanAsm(libs.asm.commons.test) +} + +sourceSets.test { + compileClasspath = archScanAsm + compileClasspath +} + tasks.test { useJUnitPlatform { excludeTags = setOf("gl-core") } configureAngelicaJava8() + classpath = archScanAsm + classpath jvmArgs("-Dceleritas.lwjglService=org.taumc.celeritas.lwjgl.HeadlessTestLWJGLService") dependsOn(":glsm:classes", ":lwjgl3-backend:classes", ":sdl-gpu:classes") } @@ -169,6 +190,13 @@ tasks.test { val downgradeRootMainClasses = tasks.named("downgradeMainClasses") val downgradeRootTestClasses = tasks.named("downgradeTestClasses") +// JVMDowngrader only declares outputs for inputs that exist at configuration, so a clean tree has none. +for (name in listOf("main", "mixin", "test")) { + tasks.named("downgrade${name.replaceFirstChar { it.uppercase() }}Classes") { + sourceSets[name].output.classesDirs.files.forEach { outputs.dir(temporaryDir.resolve(it.name)) } + } +} + val glCoreTest by tasks.registering(Test::class) { description = "Runs the gl-core tests in their own JVM with a real GL context." group = "verification" @@ -185,10 +213,23 @@ val glCoreTest by tasks.registering(Test::class) { .plus(sourceSets["test"].runtimeClasspath.minus(rawMain).minus(rawTest)) configureAngelicaJava8() - mustRunAfter(tasks.test) } -tasks.test { finalizedBy(glCoreTest) } +val verifyTestsRan by tasks.registering +listOf(tasks.test, glCoreTest).forEach { testTask -> + val name = testTask.name + val verify = tasks.register("verify${name.replaceFirstChar { it.uppercase() }}Ran") { + val resultsDir = layout.buildDirectory.dir("test-results/$name") + dependsOn(testTask) + doLast { + val dir = resultsDir.get().asFile + val xmls = dir.listFiles { f -> f.name.startsWith("TEST-") && f.name.endsWith(".xml") } ?: emptyArray() + check(xmls.isNotEmpty()) { ":$name produced no TEST-*.xml in $dir - test task likely went NO-SOURCE" } + } + } + verifyTestsRan.configure { dependsOn(verify) } +} +tasks.check { dependsOn(verifyTestsRan) } apply(from = "gradle/angelica-shadow-common.gradle.kts") apply(from = "experimental-surround/surround.root.gradle.kts") diff --git a/buildSrc/src/main/kotlin/Lwjgl3Natives.kt b/buildSrc/src/main/kotlin/Lwjgl3Natives.kt new file mode 100644 index 000000000..a58f8d84a --- /dev/null +++ b/buildSrc/src/main/kotlin/Lwjgl3Natives.kt @@ -0,0 +1,32 @@ +import org.gradle.api.Project +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.the +import javax.inject.Inject + +abstract class UnpackLwjgl3Natives : Sync() { + @get:Inject + abstract val archives: ArchiveOperations +} + +// LWJGL extracts jar-packed natives to a shared temp dir with no cross-process safety; plain files it loads in place. +fun Project.registerLwjgl3NativesUnpack(): TaskProvider { + val jars = the()["test"].runtimeClasspath.filter { + it.name.startsWith("lwjgl") && it.name.contains("-natives-") && !it.name.startsWith("lwjgl-platform-") + } + return tasks.register("unpackLwjgl3Natives") { + val archives = archives + from(jars.elements.map { files -> files.map { archives.zipTree(it) } }) + exclude("META-INF/**") + into(layout.buildDirectory.dir("lwjgl3-natives")) + } +} + +fun Test.loadLwjgl3NativesFrom(unpack: TaskProvider) { + classpath = project.files(unpack) + classpath +} diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 94d7f074f..2c480a7bf 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -204,7 +204,6 @@ afterEvaluate { testImplementation(libs.junit.platform.reporting) testImplementation(libs.junit.platform.launcher) - testImplementation(libs.archunit.junit5) testImplementation(libs.retrofuturabootstrap) { isTransitive = false } glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false } diff --git a/experimental-surround/runtime/build.gradle.kts b/experimental-surround/runtime/build.gradle.kts index 339053b07..bce827ae5 100644 --- a/experimental-surround/runtime/build.gradle.kts +++ b/experimental-surround/runtime/build.gradle.kts @@ -48,6 +48,7 @@ repositories { } tasks.withType().configureEach { + outputs.cacheIf { true } downgradeTo.set(JavaVersion.VERSION_1_8) multiReleaseOriginal.set(false) multiReleaseVersions.set(emptySet()) diff --git a/glsm/build.gradle.kts b/glsm/build.gradle.kts index 553954b73..1fd706912 100644 --- a/glsm/build.gradle.kts +++ b/glsm/build.gradle.kts @@ -183,6 +183,7 @@ dependencies { } tasks.withType().configureEach { + outputs.cacheIf { true } downgradeTo.set(JavaVersion.VERSION_1_8) multiReleaseOriginal.set(false) multiReleaseVersions.set(emptySet()) @@ -302,6 +303,8 @@ val verifyTestsRan by tasks.registering { } tasks.check { dependsOn(verifyTestsRan) } +val unpackLwjgl3Natives = registerLwjgl3NativesUnpack() + val spirvTest by tasks.registering(Test::class) { description = "Runs SpirvShaderTranslator* tests against native shaderc + spvc on Java 21." group = "verification" @@ -330,6 +333,7 @@ val spirvTest by tasks.registering(Test::class) { "-Djava.library.path=${extractNatives3.get().property("destinationFolder").let { (it as DirectoryProperty).asFile.get().path }}", "--enable-native-access=ALL-UNNAMED" ) + loadLwjgl3NativesFrom(unpackLwjgl3Natives) testLogging { showStandardStreams = true diff --git a/gradle.properties b/gradle.properties index 0ecd1fd45..a01515f84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -213,7 +213,7 @@ curseForgeRelations = requiredDependency\:gtnhlib disableSpotless = true # Uncomment this to disable Checkstyle checks (currently wildcard import check). -# disableCheckstyle = true +disableCheckstyle = true # Override the IDEA build type. Valid values are: "" (leave blank, do not override), "idea" (force use native IDEA build), "gradle" # (force use delegated build). diff --git a/gradle/angelica-run-common.gradle.kts b/gradle/angelica-run-common.gradle.kts index f4bf82e14..6ed70f691 100644 --- a/gradle/angelica-run-common.gradle.kts +++ b/gradle/angelica-run-common.gradle.kts @@ -6,7 +6,10 @@ import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles val libs = the() tasks.withType().configureEach { logLevel.set("FATAL") } -tasks.withType().configureEach { logLevel.set("FATAL") } +tasks.withType().configureEach { + logLevel.set("FATAL") + outputs.cacheIf { true } +} configurations.all { resolutionStrategy.dependencySubstitution { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f778da9f..8103d3649 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,10 +30,9 @@ joml = "1.10.8" commonsLang3 = "3.17.0" jvmDowngrader = "1.3.5" junit = "5.14.1" -asmTest = "9.7" +asmTest = "9.9.1" asmBuild = "9.7" mockito = "3.12.4" -archunit = "1.4.1" ant = "1.8.2" distantHorizonsApi = "5.1.0" @@ -64,11 +63,11 @@ junit-platform-engine = { module = "org.junit.platform:junit-platform-engine" } junit-platform-reporting = { module = "org.junit.platform:junit-platform-reporting" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } -archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref = "archunit" } ant = { module = "org.apache.ant:ant", version.ref = "ant" } distant-horizons-api = { module = "maven.modrinth:DistantHorizonsApi", version.ref = "distantHorizonsApi" } asm-tree-compile = { module = "org.ow2.asm:asm-tree", version.ref = "asmCompile" } asm-tree-test = { module = "org.ow2.asm:asm-tree", version.ref = "asmTest" } +asm-commons-test = { module = "org.ow2.asm:asm-commons", version.ref = "asmTest" } asm-build = { module = "org.ow2.asm:asm", version.ref = "asmBuild" } lwjgl2 = { module = "org.lwjgl.lwjgl:lwjgl", version.ref = "lwjgl2" } lwjgl2-util = { module = "org.lwjgl.lwjgl:lwjgl_util", version.ref = "lwjgl2" } diff --git a/sdl-gpu/build.gradle.kts b/sdl-gpu/build.gradle.kts index f9a1a5938..bb305e295 100644 --- a/sdl-gpu/build.gradle.kts +++ b/sdl-gpu/build.gradle.kts @@ -1,3 +1,5 @@ +import java.time.Duration + plugins { `java-library` `maven-publish` @@ -94,6 +96,7 @@ dependencies { } val glsmSdlTag = "glsm-sdl" +val sdlGpuDeviceTag = "sdl-gpu-device" val glsmSdlStubs = sourceSets.create("glsmSdlStubs") { java.srcDir("src/glsmSdlStubs/java") @@ -109,15 +112,29 @@ dependencies { testImplementation(glsmSdlAgent.output) } -tasks.test { - useJUnitPlatform { - excludeTags(glsmSdlTag) - } +tasks.withType().configureEach { if (System.getProperty("os.name").startsWith("Mac")) { jvmArgs("-XstartOnFirstThread") } } +tasks.test { + useJUnitPlatform { + excludeTags(glsmSdlTag, sdlGpuDeviceTag) + } +} + +val sdlGpuDeviceTest by tasks.registering(Test::class) { + description = "Runs the tests that need a real SDL GPU device." + group = "verification" + testClassesDirs = sourceSets["test"].output.classesDirs + classpath = sourceSets["test"].runtimeClasspath + useJUnitPlatform { + includeTags(sdlGpuDeviceTag) + } + mustRunAfter(tasks.test) +} + val glsmSdlRedirectAgentJar by tasks.registering(Jar::class) { archiveClassifier = "glsm-sdl-redirect-agent" from(glsmSdlAgent.output) @@ -146,9 +163,6 @@ val glsmSdlTest by tasks.registering(Test::class) { "-Dangelica.sdlgpu.disablePresenterThread=true", "-Dceleritas.lwjglService=com.gtnewhorizons.angelica.sdlgpu.SDLGPULWJGLService", ) - if (System.getProperty("os.name").startsWith("Mac")) { - jvmArgs("-XstartOnFirstThread") - } testLogging { events("passed", "skipped", "failed") exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL @@ -156,7 +170,23 @@ val glsmSdlTest by tasks.registering(Test::class) { mustRunAfter(tasks.test) } -tasks.check { dependsOn(glsmSdlTest) } +val deviceTests = listOf(glsmSdlTest, sdlGpuDeviceTest) + +deviceTests.forEach { + it.configure { + outputs.cacheIf { false } + timeout = Duration.ofMinutes(5) + systemProperty("junit.jupiter.execution.timeout.default", "60s") + systemProperty("junit.jupiter.execution.timeout.threaddump.enabled", "true") + } +} + +val unpackLwjgl3Natives = registerLwjgl3NativesUnpack() +(deviceTests + tasks.test).forEach { it.configure { loadLwjgl3NativesFrom(unpackLwjgl3Natives) } } + +if (!providers.environmentVariable("CI").isPresent) { + tasks.check { dependsOn(deviceTests) } +} tasks.named("compileJava") { val classesDir = destinationDirectory diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java index fec15f784..ae5ddeaf6 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/SdlTestRig.java @@ -10,7 +10,7 @@ import org.lwjgl.sdl.SDLError; import org.lwjgl.sdl.SDLHints; import org.lwjgl.sdl.SDLInit; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUDevice; import static org.lwjgl.sdl.SDLGPU.SDL_DestroyGPUDevice; import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUDeviceDriver; @@ -90,13 +90,13 @@ public final class SdlTestRig { public static void initSdlVideo() { SDLHints.SDL_SetHint(SDLHints.SDL_HINT_MAC_BACKGROUND_APP, "1"); - assumeTrue(SDLInit.SDL_Init(SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError()); + assertTrue(SDLInit.SDL_Init(SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError()); } public static SdlTestRig acquireRealDevice() { initSdlVideo(); realSdlDevice = SDL_CreateGPUDevice(SHADER_FORMATS, true, (CharSequence) null); - assumeTrue(realSdlDevice != 0, "No SDL GPU device: " + SDLError.SDL_GetError()); + assertTrue(realSdlDevice != 0, "No SDL GPU device: " + SDLError.SDL_GetError()); final SdlTestRig rig = new SdlTestRig(realSdlDevice); rig.frameManager.setResourceManager(rig.resourceManager); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceImageAtomicsGpuTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceImageAtomicsGpuTest.java index ff1b15d2b..ce4e80f6d 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceImageAtomicsGpuTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceImageAtomicsGpuTest.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import it.unimi.dsi.fastutil.longs.Long2BooleanOpenHashMap; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +@Tag("sdl-gpu-device") class DeviceImageAtomicsGpuTest { private Device device; @@ -34,7 +36,7 @@ class DeviceImageAtomicsGpuTest { @Test void verdictsAreCachedPerSizeAndDroppedWithTheDevice() { device = new Device(); - assumeTrue(device.createDevice(), "no SDL GPU device on this machine"); + assertTrue(device.createDevice(), "no SDL GPU device on this machine"); assumeTrue(device.metalTextureAtomics(), "needs native Metal compiling at MSL 3.1 or newer"); assertFalse(device.supportsImageAtomics(40, 10, 2)); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java index 45a5aa3de..109cdb8ef 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java @@ -3,13 +3,14 @@ package com.gtnewhorizons.angelica.sdlgpu.device; import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +@Tag("sdl-gpu-device") class DeviceLifecycleGpuTest { private Device device; @@ -27,7 +28,7 @@ class DeviceLifecycleGpuTest { @Test void createsWithoutAWindowAndIsMemoized() { device = new Device(); - assumeTrue(device.createDevice(), "no SDL GPU device on this machine"); + assertTrue(device.createDevice(), "no SDL GPU device on this machine"); final long handle = device.getDevice(); assertNotEquals(0L, handle); @@ -38,7 +39,7 @@ class DeviceLifecycleGpuTest { @Test void destroyIsIdempotentOnAnUnclaimedDevice() { device = new Device(); - assumeTrue(device.createDevice(), "no SDL GPU device on this machine"); + assertTrue(device.createDevice(), "no SDL GPU device on this machine"); device.destroyDevice(); assertEquals(0L, device.getDevice()); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java index 2865f93d8..76a468e7e 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/FenceProbeRealDeviceTest.java @@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.device; import com.gtnewhorizons.angelica.sdlgpu.RealDeviceExtension; import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.lwjgl.system.MemoryStack; @@ -18,6 +19,7 @@ import static org.lwjgl.sdl.SDLVersion.SDL_GetVersion; import static org.lwjgl.sdl.SDLVersion.SDL_VERSIONNUM; @ExtendWith(RealDeviceExtension.class) +@Tag("sdl-gpu-device") class FenceProbeRealDeviceTest { @Test diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlHeadlessRig.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlHeadlessRig.java index 1d0e38621..b032da1bb 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlHeadlessRig.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlHeadlessRig.java @@ -24,7 +24,6 @@ import java.nio.ByteBuffer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; public final class GlsmSdlHeadlessRig { @@ -48,7 +47,7 @@ public final class GlsmSdlHeadlessRig { public static void boot() { if (booted) return; SdlTestRig.initSdlVideo(); - assumeTrue(SDLGPUGate.device().createDevice(), () -> "no SDL GPU device: " + SDLError.SDL_GetError()); + assertTrue(SDLGPUGate.device().createDevice(), () -> "no SDL GPU device: " + SDLError.SDL_GetError()); Reflect.setStatic(SDLGPUGate.class, "engaged", true); final RenderBackend backend = BackendManager.RENDER_BACKEND; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java index b0bd11a0c..436e1048b 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import com.gtnewhorizons.angelica.sdlgpu.resource.FboState; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(RealDeviceExtension.class) +@Tag("sdl-gpu-device") class AttachmentClearTest { private static SdlTestRig rig; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PerFrameBlockDedupTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PerFrameBlockDedupTest.java index ff1dd6c3d..902d2518b 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PerFrameBlockDedupTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PerFrameBlockDedupTest.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.ProgramObject; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.UniformMemberInfo; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.lwjgl.util.spvc.Spvc; @@ -97,6 +98,7 @@ class PerFrameBlockDedupTest { } @Test + @Tag("sdl-gpu-device") void flushUniformBlocksEndsTheCopyPassOnceForTwoDirtyBlocks() throws Exception { final SdlTestRig rig = SdlTestRig.acquireRealDevice(); try { diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/StencilMaskGpuTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/StencilMaskGpuTest.java index 462c806fc..2048b7c03 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/StencilMaskGpuTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/StencilMaskGpuTest.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.lwjgl.opengl.GL20; @@ -21,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.lwjgl.sdl.SDLGPU.*; @ExtendWith(RealDeviceExtension.class) +@Tag("sdl-gpu-device") class StencilMaskGpuTest { private static SdlTestRig rig; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/BufferSizeGpuExactnessTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/BufferSizeGpuExactnessTest.java index f61f4e991..488391771 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/BufferSizeGpuExactnessTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/BufferSizeGpuExactnessTest.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import com.gtnewhorizons.angelica.sdlgpu.device.Device; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.lwjgl.opengl.GL15; @@ -14,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(RealDeviceExtension.class) +@Tag("sdl-gpu-device") class BufferSizeGpuExactnessTest { private static Device device; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/CopyTexSubImageFromFbo0GpuTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/CopyTexSubImageFromFbo0GpuTest.java index a48be8a32..6ebc2882c 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/CopyTexSubImageFromFbo0GpuTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/CopyTexSubImageFromFbo0GpuTest.java @@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,6 +22,7 @@ import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; @ExtendWith(RealDeviceExtension.class) @Timeout(120) +@Tag("sdl-gpu-device") class CopyTexSubImageFromFbo0GpuTest { private static final int SIZE = 64; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/UploadArenaGpuRoundTripTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/UploadArenaGpuRoundTripTest.java index 6b63e3cd3..dd2eb5021 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/UploadArenaGpuRoundTripTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/resource/UploadArenaGpuRoundTripTest.java @@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,6 +29,7 @@ import static org.lwjgl.system.MemoryStack.stackPush; @ExtendWith(RealDeviceExtension.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Tag("sdl-gpu-device") class UploadArenaGpuRoundTripTest { private static Device device; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/FragmentStorageTextureReadGpuTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/FragmentStorageTextureReadGpuTest.java index 99d373dee..2e2d1c27e 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/FragmentStorageTextureReadGpuTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/FragmentStorageTextureReadGpuTest.java @@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader; import com.gtnewhorizons.angelica.sdlgpu.RealDeviceExtension; import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -31,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.lwjgl.sdl.SDLGPU.*; @ExtendWith(RealDeviceExtension.class) +@Tag("sdl-gpu-device") class FragmentStorageTextureReadGpuTest { private static SdlTestRig rig; diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/ArchScan.java b/src/test/java/com/gtnewhorizons/angelica/arch/ArchScan.java new file mode 100644 index 000000000..df4e2796f --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/arch/ArchScan.java @@ -0,0 +1,181 @@ +package com.gtnewhorizons.angelica.arch; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +final class ArchScan { + + enum Dir { + MAIN("build/classes/java/main"), + MIXIN("build/classes/java/mixin"), + GLSM("glsm/build/classes/java/main"), + LWJGL3_BACKEND("lwjgl3-backend/build/classes/java/main"), + SDL_GPU("sdl-gpu/build/classes/java/main"); + + final String path; + + Dir(String path) { + this.path = path; + } + } + + enum Kind { FIELD, CALL } + + record Access(String method, Kind kind, String owner, String name, String desc) { + + String target() { + return owner + "." + name + (kind == Kind.FIELD ? " " : "") + desc; + } + + List descriptorTypes() { + return kind == Kind.FIELD ? Collections.singletonList(Type.getType(desc).getClassName()) : signatureTypes(desc); + } + } + + record ClassConstant(String method, String type) { + } + + record Member(String name, String desc) { + } + + record ScannedClass(Dir dir, String name, Set annotations, Set references, List methods, List accesses, List classConstants) { + } + + private static final List CLASSES = scan(); + private static final Map BY_NAME = index(CLASSES); + + private ArchScan() { + } + + static List in(Dir... dirs) { + final List wanted = Arrays.asList(dirs); + final List selected = new ArrayList<>(); + for (ScannedClass c : CLASSES) { + if (wanted.contains(c.dir())) selected.add(c); + } + return selected; + } + + static List signatureTypes(String desc) { + final List types = new ArrayList<>(); + for (Type argument : Type.getArgumentTypes(desc)) { + types.add(argument.getClassName()); + } + types.add(Type.getReturnType(desc).getClassName()); + return types; + } + + static ScannedClass find(String name) { + return BY_NAME.get(name); + } + + static ScannedClass get(String name) { + final ScannedClass c = BY_NAME.get(name); + if (c == null) throw new IllegalStateException(name + " was not scanned; the scan paths are likely wrong"); + return c; + } + + private static Map index(List classes) { + final Map byName = new HashMap<>(); + for (ScannedClass c : classes) { + byName.put(c.name(), c); + } + return byName; + } + + private static List scan() { + final List classes = new ArrayList<>(); + for (Dir dir : Dir.values()) { + final Path root = Paths.get(dir.path); + if (!Files.isDirectory(root)) throw new IllegalStateException(dir.path + " is not a directory"); + final int before = classes.size(); + try (Stream files = Files.walk(root)) { + for (final Iterator it = files.iterator(); it.hasNext(); ) { + final Path file = it.next(); + if (file.toString().endsWith(".class")) classes.add(read(dir, Files.readAllBytes(file))); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + if (classes.size() == before) throw new IllegalStateException(dir.path + " yielded no classes"); + } + return classes; + } + + private static ScannedClass read(Dir dir, byte[] bytes) { + final Set references = new HashSet<>(); + final ClassNode node = new ClassNode(); + final Remapper recorder = new Remapper() { + @Override + public String map(String internalName) { + references.add(className(internalName)); + return internalName; + } + }; + new ClassReader(bytes).accept(new ClassRemapper(node, recorder), ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + final String name = className(node.name); + references.remove(name); + + final Set annotations = new HashSet<>(); + if (node.visibleAnnotations != null) { + for (AnnotationNode annotation : node.visibleAnnotations) { + annotations.add(Type.getType(annotation.desc).getClassName()); + } + } + + final List methods = new ArrayList<>(); + final List accesses = new ArrayList<>(); + final List classConstants = new ArrayList<>(); + for (MethodNode method : node.methods) { + methods.add(new Member(method.name, method.desc)); + final String origin = method.name + method.desc; + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof FieldInsnNode field) { + accesses.add(new Access(origin, Kind.FIELD, className(field.owner), field.name, field.desc)); + } else if (insn instanceof MethodInsnNode call) { + accesses.add(new Access(origin, Kind.CALL, className(call.owner), call.name, call.desc)); + } else if (insn instanceof InvokeDynamicInsnNode dynamic) { + for (Object argument : dynamic.bsmArgs) { + if (argument instanceof Handle handle) accesses.add(new Access(origin, handle.getTag() <= Opcodes.H_PUTSTATIC ? Kind.FIELD : Kind.CALL, className(handle.getOwner()), handle.getName(), handle.getDesc())); + } + } else if (insn instanceof LdcInsnNode ldc && ldc.cst instanceof Type type && (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY)) { + classConstants.add(new ClassConstant(origin, type.getClassName())); + } + } + } + return new ScannedClass(dir, name, annotations, references, methods, accesses, classConstants); + } + + private static String className(String internalName) { + return Type.getObjectType(internalName).getClassName(); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/EarlyLoadingBoundaryTest.java b/src/test/java/com/gtnewhorizons/angelica/arch/EarlyLoadingBoundaryTest.java index ca01dbed5..a3682cf4c 100644 --- a/src/test/java/com/gtnewhorizons/angelica/arch/EarlyLoadingBoundaryTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/arch/EarlyLoadingBoundaryTest.java @@ -1,9 +1,8 @@ package com.gtnewhorizons.angelica.arch; -import com.tngtech.archunit.core.domain.JavaAccess; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.gtnewhorizons.angelica.arch.ArchScan.Access; +import com.gtnewhorizons.angelica.arch.ArchScan.Dir; +import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -18,23 +17,20 @@ class EarlyLoadingBoundaryTest { private static final String TRACY = "com.gtnewhorizons.angelica.glsm.profiling.Tracy"; private static final Pattern GUARDED = Pattern.compile( - ".*/com/gtnewhorizons/angelica/(loading/[^/]+|mixins/Mixins(\\$[^/]*)?|AngelicaMod(\\$[^/]*)?|config/AngelicaConfig(\\$[^/]*)?)\\.class$"); - - private static final JavaClasses classes = new ClassFileImporter() - .withImportOption(location -> location.matches(GUARDED)) - .importPaths("build/classes/java/main"); + "com\\.gtnewhorizons\\.angelica\\.(loading\\.[^.]+|mixins\\.Mixins(\\$[^.]*)?|AngelicaMod(\\$[^.]*)?|config\\.AngelicaConfig(\\$[^.]*)?)"); @Test void earlyLoadingClassesDoNotAccessTracy() { - assertFalse(classes.isEmpty(), "no guarded classes imported"); + int guarded = 0; final List violations = new ArrayList<>(); - for (JavaClass origin : classes) { - for (JavaAccess access : origin.getAccessesFromSelf()) { - if (access.getTargetOwner().getName().equals(TRACY)) { - violations.add(origin.getFullName() + " -> " + access.getTarget().getFullName()); - } + for (ScannedClass origin : ArchScan.in(Dir.MAIN)) { + if (!GUARDED.matcher(origin.name()).matches()) continue; + guarded++; + for (Access access : origin.accesses()) { + if (access.owner().equals(TRACY)) violations.add(origin.name() + "." + access.method() + " -> " + access.target()); } } + assertFalse(guarded == 0, "no guarded classes scanned"); assertTrue(violations.isEmpty(), "Coremod-phase code must not touch glsm.profiling.Tracy:\n" + String.join("\n", violations)); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java b/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java index bd64270e8..1b2f1ca54 100644 --- a/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java @@ -1,17 +1,15 @@ package com.gtnewhorizons.angelica.arch; -import com.tngtech.archunit.core.domain.AccessTarget; -import com.tngtech.archunit.core.domain.JavaAccess; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.core.importer.ImportOption; +import com.gtnewhorizons.angelica.arch.ArchScan.Access; +import com.gtnewhorizons.angelica.arch.ArchScan.Dir; +import com.gtnewhorizons.angelica.arch.ArchScan.Member; +import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -29,24 +27,16 @@ class Lwjgl3AwareBoundaryTest { "com.gtnewhorizons.angelica.sdlgpu.", "com.gtnewhorizons.angelica.lwjgl3."); - private static final JavaClasses classes = new ClassFileImporter() - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) - .importPaths( - "build/classes/java/main", - "build/classes/java/mixin", - "glsm/build/classes/java/main", - "lwjgl3-backend/build/classes/java/main", - "sdl-gpu/build/classes/java/main"); + private static final List classes = ArchScan.in(Dir.values()); @Test void awareClassesDoNotReadRemappedMembers() { final List violations = new ArrayList<>(); - for (JavaClass origin : classes) { - if (!isAware(origin)) continue; - for (JavaAccess access : origin.getAccessesFromSelf()) { - final JavaClass owner = access.getTargetOwner(); - if (isUntransformed(owner) || isAware(owner)) continue; - if (mentionsLwjgl3Type(access.getTarget())) violations.add(describe(access)); + for (ScannedClass origin : classes) { + if (!isAware(origin.name())) continue; + for (Access access : origin.accesses()) { + if (isUntransformed(access.owner()) || isAware(access.owner())) continue; + if (mentionsLwjgl3Type(access)) violations.add(describe(origin, access)); } } assertTrue(violations.isEmpty(), "@Lwjgl3Aware code referencing a remapped member with an org.lwjgl type in its descriptor:\n" + String.join("\n", violations)); @@ -55,11 +45,11 @@ class Lwjgl3AwareBoundaryTest { @Test void remappedClassesDoNotReadAwareMembers() { final List violations = new ArrayList<>(); - for (JavaClass origin : classes) { - if (isAware(origin) || isUntransformed(origin)) continue; - for (JavaAccess access : origin.getAccessesFromSelf()) { - if (!isAware(access.getTargetOwner())) continue; - if (mentionsLwjgl3Type(access.getTarget())) violations.add(describe(access)); + for (ScannedClass origin : classes) { + if (isAware(origin.name()) || isUntransformed(origin.name())) continue; + for (Access access : origin.accesses()) { + if (!isAware(access.owner())) continue; + if (mentionsLwjgl3Type(access)) violations.add(describe(origin, access)); } } assertTrue(violations.isEmpty(), "Remapped code referencing an @Lwjgl3Aware member with an org.lwjgl type in its descriptor:\n" + String.join("\n", violations)); @@ -69,64 +59,51 @@ class Lwjgl3AwareBoundaryTest { @Test void nativeLwjgl3ModulesAreFullyAware() { final List missing = new ArrayList<>(); - for (JavaClass c : classes) { - if (AWARE_MODULES.stream().anyMatch(p -> c.getName().startsWith(p)) && !isAware(c)) { - missing.add(c.getName()); - } + int inAwareModules = 0; + for (ScannedClass c : classes) { + if (AWARE_MODULES.stream().noneMatch(p -> c.name().startsWith(p))) continue; + inAwareModules++; + if (!isAware(c.name())) missing.add(c.name()); } + assertFalse(inAwareModules == 0, "no classes scanned under " + AWARE_MODULES); assertTrue(missing.isEmpty(), "Classes missing @Lwjgl3Aware:\n" + String.join("\n", missing)); } @Test void renderBackendSignaturesAreRemapSafe() { - final JavaClass renderBackend = classes.get("com.gtnewhorizons.angelica.glsm.backend.RenderBackend"); + final ScannedClass renderBackend = ArchScan.get("com.gtnewhorizons.angelica.glsm.backend.RenderBackend"); final List violations = new ArrayList<>(); - for (JavaMethod method : renderBackend.getMethods()) { - final List types = new ArrayList<>(method.getRawParameterTypes()); - types.add(method.getRawReturnType()); - if (types.stream().anyMatch(Lwjgl3AwareBoundaryTest::isRemapUnsafeType)) { - violations.add(method.getFullName()); + for (Member method : renderBackend.methods()) { + if (method.name().startsWith("<")) continue; + if (ArchScan.signatureTypes(method.desc()).stream().anyMatch(Lwjgl3AwareBoundaryTest::isRemapUnsafeType)) { + violations.add(renderBackend.name() + "." + method.name() + method.desc()); } } assertTrue(violations.isEmpty(), "RenderBackend methods with an org.lwjgl/org.lwjglx/lwjgl3ify type in their signature:\n" + String.join("\n", violations)); } - private static boolean isAware(JavaClass c) { - return c.isAnnotatedWith(AWARE); + private static boolean isAware(String name) { + final ScannedClass c = ArchScan.find(name); + return c != null && c.annotations().contains(AWARE); } - private static boolean isUntransformed(JavaClass c) { - return UNTRANSFORMED_PACKAGES.stream().anyMatch(p -> c.getName().startsWith(p)); + private static boolean isUntransformed(String name) { + return UNTRANSFORMED_PACKAGES.stream().anyMatch(name::startsWith); } - private static boolean isLwjgl3Type(JavaClass type) { - return type.getName().startsWith("org.lwjgl."); + private static boolean isLwjgl3Type(String name) { + return name.startsWith("org.lwjgl."); } - private static boolean isRemapUnsafeType(JavaClass type) { - final String name = type.getName(); - return isLwjgl3Type(type) || name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify."); + private static boolean isRemapUnsafeType(String name) { + return isLwjgl3Type(name) || name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify."); } - private static boolean mentionsLwjgl3Type(AccessTarget target) { - return descriptorTypes(target).stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type); + private static boolean mentionsLwjgl3Type(Access access) { + return access.descriptorTypes().stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type); } - private static List descriptorTypes(AccessTarget target) { - if (target instanceof AccessTarget.FieldAccessTarget field) { - return List.of(field.getRawType()); - } - if (target instanceof AccessTarget.CodeUnitCallTarget call) { - final List types = new ArrayList<>(call.getRawParameterTypes()); - if (target instanceof AccessTarget.MethodCallTarget method) { - types.add(method.getRawReturnType()); - } - return types; - } - return List.of(); - } - - private static String describe(JavaAccess access) { - return " " + access.getOwner().getFullName() + " -> " + access.getTarget().getFullName(); + private static String describe(ScannedClass origin, Access access) { + return " " + origin.name() + "." + access.method() + " -> " + access.target(); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/NoDirectGLCallsTest.java b/src/test/java/com/gtnewhorizons/angelica/arch/NoDirectGLCallsTest.java index dd19698b8..33b800b4f 100644 --- a/src/test/java/com/gtnewhorizons/angelica/arch/NoDirectGLCallsTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/arch/NoDirectGLCallsTest.java @@ -1,15 +1,16 @@ package com.gtnewhorizons.angelica.arch; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.lang.ArchRule; +import com.gtnewhorizons.angelica.arch.ArchScan.Access; +import com.gtnewhorizons.angelica.arch.ArchScan.Dir; +import com.gtnewhorizons.angelica.arch.ArchScan.Kind; +import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass; import org.junit.jupiter.api.Test; -import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target; -import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameMatching; -import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Enforces that all GL calls go through GLStateManager / RENDER_BACKEND. Direct method calls on org.lwjgl.opengl GL command classes are banned outside @@ -23,27 +24,22 @@ class NoDirectGLCallsTest { // Matches GL command classes but not LWJGL infrastructure (Display, etc.) // Covers: GL11-GL44, ARB*, EXT*, NV*, AMD*, ATI*, KHR*, INTEL*, GLContext, plus OpenGlHelper - private static final String GL_COMMAND_CLASSES = "org\\.lwjgl\\.opengl\\.(?!Display|ContextAttribs|PixelFormat|Drawable|SharedDrawable).*"; + private static final Pattern GL_COMMAND_CLASSES = Pattern.compile("org\\.lwjgl\\.opengl\\.(?!Display|ContextAttribs|PixelFormat|Drawable|SharedDrawable).*"); - private static final JavaClasses classes = new ClassFileImporter() - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) - .importPaths( - "build/classes/java/main", - "build/classes/java/mixin" - ); - - private static final ArchRule NO_DIRECT_GL_CALLS = noClasses() - .that().resideOutsideOfPackages( - "com.gtnewhorizons.angelica.glsm.backend..", - "com.gtnewhorizons.angelica.loading.." - ) - .should().callMethodWhere( - target(owner(nameMatching(GL_COMMAND_CLASSES))) - ) - .because("GL calls must go through GLStateManager, not directly to LWJGL or the RENDER_BACKEND"); + private static final List EXEMPT_PACKAGES = List.of( + "com.gtnewhorizons.angelica.glsm.backend.", + "com.gtnewhorizons.angelica.loading."); @Test void noDirectGLCalls() { - NO_DIRECT_GL_CALLS.check(classes); + final List violations = new ArrayList<>(); + for (ScannedClass origin : ArchScan.in(Dir.MAIN, Dir.MIXIN)) { + if (EXEMPT_PACKAGES.stream().anyMatch(origin.name()::startsWith)) continue; + for (Access access : origin.accesses()) { + if (access.kind() != Kind.CALL || access.name().equals("")) continue; + if (GL_COMMAND_CLASSES.matcher(access.owner()).matches()) violations.add(" " + origin.name() + "." + access.method() + " -> " + access.target()); + } + } + assertTrue(violations.isEmpty(), "GL calls must go through GLStateManager, not directly to LWJGL or the RENDER_BACKEND:\n" + String.join("\n", violations)); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/NoWildcardImportsTest.java b/src/test/java/com/gtnewhorizons/angelica/arch/NoWildcardImportsTest.java new file mode 100644 index 000000000..62ee3b3d3 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/arch/NoWildcardImportsTest.java @@ -0,0 +1,43 @@ +package com.gtnewhorizons.angelica.arch; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NoWildcardImportsTest { + + private static final List SOURCE_DIRS = List.of("src/main/java", "src/mixin/java", "src/test/java"); + + private static final Pattern WILDCARD_IMPORT = Pattern.compile("\\s*import\\s+(?!static\\s)[\\w.]+\\.\\*\\s*;.*"); + + @Test + void noWildcardImports() throws IOException { + final List violations = new ArrayList<>(); + for (String dir : SOURCE_DIRS) { + final List sources; + try (Stream files = Files.walk(Paths.get(dir))) { + sources = files.filter(file -> file.toString().endsWith(".java")).collect(Collectors.toList()); + } + assertFalse(sources.isEmpty(), dir + " has no Java sources"); + for (Path source : sources) { + final String[] lines = new String(Files.readAllBytes(source), StandardCharsets.ISO_8859_1).split("\r?\n"); + for (int i = 0; i < lines.length; i++) { + if (WILDCARD_IMPORT.matcher(lines[i]).matches()) violations.add(" " + source + ":" + (i + 1)); + } + } + } + assertTrue(violations.isEmpty(), "Wildcard imports (static ones are allowed):\n" + String.join("\n", violations)); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/SDLGPUEncapsulationTest.java b/src/test/java/com/gtnewhorizons/angelica/arch/SDLGPUEncapsulationTest.java index 069e82f90..1cdde7118 100644 --- a/src/test/java/com/gtnewhorizons/angelica/arch/SDLGPUEncapsulationTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/arch/SDLGPUEncapsulationTest.java @@ -1,21 +1,16 @@ package com.gtnewhorizons.angelica.arch; -import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.core.domain.JavaAccess; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.domain.ReferencedClassObject; -import com.tngtech.archunit.core.domain.JavaStaticInitializer; -import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.lang.ArchRule; +import com.gtnewhorizons.angelica.arch.ArchScan.Access; +import com.gtnewhorizons.angelica.arch.ArchScan.ClassConstant; +import com.gtnewhorizons.angelica.arch.ArchScan.Dir; +import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass; import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.function.Predicate; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -29,69 +24,59 @@ class SDLGPUEncapsulationTest { private static final String LWJGL3_AWARE = "me.eigenraven.lwjgl3ify.api.Lwjgl3Aware"; - private static final DescribedPredicate SDLGPU_BEHIND_THE_GATE = - new DescribedPredicate<>("anything under " + SDLGPU_ROOT + " other than SDLGPUGate") { - @Override - public boolean test(JavaClass c) { - final String name = c.getName(); - return name.startsWith(SDLGPU_ROOT + ".") && !GATE.equals(name); - } - }; - - private static final DescribedPredicate LWJGL3_ONLY_TYPE = - new DescribedPredicate<>("types that only exist under lwjgl3ify/LWJGL3") { - @Override - public boolean test(JavaClass c) { - final String name = c.getName(); - if (LWJGL3_AWARE.equals(name)) return false; - return name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.") || name.startsWith("org.lwjgl.sdl."); - } - }; - - private static final JavaClasses CLASSES = new ClassFileImporter() - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) - .importPaths("build/classes/java/main", "build/classes/java/mixin", "glsm/build/classes/java/main"); - - private static final JavaClasses SDLGPU_CLASSES = new ClassFileImporter() - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) - .importPaths("sdl-gpu/build/classes/java/main"); - - private static final ArchRule SDLGPU_REACHED_ONLY_THROUGH_THE_GATE = noClasses() - .that().resideOutsideOfPackage(SDLGPU_ROOT + "..") - .should().dependOnClassesThat(SDLGPU_BEHIND_THE_GATE) - .because("SDLGPUGate is the only :sdl-gpu class safe to load without LWJGL3"); - - private static final ArchRule NO_LWJGL3_ONLY_TYPES = noClasses() - .that().resideOutsideOfPackage(SDLGPU_ROOT + "..") - .should().dependOnClassesThat(LWJGL3_ONLY_TYPE) - .because("main, mixin and glsm all load on an LWJGL2 launch, where these types are absent"); - @Test void sdlGpuIsReachedOnlyThroughTheGate() { - SDLGPU_REACHED_ONLY_THROUGH_THE_GATE.check(CLASSES); + final List violations = referencesOutsideSdlGpu(SDLGPUEncapsulationTest::isBehindTheGate); + assertTrue(violations.isEmpty(), "SDLGPUGate is the only :sdl-gpu class safe to load without LWJGL3:\n" + String.join("\n", violations)); } @Test void noLwjgl3OnlyTypesOutsideSdlGpu() { - NO_LWJGL3_ONLY_TYPES.check(CLASSES); + final List violations = referencesOutsideSdlGpu(SDLGPUEncapsulationTest::isLwjgl3OnlyType); + assertTrue(violations.isEmpty(), "main, mixin and glsm all load on an LWJGL2 launch, where these types are absent:\n" + String.join("\n", violations)); } @Test void gateStaticInitIsLwjgl3Free() { - final Optional clinit = SDLGPU_CLASSES.get(GATE).getStaticInitializer(); - assertTrue(clinit.isPresent(), GATE + " has no static initializer to check; the importer path is likely wrong"); + final ScannedClass gate = ArchScan.get(GATE); + assertTrue(gate.methods().stream().anyMatch(m -> m.name().equals("")), GATE + " has no static initializer to check; the scan path is likely wrong"); final List violations = new ArrayList<>(); - for (JavaAccess access : clinit.get().getAccessesFromSelf()) { - if (isLwjgl3(access.getTargetOwner())) violations.add(" " + access.getTarget().getFullName()); + for (Access access : gate.accesses()) { + if (isStaticInit(access.method()) && isLwjgl3(access.owner())) violations.add(" " + access.target()); } - for (ReferencedClassObject ref : clinit.get().getReferencedClassObjects()) { - if (isLwjgl3(ref.getValue())) violations.add(" " + ref.getValue().getName() + ".class"); + for (ClassConstant constant : gate.classConstants()) { + if (isStaticInit(constant.method()) && isLwjgl3(constant.type())) violations.add(" " + constant.type() + ".class"); } assertTrue(violations.isEmpty(), GATE + " static init resolves LWJGL3 types:\n" + String.join("\n", violations)); } - private static boolean isLwjgl3(JavaClass c) { - return LWJGL3_ONLY_TYPE.test(c) || c.getName().startsWith("org.lwjgl."); + private static List referencesOutsideSdlGpu(Predicate forbidden) { + final List violations = new ArrayList<>(); + for (ScannedClass origin : ArchScan.in(Dir.MAIN, Dir.MIXIN, Dir.GLSM)) { + if (origin.name().startsWith(SDLGPU_ROOT + ".")) continue; + for (String reference : origin.references()) { + if (forbidden.test(reference)) violations.add(" " + origin.name() + " -> " + reference); + } + } + Collections.sort(violations); + return violations; + } + + private static boolean isBehindTheGate(String name) { + return name.startsWith(SDLGPU_ROOT + ".") && !GATE.equals(name); + } + + private static boolean isLwjgl3OnlyType(String name) { + if (LWJGL3_AWARE.equals(name)) return false; + return name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.") || name.startsWith("org.lwjgl.sdl."); + } + + private static boolean isLwjgl3(String name) { + return isLwjgl3OnlyType(name) || name.startsWith("org.lwjgl."); + } + + private static boolean isStaticInit(String method) { + return method.startsWith("") || method.startsWith("lambda$static$"); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/compat/bop/BopFogTestSupport.java b/src/test/java/com/gtnewhorizons/angelica/compat/bop/BopFogTestSupport.java index 85c43ac27..6dd69c50a 100644 --- a/src/test/java/com/gtnewhorizons/angelica/compat/bop/BopFogTestSupport.java +++ b/src/test/java/com/gtnewhorizons/angelica/compat/bop/BopFogTestSupport.java @@ -81,15 +81,19 @@ final class BopFogTestSupport { } } + static T stub(Class type) { + return Mockito.mock(type, Mockito.withSettings().stubOnly()); + } + static World mockWorld(Grid grid) { - final World world = Mockito.mock(World.class); - final IChunkProvider provider = Mockito.mock(IChunkProvider.class); + final World world = stub(World.class); + final IChunkProvider provider = stub(IChunkProvider.class); when(world.getChunkProvider()).thenReturn(provider); when(provider.chunkExists(anyInt(), anyInt())).thenReturn(true); - final BiomeGenBase fogBiome = BopFog.mockBiome(); + final BiomeGenBase fogBiome = Mockito.mock(BiomeGenBase.class, Mockito.withSettings().extraInterfaces(BopFog.TYPE).stubOnly()); when(BopFog.stubCall(fogBiome)).thenAnswer(inv -> grid.colorAt(inv.getArgument(0), inv.getArgument(1), inv.getArgument(2))); - final BiomeGenBase plainBiome = Mockito.mock(BiomeGenBase.class); + final BiomeGenBase plainBiome = stub(BiomeGenBase.class); when(world.getBiomeGenForCoords(anyInt(), anyInt())).thenAnswer(inv -> grid.isFog(inv.getArgument(0), inv.getArgument(1)) ? fogBiome : plainBiome); return world; diff --git a/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogAggregateTest.java b/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogAggregateTest.java index 147de923f..b4e0bbda2 100644 --- a/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogAggregateTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogAggregateTest.java @@ -12,9 +12,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.mockWorld; +import static com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.stub; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; @@ -26,8 +26,8 @@ class FogAggregateTest { } private static World world(AtomicBoolean loaded, AtomicInteger color) { - final World world = mock(World.class); - final IChunkProvider provider = mock(IChunkProvider.class); + final World world = stub(World.class); + final IChunkProvider provider = stub(IChunkProvider.class); when(world.getChunkProvider()).thenReturn(provider); when(provider.chunkExists(anyInt(), anyInt())).thenAnswer(inv -> loaded.get()); final BiomeGenBase biome = BopFog.mockBiome(); @@ -146,7 +146,7 @@ class FogAggregateTest { final AtomicBoolean loaded = new AtomicBoolean(false); final World world = world(loaded, new AtomicInteger(0x010203)); final BiomeGenBase fog = world.getBiomeGenForCoords(0, 0); - when(world.getBiomeGenForCoords(anyInt(), anyInt())).thenReturn(mock(BiomeGenBase.class)); + when(world.getBiomeGenForCoords(anyInt(), anyInt())).thenReturn(stub(BiomeGenBase.class)); final float[] actual = new float[4]; FogColourGrid.accumulateInterior(world, 0, 64, 0, 1, actual); assertBits(new float[4], actual); diff --git a/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogColourGridTest.java b/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogColourGridTest.java index fc76a1528..93036d651 100644 --- a/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogColourGridTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/compat/bop/FogColourGridTest.java @@ -5,12 +5,12 @@ import net.minecraft.world.biome.BiomeGenBase; import net.minecraft.world.chunk.IChunkProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.util.function.IntBinaryOperator; import com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.BopFog; +import static com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.stub; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.times; @@ -25,8 +25,8 @@ class FogColourGridTest { } private static World mockWorld(boolean chunksLoaded) { - final World world = Mockito.mock(World.class); - final IChunkProvider provider = Mockito.mock(IChunkProvider.class); + final World world = stub(World.class); + final IChunkProvider provider = stub(IChunkProvider.class); when(world.getChunkProvider()).thenReturn(provider); when(provider.chunkExists(anyInt(), anyInt())).thenReturn(chunksLoaded); return world; diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/culling/SectionMetaBufferTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/culling/SectionMetaBufferTest.java index 87cd5de99..8c2230b28 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/culling/SectionMetaBufferTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/culling/SectionMetaBufferTest.java @@ -262,7 +262,7 @@ class SectionMetaBufferTest { }); try { - Thread.sleep(2000); + Thread.sleep(200); stop.set(true); writer.join(); reader.join(); diff --git a/src/test/resources/archunit.properties b/src/test/resources/archunit.properties deleted file mode 100644 index 0675408e0..000000000 --- a/src/test/resources/archunit.properties +++ /dev/null @@ -1,2 +0,0 @@ -# The arch tests only need names and descriptors off the imported bytecode. -resolveMissingDependenciesFromClassPath=false