Speed up tests and run SDL-GPU device tests on CI (#2253)

* Speed up tests and run SDL-GPU device tests on CI
* Arch rules share one ASM scan instead of ArchUnit
* Tests needing an SDL GPU device fail without one instead of skipping
* Replace checkstyle's wildcard-import check with a test
* Use the runner's preinstalled Temurin JDKs
* Cache build/rfg so the Minecraft decompile chain can be skipped
* Unpack LWJGL natives before tests
* Pin the shared workflow commit
This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-12 13:26:28 -07:00
1 parent 46f8157087
commit 85d0641a73
33 files changed
+544 -202

No files matched your search

+36 -1
View File
@@ -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
+43 -2
View File
@@ -89,8 +89,11 @@ tasks.register<Copy>("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<DowngradeFiles>("downgradeMainClasses")
val downgradeRootTestClasses = tasks.named<DowngradeFiles>("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<DowngradeFiles>("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")
+32
View File
@@ -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<UnpackLwjgl3Natives> {
val jars = the<SourceSetContainer>()["test"].runtimeClasspath.filter {
it.name.startsWith("lwjgl") && it.name.contains("-natives-") && !it.name.startsWith("lwjgl-platform-")
}
return tasks.register<UnpackLwjgl3Natives>("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<UnpackLwjgl3Natives>) {
classpath = project.files(unpack) + classpath
}
-1
View File
@@ -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 }
@@ -48,6 +48,7 @@ repositories {
}
tasks.withType<DowngradeFiles>().configureEach {
outputs.cacheIf { true }
downgradeTo.set(JavaVersion.VERSION_1_8)
multiReleaseOriginal.set(false)
multiReleaseVersions.set(emptySet())
+4
View File
@@ -183,6 +183,7 @@ dependencies {
}
tasks.withType<DowngradeFiles>().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
+1 -1
View File
@@ -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).
+4 -1
View File
@@ -6,7 +6,10 @@ import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
val libs = the<LibrariesForLibs>()
tasks.withType<DowngradeJar>().configureEach { logLevel.set("FATAL") }
tasks.withType<DowngradeFiles>().configureEach { logLevel.set("FATAL") }
tasks.withType<DowngradeFiles>().configureEach {
logLevel.set("FATAL")
outputs.cacheIf { true }
}
configurations.all {
resolutionStrategy.dependencySubstitution {
+2 -3
View File
@@ -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" }
+38 -8
View File
@@ -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<Test>().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<JavaCompile>("compileJava") {
val classesDir = destinationDirectory
@@ -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);
@@ -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));
@@ -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());
@@ -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
@@ -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;
@@ -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;
@@ -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 {
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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<String> 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<String> annotations, Set<String> references, List<Member> methods, List<Access> accesses, List<ClassConstant> classConstants) {
}
private static final List<ScannedClass> CLASSES = scan();
private static final Map<String, ScannedClass> BY_NAME = index(CLASSES);
private ArchScan() {
}
static List<ScannedClass> in(Dir... dirs) {
final List<Dir> wanted = Arrays.asList(dirs);
final List<ScannedClass> selected = new ArrayList<>();
for (ScannedClass c : CLASSES) {
if (wanted.contains(c.dir())) selected.add(c);
}
return selected;
}
static List<String> signatureTypes(String desc) {
final List<String> 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<String, ScannedClass> index(List<ScannedClass> classes) {
final Map<String, ScannedClass> byName = new HashMap<>();
for (ScannedClass c : classes) {
byName.put(c.name(), c);
}
return byName;
}
private static List<ScannedClass> scan() {
final List<ScannedClass> 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<Path> files = Files.walk(root)) {
for (final Iterator<Path> 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<String> 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<String> annotations = new HashSet<>();
if (node.visibleAnnotations != null) {
for (AnnotationNode annotation : node.visibleAnnotations) {
annotations.add(Type.getType(annotation.desc).getClassName());
}
}
final List<Member> methods = new ArrayList<>();
final List<Access> accesses = new ArrayList<>();
final List<ClassConstant> 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();
}
}
@@ -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<String> 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));
}
}
@@ -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<ScannedClass> classes = ArchScan.in(Dir.values());
@Test
void awareClassesDoNotReadRemappedMembers() {
final List<String> 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<String> 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<String> 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<String> violations = new ArrayList<>();
for (JavaMethod method : renderBackend.getMethods()) {
final List<JavaClass> 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<JavaClass> descriptorTypes(AccessTarget target) {
if (target instanceof AccessTarget.FieldAccessTarget field) {
return List.of(field.getRawType());
}
if (target instanceof AccessTarget.CodeUnitCallTarget call) {
final List<JavaClass> 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();
}
}
@@ -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<String> EXEMPT_PACKAGES = List.of(
"com.gtnewhorizons.angelica.glsm.backend.",
"com.gtnewhorizons.angelica.loading.");
@Test
void noDirectGLCalls() {
NO_DIRECT_GL_CALLS.check(classes);
final List<String> 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("<init>")) 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));
}
}
@@ -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<String> 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<String> violations = new ArrayList<>();
for (String dir : SOURCE_DIRS) {
final List<Path> sources;
try (Stream<Path> 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));
}
}
@@ -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<JavaClass> 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<JavaClass> 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<String> 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<String> 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<JavaStaticInitializer> 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("<clinit>")), GATE + " has no static initializer to check; the scan path is likely wrong");
final List<String> 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<String> referencesOutsideSdlGpu(Predicate<String> forbidden) {
final List<String> 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("<clinit>") || method.startsWith("lambda$static$");
}
}
@@ -81,15 +81,19 @@ final class BopFogTestSupport {
}
}
static <T> T stub(Class<T> 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;
@@ -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);
@@ -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;
@@ -262,7 +262,7 @@ class SectionMetaBufferTest {
});
try {
Thread.sleep(2000);
Thread.sleep(200);
stop.set(true);
writer.join();
reader.join();
-2
View File
@@ -1,2 +0,0 @@
# The arch tests only need names and descriptors off the imported bytecode.
resolveMissingDependenciesFromClassPath=false