JUnit GL Extension
GLTestExtension
This is an extension class for JUnit to enable OpenGL unit tests. The implementation is from an all-right-reserved project but this class is authored by me (tttsaurus); I thereby grant the free usage of this class.
Note:
A new OpenGL context will be created and disposed for every test class (not test method!)
Usage: annotate the test class with
@ExtendWith(GLTestExtension.class)and wrap your test logic withGLTestExtension.submit(() -> { ... }).join();and preferably
GLTestExtension.submit(() -> { GLTestExtension.assumeGL46(); ... }).join();You might want to order your test methods due to GL state changes
java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.NonNull;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.system.MemoryUtil;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author tttsaurus
*/
public class GLTestExtension implements BeforeAllCallback, AfterAllCallback {
private static final Logger LOGGER = LogManager.getLogger("JUnit GL Test");
private static final ReentrantLock GLOBAL_GL_LOCK = new ReentrantLock(true);
private static long window;
private static ExecutorService glThread = null;
public static Logger logger() {
return LOGGER;
}
private static int versionMajor = -1;
private static int versionMinor = -1;
public static int getVersionMajor() {
return versionMajor;
}
public static int getVersionMinor() {
return versionMinor;
}
public static void assumeGL46() {
Assumptions.assumeTrue(versionMajor == 4 && versionMinor == 6);
}
private void initGL() {
versionMajor = -1;
versionMinor = -1;
glThread = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "GL Test Thread");
t.setDaemon(true);
return t;
});
submit(() -> {
GLFWErrorCallback.createPrint(System.err).set();
if (!GLFW.glfwInit()) {
throw new IllegalStateException("\"GLFW.glfwInit\" failed.");
}
GLFW.glfwDefaultWindowHints();
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
window = GLFW.glfwCreateWindow(
1, 1,
"gl-tests",
MemoryUtil.NULL,
MemoryUtil.NULL);
if (window == MemoryUtil.NULL) {
throw new IllegalStateException("\"GLFW.glfwCreateWindow\" failed.");
}
GLFW.glfwMakeContextCurrent(window);
GL.createCapabilities();
GLFW.glfwSwapInterval(0);
LOGGER.info("GL context initialized.");
String rawGLVersion = GL11.glGetString(GL11.GL_VERSION);
if (rawGLVersion != null) {
String[] parts = rawGLVersion.split("\\s+")[0].split("\\.");
if (parts.length >= 2) {
try {
versionMajor = Integer.parseInt(parts[0]);
versionMinor = Integer.parseInt(parts[1]);
} catch (NumberFormatException ignored) {
}
}
} else {
rawGLVersion = "";
}
LOGGER.info("OpenGL version: {}", rawGLVersion);
if (rawGLVersion.isEmpty() || versionMajor == -1 || versionMinor == -1) {
throw new RuntimeException("Failed to parse the OpenGL version.");
}
LOGGER.info("Parsed OpenGL version: {}.{}", versionMajor, versionMinor);
}).join();
}
private void destroyGL() {
if (glThread == null) {
return;
}
submit(() -> {
if (window != MemoryUtil.NULL) {
GLFW.glfwMakeContextCurrent(MemoryUtil.NULL);
GL.setCapabilities(null);
GLFW.glfwDestroyWindow(window);
window = MemoryUtil.NULL;
}
GLFW.glfwTerminate();
LOGGER.info("GL context destroyed.");
}).join();
versionMajor = -1;
versionMinor = -1;
glThread.shutdown();
try {
if (!glThread.awaitTermination(2, TimeUnit.SECONDS)) {
glThread.shutdownNow();
}
} catch (InterruptedException e) {
glThread.shutdownNow();
Thread.currentThread().interrupt();
} finally {
LOGGER.info("GL thread shutdown.");
}
}
@Override
public void beforeAll(@NonNull ExtensionContext context) throws Exception {
GLOBAL_GL_LOCK.lock();
try {
initGL();
} catch (Throwable t) {
GLOBAL_GL_LOCK.unlock();
throw t;
}
}
@Override
public void afterAll(@NonNull ExtensionContext context) throws Exception {
try {
destroyGL();
} finally {
GLOBAL_GL_LOCK.unlock();
}
}
public static CompletableFuture<Void> submit(Runnable r) {
Executor executor = glThread;
if (executor == null) {
CompletableFuture<Void> f = new CompletableFuture<>();
f.completeExceptionally(new IllegalStateException("GLTestExtension not initialized yet."));
return f;
}
return CompletableFuture.runAsync(r, executor);
}
public static <T> CompletableFuture<T> submit(Callable<T> c) {
Executor executor = glThread;
if (executor == null) {
CompletableFuture<T> f = new CompletableFuture<>();
f.completeExceptionally(new IllegalStateException("GLTestExtension not initialized yet."));
return f;
}
return CompletableFuture.supplyAsync(() -> {
try {
return c.call();
} catch (Exception e) {
throw new CompletionException(e);
}
}, executor);
}
}