Every once in a while you go back to your toolbox and discover something shiny you haven’t shown the world before. I have a client who needed some OpenGL work done on Android, which brings up the whole question of how to unit test OpenGL code. I had some support classes for this lurking around from some earlier work, so I thought I’d pass them along.
Those of you who write unit tests know that global state is the bane of our lives. OpenGL, unfortunately, is all about global state. We make calls to the OpenGL API’s, but we rely on the fact that there’s an environment set up behind us and, in the case of OpenGL ES 2.0, this relies on thread-local variables and other interesting stuff. While it’s technically possible to manually set up an entire OpenGL ES 2.0 environment for unit testing purposes, this is a real nuisance. Wouldn’t it be much simpler if we could somehow let Android do the work for us? Hence the shiny tool, the code for which can be found at https://github.com/SilverBayTech/AndroidOpenGLUnitTesting.
To review, what we would want to have happen is the following:
- We need to get a
GLSurfaceView
set up. - We need to let the
GLSurfaceView
get the OpenGL thread that it uses to call the renderer running, and the whole OpenGL environment set up. The environment is obviously important in both OpenGL ES 1.0 and 2.0, but the renderer is the only (easy) way to get to theGL10
object for OpenGL ES 1.0 projects. - We then need to get our test code to run in that OpenGL thread.
- And we want to trigger all of that from a standard JUnit-type test
Fortunately, the GLSurfaceView
class uses a message queuing approach. Thus, we can get our test code run for us by wrapping the test in an appropriate Runnable
and then putting it on the GLSurfaceView
queue. This will cause that Runnable
to be executed on the OpenGL thread. We do have to be careful as to when we queue that event, however, as there are setup events that have to happen first. One way to know that the whole environment is set up is when the calls to the renderer start being made. In addition, we need to wait until after the code has been executed before we return from our test case.
So here’s how we tackle this.
Step 1 – A Unit Test Activity
We create an activity in the application project that looks like this:
public class OpenGLES20UnitTestActivity extends Activity { private GLSurfaceView surfaceView; private CountDownLatch latch; public OpenGLUnitTestActivity() { latch = new CountDownLatch(1); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); surfaceView = new GLSurfaceView(this); surfaceView.setEGLContextClientVersion(2); surfaceView.setRenderer(new EmptyRenderer(latch)); setContentView(surfaceView); } @Override protected void onResume() { super.onResume(); surfaceView.onResume(); } @Override protected void onPause() { super.onPause(); surfaceView.onPause(); } public GLSurfaceView getSurfaceView() throws InterruptedException { if (latch != null) { latch.await(); latch = null; } return surfaceView; } private static class EmptyRenderer implements GLSurfaceView.Renderer { private CountDownLatch latch; public EmptyRenderer(CountDownLatch latch) { this.latch = latch; } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { } @Override public void onDrawFrame(GL10 gl) { if (latch != null) { latch.countDown(); latch = null; } } } }
All this activity does is set up a GLSurfaceView
with a dummy renderer, and make available a getSurfaceView
method that will return the GLSurfaceView
, but will block until the renderer has actually been called. (For brevity, the comments in the source code have been removed here, but you’ll probably be able to follow it.)
Step 2 – A Unit Test Base Class
Next, we create a base class for our OpenGL tests. This class goes into the unit test project, not the application project.
public class BaseOpenGLES20UnitTest extends ActivityInstrumentationTestCase2<OpenGLES20UnitTestActivity> { private OpenGLES20UnitTestActivity activity; public BaseOpenGLES20UnitTest() { super(OpenGLES20UnitTestActivity); } @Override public void setUp() { activity = getActivity(); } @Override public void tearDown() { activity.finish(); } public void runOnGLThread(final TestWrapper test) throws Throwable { final CountDownLatch latch = new CountDownLatch(1); activity.getSurfaceView().queueEvent(new Runnable() { public void run() { test.executeWrapper(); latch.countDown(); } }); latch.await(); test.rethrowExceptions(); } public static abstract class TestWrapper { private Error error = null; private Throwable throwable = null; public TestWrapper() { } public void executeWrapper() { try { executeTest(); } catch (Error e) { synchronized (this) { error = e; } } catch (Throwable t) { synchronized (this) { throwable = t; } } } public void rethrowExceptions() { synchronized (this) { if (error != null) { throw error; } if (throwable != null) { throw new RuntimeException("Unexpected exception", throwable); } } } public abstract void executeTest() throws Throwable; } }
As you can see, this is a standard ActivityInstrumentationTestCase2
-based test class that will use the activity we’ve created, and will handle getting the activity (and thus the GLSurfaceView
up and running, and then torn down afterwards.
The key method is runOnGLThread
, which is what will be called from the JUnit tests. It takes the test (which is implemented as an anonymous class derived from TestWrapper
), queues it to be run by the GLSurfaceView
, and waits for the test to finish. The TestWrapper
catches any exceptions that are thrown (assertions from JUnit, or Other Bad Things) and then re-throws them into the thread executing the JUnit test.
An Example
Suppose we were trying to unit test a bit of utility code for compiling shaders:
public class ShaderUtils { public static int compileShader(int type, String shaderCode) { int shaderHandle = GLES20.glCreateShader(type); GLES20.glShaderSource(shaderHandle, shaderCode); GLES20.glCompileShader(shaderHandle); int[] compileStatus = new int[1]; GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0); if (compileStatus[0] == 0) { StringBuilder builder = new StringBuilder(); builder.append("Error compiling "); builder.append(type == GLES20.GL_VERTEX_SHADER ? "vertex" : "fragment"); builder.append(" shader: "); builder.append(GLES20.glGetShaderInfoLog(shaderHandle)); Log.e("ShaderUtils", builder.toString()); GLES20.glDeleteShader(shaderHandle); return 0; } return shaderHandle; } }
This should look pretty familiar to anybody doing OpenGL ES 2.0. At minimum, our unit test should verify that valid code compiles (resulting in a valid, non-zero shader handle) and that invalid code fails to compile, and results in a return code of zero.
In this case, our unit test could look like this:
public class ShaderUtilsTest extends BaseOpenGLES20UnitTest { private static final String validFragmentShaderCode = "precision mediump float; \n" + "void main() \n" + "{ \n" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); \n" + "} \n"; private static final String invalidFragmentShaderCode = "precision mediump float; \n" + "void main() \n" + "{ \n" + " syntax error \n" + "} \n"; public ShaderUtilsTest() { } public void test_compile_withValidCode_compiles() throws Throwable { runOnGLThread(new TestWrapper() { @Override public void executeTest() throws Throwable { int handle = ShaderUtils.compileShader(GLES20.GL_FRAGMENT_SHADER, validFragmentShaderCode); assertTrue(handle != 0); assertTrue(GLES20.glIsShader(handle)); } }); } public void test_compile_withInvalidCode_fails() throws Throwable { runOnGLThread(new TestWrapper() { @Override public void executeTest() throws Throwable { int handle = ShaderUtils.compileShader(GLES20.GL_FRAGMENT_SHADER, invalidFragmentShaderCode); assertTrue(handle == 0); } }); } }
Outside of the fact that we have to have the additional test wrapper, everything works nicely. Since the code that is inside the wrapper is run in the OpenGL context, it can make direct calls to OpenGL routines, like the call to GLES20.glIsShader
in the first test, to verify that the “post” state of the OpenGL context is correct. Each individual test iteration fires up the activity when it begins and shuts it down when it ends, so each test has a virgin OpenGL context, just the way that GLSurfaceView
initializes it.
Summary
Thus, if you want to use this:
- Copy the
OpenGLES20UnitTestActivity
class into your application project - Make an entry for
OpenGLES20UnitTestActivity
in yourAndroidManifest.xml
. - Copy the
BaseOpenGLES20UnitTest
class into your test project - Start writing unit tests
A few notes:
- The classes I’ve shown above are for OpenGL ES 2.0. If you’re stuck in a time warp and still coding for earlier versions of OpenGL, the GitHub project contains equivalent classes for 1.0. These differ in that they set the
GLSurfaceView
to use version 1, plus they grab theGL10
object from the renderer and then pass it into theexecuteTest
method for you. - The code in the activity to use the
CountDownLatch
once and then null it out is pure paranoia. It should work without that, but I’m sometimes paranoid about edge cases like continuing to decrement a latch when it has already reached zero. - The more observant of you may have noted that this implies that the
OpenGLESXXUnitTestActivity
will be in your application when you compile it for release. True. At one level, this is (IMHO) a small price to pay. It won’t ever get invoked by your application – its only ever fired up by the test case – but it’s there. If you’re really concerned about the few extra bytes this will consume in your application, you can always tweak your ProGuard configuration so that it does not protect this particular class. ProGuard will find it as not used, and strip it out. Leaving it in thecom.silverbaytech.android.openglunittesting
package might make this easier.
Android OpenGL Unit Testing originally appeared on http://www.silverbaytech.com/blog/.