/** * Copyright 2010 JogAmp Community. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of JogAmp Community. */ package com.jogamp.opengl.test.junit.util; import java.io.BufferedReader; import java.io.File; import java.io.OutputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicInteger; import com.jogamp.nativewindow.NativeWindowFactory; import com.jogamp.opengl.GL; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilitiesImmutable; import com.jogamp.opengl.GLDrawable; import com.jogamp.opengl.GLEventListener; import com.jogamp.junit.util.SingletonJunitCase; import com.jogamp.opengl.util.GLReadBufferUtil; import com.jogamp.opengl.util.texture.TextureIO; import org.junit.BeforeClass; import org.junit.AfterClass; import org.junit.FixMethodOrder; import org.junit.runners.MethodSorters; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.TestClass; @FixMethodOrder(MethodSorters.NAME_ASCENDING) public abstract class UITestCase extends SingletonJunitCase { private static volatile boolean resetXRandRIfX11AfterClass = false; private static volatile int maxMethodNameLen = 0; public static void setResetXRandRIfX11AfterClass() { resetXRandRIfX11AfterClass = true; } /** * Iterates through all outputs and sets the preferred mode and normal rotation using RandR 1.3. * <p> * With NV drivers, one need to add the Modes in proper order to the Screen's Subsection "Display", * otherwise they are either in unsorted resolution order or even n/a! * </p> * @return error-code with {@code zero} for no error */ @SuppressWarnings("unused") public static int resetXRandRIfX11() { int errorCode = 0; if( NativeWindowFactory.isInitialized() && NativeWindowFactory.TYPE_X11 == NativeWindowFactory.getNativeWindowType(true) ) { try { final List<String> outputDevices = new ArrayList<String>(); // final List<String> outputSizes = new ArrayList<String>(); final StringBuilder out = new StringBuilder(); final String[] cmdlineQuery = new String[] { "xrandr", "-q" }; errorCode = processCommand(cmdlineQuery, null, out, "xrandr-query> "); if( 0 != errorCode ) { System.err.println("XRandR Query Error Code "+errorCode); System.err.println(out.toString()); } else { // Parse connected output devices ! final BufferedReader in = new BufferedReader( new StringReader( out.toString() ) ); String line = null; while ( ( line = in.readLine() ) != null) { final String lline = line.toLowerCase(); if( lline.contains("connected") && !lline.contains("disconnected") ) { final String od = getFirst(line); if( null != od ) { outputDevices.add( od ); /** if ( ( line = in.readLine() ) != null ) { outputSizes.add( getFirst(line) ); } else { outputSizes.add( null ); } */ } } } for(int i=0; i<outputDevices.size(); i++) { final String outputDevice = outputDevices.get(i); final String outputSize = null; // outputSizes.get(i) final String[] cmdline; if( null != outputSize ) { cmdline = new String[] { "xrandr", "--output", outputDevice, "--mode", outputSize, "--rotate", "normal" }; } else { cmdline = new String[] { "xrandr", "--output", outputDevice, "--preferred", "--rotate", "normal" }; } System.err.println("XRandR 1.2 Reset: "+Arrays.asList(cmdline)); errorCode = processCommand(cmdline, System.err, null, "xrandr-1.2-reset> "); if( 0 != errorCode ) { System.err.println("XRandR 1.2 Reset Error Code "+errorCode); break; } } /** * RandR 1.1 reset does not work .. if( 0 != errorCode ) { final String[] cmdline = new String[] { "xrandr", "-s", "0", "-o", "normal" }; System.err.println("XRandR 1.1 Reset: "+Arrays.asList(cmdline)); errorCode = processCommand(cmdline, System.err, null, "xrandr-1.1-reset> "); if( 0 != errorCode ) { System.err.println("XRandR 1.1 Reset Error Code "+errorCode); } } */ } } catch (final Exception e) { System.err.println("Caught "+e.getClass().getName()+": "+e.getMessage()); e.printStackTrace(); errorCode = -1; } } return errorCode; } private static String getFirst(final String line) { final StringTokenizer tok = new StringTokenizer(line); if( tok.hasMoreTokens() ) { final String s = tok.nextToken().trim(); if( s.length() > 0 ) { return s; } } return null; } public static int processCommand(final String[] cmdline, final OutputStream outstream, final StringBuilder outstring, final String outPrefix) { int errorCode = 0; final Object ioSync = new Object(); try { synchronized ( ioSync ) { final ProcessBuilder pb = new ProcessBuilder(cmdline); pb.redirectErrorStream(true); final Process p = pb.start(); final MiscUtils.StreamDump dump; if( null != outstream ) { dump = new MiscUtils.StreamDump( outstream, outPrefix, p.getInputStream(), ioSync); } else if( null != outstring ) { dump = new MiscUtils.StreamDump( outstring, outPrefix, p.getInputStream(), ioSync); } else { throw new IllegalArgumentException("Output stream and string are null"); } dump.start(); while( !dump.eos() ) { ioSync.wait(); } p.waitFor(); // should be fine by now .. errorCode = p.exitValue(); } } catch (final Exception e) { System.err.println("Caught "+e.getClass().getName()+": "+e.getMessage()); e.printStackTrace(); errorCode = Integer.MIN_VALUE; } return errorCode; } public int getMaxTestNameLen() { if(0 == maxMethodNameLen) { int ml = 0; final TestClass tc = new TestClass(getClass()); final List<FrameworkMethod> testMethods = tc.getAnnotatedMethods(org.junit.Test.class); for(final Iterator<FrameworkMethod> iter=testMethods.iterator(); iter.hasNext(); ) { final int l = iter.next().getName().length(); if( ml < l ) { ml = l; } } maxMethodNameLen = ml; } return maxMethodNameLen; } @BeforeClass public static final void oneTimeSetUpUITest() { // one-time initialization code } @AfterClass public static final void oneTimeTearDownUITest() { // one-time cleanup code if( resetXRandRIfX11AfterClass ) { resetXRandRIfX11(); } } public String getSnapshotFilename(final int sn, String postSNDetail, final GLCapabilitiesImmutable caps, final int width, final int height, final boolean sinkHasAlpha, String fileSuffix, final String destPath) { if(null == fileSuffix) { fileSuffix = TextureIO.PNG; } final int maxSimpleTestNameLen = getMaxTestNameLen()+getClass().getSimpleName().length()+1; final String simpleTestName = this.getSimpleTestName("."); final String filenameBaseName; { final String accel = caps.getHardwareAccelerated() ? "hw" : "sw" ; final String scrnm; if(caps.isOnscreen()) { scrnm = "onscreen"; } else if(caps.isFBO()) { scrnm = "fbobject"; } else if(caps.isPBuffer()) { scrnm = "pbuffer_"; } else if(caps.isBitmap()) { scrnm = "bitmap__"; } else { scrnm = "unknown_"; } final String dblb = caps.getDoubleBuffered() ? "dbl" : "one"; final String F_pfmt = sinkHasAlpha ? "rgba" : "rgb_"; final String pfmt = "rgba" + caps.getRedBits() + caps.getGreenBits() + caps.getBlueBits() + caps.getAlphaBits(); final int depthBits = caps.getDepthBits(); final int stencilBits = caps.getStencilBits(); final int samples = caps.getNumSamples() ; final String aaext = caps.getSampleExtension(); postSNDetail = null != postSNDetail ? "-"+postSNDetail : ""; filenameBaseName = String.format("%-"+maxSimpleTestNameLen+"s-n%04d%s-%-6s-%s-%s-B%s-F%s_I%s-D%02d-St%02d-Sa%02d_%s-%04dx%04d.%s", simpleTestName, sn, postSNDetail, caps.getGLProfile().getName(), accel, scrnm, dblb, F_pfmt, pfmt, depthBits, stencilBits, samples, aaext, width, height, fileSuffix).replace(' ', '_'); } return null != destPath ? destPath + File.separator + filenameBaseName : filenameBaseName; } /** * Takes a snapshot of the drawable's current front framebuffer. Example filenames: * <pre> * TestGLDrawableAutoDelegateOnOffscrnCapsNEWT.testES2OffScreenFBOSglBuf____-n0001-msaa0-GLES2_-sw-fbobject-Bdbl-Frgb__Irgba8888_-D24-St00-Sa00_default-0400x0300.png * TestGLDrawableAutoDelegateOnOffscrnCapsNEWT.testES2OffScreenPbufferDblBuf-n0003-msaa0-GLES2_-sw-pbuffer_-Bdbl-Frgb__Irgba8880-D24-St00-Sa00_default-0200x0150.png * TestGLDrawableAutoDelegateOnOffscrnCapsNEWT.testGL2OffScreenPbufferSglBuf-n0003-msaa0-GL2___-hw-pbuffer_-Bone-Frgb__Irgba5551-D24-St00-Sa00_default-0200x0150.png * </pre> * @param sn sequential number * @param postSNDetail optional detail to be added to the filename after <code>sn</code> * @param gl the current GL context object. It's read drawable is being used as the pixel source and to gather some details which will end up in the filename. * @param readBufferUtil the {@link GLReadBufferUtil} to be used to read the pixels for the screenshot. * @param fileSuffix Optional file suffix without a <i>dot</i> defining the file type, i.e. <code>"png"</code>. * If <code>null</code> the <code>"png"</code> as defined in {@link TextureIO#PNG} is being used. * @param destPath Optional platform dependent file path. It shall use {@link File#separatorChar} as is directory separator. * It shall not end with a directory separator, {@link File#separatorChar}. * If <code>null</code> the current working directory is being used. */ public void snapshot(final int sn, final String postSNDetail, final GL gl, final GLReadBufferUtil readBufferUtil, final String fileSuffix, final String destPath) { final GLDrawable drawable = gl.getContext().getGLReadDrawable(); final String filename = getSnapshotFilename(sn, postSNDetail, drawable.getChosenGLCapabilities(), drawable.getSurfaceWidth(), drawable.getSurfaceHeight(), readBufferUtil.hasAlpha(), fileSuffix, destPath); System.err.println(Thread.currentThread().getName()+": ** screenshot: "+filename); gl.glFinish(); // just make sure rendering finished .. try { snapshot(gl, readBufferUtil, filename); } catch (final ClassNotFoundException cnfe) { // Texture class belongs to jogl-util.jar which my not be included in test environment! System.err.println("Caught ClassNotFoundException: "+cnfe.getMessage()); } catch (final NoClassDefFoundError cnfe) { // Texture class belongs to jogl-util.jar which my not be included in test environment! System.err.println("Caught NoClassDefFoundError: "+cnfe.getMessage()); } } private void snapshot(final GL gl, final GLReadBufferUtil readBufferUtil, final String filename) throws ClassNotFoundException, NoClassDefFoundError { if(readBufferUtil.readPixels(gl, false)) { readBufferUtil.write(new File(filename)); } } public class SnapshotGLEventListener implements GLEventListener { private final GLReadBufferUtil screenshot; private volatile boolean makeShot = false; private volatile boolean makeShotAlways = false; private volatile boolean verbose = false; private final AtomicInteger displayCount = new AtomicInteger(0); private final AtomicInteger reshapeCount = new AtomicInteger(0); private volatile String postSNDetail = null; public SnapshotGLEventListener(final GLReadBufferUtil screenshot) { this.screenshot = screenshot; } public SnapshotGLEventListener() { this.screenshot = new GLReadBufferUtil(false, false); } public int getDisplayCount() { return displayCount.get(); } public int getReshapeCount() { return reshapeCount.get(); } public GLReadBufferUtil getGLReadBufferUtil() { return screenshot; } public void init(final GLAutoDrawable drawable) {} public void dispose(final GLAutoDrawable drawable) {} public void display(final GLAutoDrawable drawable) { final GL gl = drawable.getGL(); final boolean _makeShot = makeShot || makeShotAlways; if(verbose) { System.err.println(Thread.currentThread().getName()+": ** display: "+displayCount+": "+drawable.getSurfaceWidth()+"x"+drawable.getSurfaceHeight()+", makeShot "+_makeShot); } if(_makeShot) { makeShot=false; snapshot(displayCount.get(), postSNDetail, gl, screenshot, TextureIO.PNG, null); } displayCount.incrementAndGet(); } public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) { if(verbose) { System.err.println(Thread.currentThread().getName()+": ** reshape: "+reshapeCount+": "+width+"x"+height+" - "+drawable.getSurfaceWidth()+"x"+drawable.getSurfaceHeight()); } reshapeCount.incrementAndGet(); } public void setMakeSnapshot() { makeShot=true; } public void setMakeSnapshotAlways(final boolean v) { makeShotAlways=v; } public void setVerbose(final boolean v) { verbose=v; } public void setPostSNDetail(final String v) { postSNDetail = v; } }; }