diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/OffScreenshotAppState.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/OffScreenshotAppState.java new file mode 100644 index 0000000000..8abeef9dde --- /dev/null +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/OffScreenshotAppState.java @@ -0,0 +1,58 @@ +package org.jmonkeyengine.screenshottests.testframework; + +import com.jme3.app.Application; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import com.jme3.renderer.Renderer; +import com.jme3.system.JmeSystem; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Texture2D; +import com.jme3.util.BufferUtils; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Optional; + +public class OffScreenshotAppState extends AbstractAppState{ + + private final Texture2D renderTexture; + private Renderer renderer; + private final FrameBuffer frameBuffer; + private Optional capture = Optional.empty(); + + private ByteBuffer outBuf; + + public void takeScreenshot(Path pathToSaveTo) { + capture = Optional.of(pathToSaveTo); + } + + public OffScreenshotAppState(Texture2D renderTexture, FrameBuffer frameBuffer) { + this.renderTexture = renderTexture; + this.frameBuffer = frameBuffer; + } + + @Override + public void initialize(AppStateManager stateManager, Application app) { + super.initialize(stateManager, app); + renderer = app.getRenderManager().getRenderer(); + outBuf = BufferUtils.createByteBuffer(renderTexture.getImage().getWidth() * renderTexture.getImage().getHeight() * 4); + } + + @Override + public void postRender() { + super.postRender(); + if (capture.isPresent()) { + + renderer.readFrameBuffer(frameBuffer, outBuf); + try (FileOutputStream fileOutBuf = new FileOutputStream(capture.get().toFile())){ + + JmeSystem.writeImageFile(fileOutBuf, "png",outBuf, renderTexture.getImage().getWidth(), renderTexture.getImage().getHeight()); + }catch (IOException e) { + throw new RuntimeException(e); + } + capture = Optional.empty(); + } + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/PixelSamenessDegree.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/PixelSamenessDegree.java index 3fcc2abaed..c2e7a6ef63 100644 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/PixelSamenessDegree.java +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/PixelSamenessDegree.java @@ -38,7 +38,7 @@ */ public enum PixelSamenessDegree{ SAME(1, null), - NEGLIGIBLY_DIFFERENT(1, ColorRGBA.Green), + NEGLIGIBLY_DIFFERENT(3, ColorRGBA.Green), SUBTLY_DIFFERENT(10, ColorRGBA.Blue), MEDIUMLY_DIFFERENT(20, ColorRGBA.Yellow), diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScenarioScreenshotRecorder.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScenarioScreenshotRecorder.java new file mode 100644 index 0000000000..e4fb3915e7 --- /dev/null +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScenarioScreenshotRecorder.java @@ -0,0 +1,35 @@ +package org.jmonkeyengine.screenshottests.testframework; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ScenarioScreenshotRecorder { + + /** + * scenario name -> frame number -> screenshot path + */ + Map> screenshotsAtFrames = new HashMap<>(); + + public void recordScreenshot(String scenarioName, int frameNumber, Path screenshotPath){ + screenshotsAtFrames.computeIfAbsent(scenarioName, k -> new HashMap<>()).put(frameNumber, screenshotPath); + } + + public Optional getScreenshotsAtFrame(String scenarioName, int frameNumber){ + if(!screenshotsAtFrames.containsKey(scenarioName) || !screenshotsAtFrames.get(scenarioName).containsKey(frameNumber)){ + return Optional.empty(); + }else{ + return Optional.of(screenshotsAtFrames.get(scenarioName).get(frameNumber)); + } + } + + public void addAll(ScenarioScreenshotRecorder other) { + for (Map.Entry> scenarioEntry : other.screenshotsAtFrames.entrySet()) { + String scenarioName = scenarioEntry.getKey(); + for (Map.Entry frameEntry : scenarioEntry.getValue().entrySet()) { + recordScreenshot(scenarioName, frameEntry.getKey(), frameEntry.getValue()); + } + } + } +} diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotNoInputAppState.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotNoInputAppState.java deleted file mode 100644 index 2817bb0e73..0000000000 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotNoInputAppState.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright (c) 2024 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * 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. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "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 THE COPYRIGHT OWNER 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. - */ -package org.jmonkeyengine.screenshottests.testframework; - -import com.jme3.app.Application; -import com.jme3.app.state.AbstractAppState; -import com.jme3.app.state.AppStateManager; -import com.jme3.input.controls.ActionListener; -import com.jme3.post.SceneProcessor; -import com.jme3.profile.AppProfiler; -import com.jme3.renderer.Camera; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.Renderer; -import com.jme3.renderer.ViewPort; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.system.JmeSystem; -import com.jme3.texture.FrameBuffer; -import com.jme3.util.BufferUtils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * This is more or less the same as ScreenshotAppState but without the keyboard input - * (because in a headless environment, there is no keyboard and trying to configure it caused - * errors). - * - * @author Richard Tingle (aka richtea) - * - */ -public class ScreenshotNoInputAppState extends AbstractAppState implements ActionListener, SceneProcessor { - - private static final Logger logger = Logger.getLogger(ScreenshotNoInputAppState.class.getName()); - private String filePath; - private boolean capture = false; - private boolean numbered = true; - private Renderer renderer; - private RenderManager rm; - private ByteBuffer outBuf; - private String shotName; - private long shotIndex = 0; - private int width, height; - - /** - * ViewPort to which the SceneProcessor is attached - */ - private ViewPort last; - - /** - * Using this constructor, the screenshot files will be written sequentially to the system - * default storage folder. - */ - public ScreenshotNoInputAppState() { - this(null); - } - - /** - * This constructor allows you to specify the output file path of the screenshot. - * Include the separator at the end of the path. - * Use an empty string to use the application folder. Use NULL to use the system - * default storage folder. - * @param filePath The screenshot file path to use. Include the separator at the end of the path. - */ - public ScreenshotNoInputAppState(String filePath) { - this.filePath = filePath; - } - - /** - * This constructor allows you to specify the output file path of the screenshot. - * Include the separator at the end of the path. - * Use an empty string to use the application folder. Use NULL to use the system - * default storage folder. - * @param filePath The screenshot file path to use. Include the separator at the end of the path. - * @param fileName The name of the file to save the screenshot as. - */ - public ScreenshotNoInputAppState(String filePath, String fileName) { - this.filePath = filePath; - this.shotName = fileName; - } - - /** - * This constructor allows you to specify the output file path of the screenshot and - * a base index for the shot index. - * Include the separator at the end of the path. - * Use an empty string to use the application folder. Use NULL to use the system - * default storage folder. - * @param filePath The screenshot file path to use. Include the separator at the end of the path. - * @param shotIndex The base index for screenshots. The first screenshot will have - * shotIndex + 1 appended, the next shotIndex + 2, and so on. - */ - public ScreenshotNoInputAppState(String filePath, long shotIndex) { - this.filePath = filePath; - this.shotIndex = shotIndex; - } - - /** - * This constructor allows you to specify the output file path of the screenshot and - * a base index for the shot index. - * Include the separator at the end of the path. - * Use an empty string to use the application folder. Use NULL to use the system - * default storage folder. - * @param filePath The screenshot file path to use. Include the separator at the end of the path. - * @param fileName The name of the file to save the screenshot as. - * @param shotIndex The base index for screenshots. The first screenshot will have - * shotIndex + 1 appended, the next shotIndex + 2, and so on. - */ - public ScreenshotNoInputAppState(String filePath, String fileName, long shotIndex) { - this.filePath = filePath; - this.shotName = fileName; - this.shotIndex = shotIndex; - } - - /** - * Set the file path to store the screenshot. - * Include the separator at the end of the path. - * Use an empty string to use the application folder. Use NULL to use the system - * default storage folder. - * @param filePath File path to use to store the screenshot. Include the separator at the end of the path. - */ - public void setFilePath(String filePath) { - this.filePath = filePath; - } - - /** - * Set the file name of the screenshot. - * @param fileName File name to save the screenshot as. - */ - public void setFileName(String fileName) { - this.shotName = fileName; - } - - /** - * Sets the base index that will used for subsequent screenshots. - * - * @param index the desired base index - */ - public void setShotIndex(long index) { - this.shotIndex = index; - } - - /** - * Sets if the filename should be appended with a number representing the - * current sequence. - * @param numberedWanted If numbering is wanted. - */ - public void setIsNumbered(boolean numberedWanted) { - this.numbered = numberedWanted; - } - - @Override - public void initialize(AppStateManager stateManager, Application app) { - if (!super.isInitialized()) { - List vps = app.getRenderManager().getPostViews(); - last = vps.get(vps.size() - 1); - last.addProcessor(this); - - if (shotName == null) { - shotName = app.getClass().getSimpleName(); - } - } - - super.initialize(stateManager, app); - } - - /** - * Clean up this AppState during the first update after it gets detached. - *

- * Because each ScreenshotAppState is also a SceneProcessor (in addition to - * being an AppState) this method is also invoked when the SceneProcessor - * get removed from its ViewPort, leading to an indirect recursion: - *

  1. AppStateManager invokes ScreenshotAppState.cleanup()
  2. - *
  3. cleanup() invokes ViewPort.removeProcessor()
  4. - *
  5. removeProcessor() invokes ScreenshotAppState.cleanup()
  6. - *
  7. ... and so on.
  8. - *
- *

- * In order to break this recursion, this method only removes the - * SceneProcessor if it has not previously been removed. - *

- * A better design would have the AppState and SceneProcessor be 2 distinct - * objects, but doing so now might break applications that rely on them - * being a single object. - */ - @Override - public void cleanup() { - ViewPort viewPort = last; - if (viewPort != null) { - last = null; - viewPort.removeProcessor(this); // XXX indirect recursion! - } - - super.cleanup(); - } - - @Override - public void onAction(String name, boolean value, float tpf) { - if (value) { - capture = true; - } - } - - public void takeScreenshot() { - capture = true; - } - - @Override - public void initialize(RenderManager rm, ViewPort vp) { - renderer = rm.getRenderer(); - this.rm = rm; - reshape(vp, vp.getCamera().getWidth(), vp.getCamera().getHeight()); - } - - @Override - public boolean isInitialized() { - return super.isInitialized() && renderer != null; - } - - @Override - public void reshape(ViewPort vp, int w, int h) { - outBuf = BufferUtils.createByteBuffer(w * h * 4); - width = w; - height = h; - } - - @Override - public void preFrame(float tpf) { - // do nothing - } - - @Override - public void postQueue(RenderQueue rq) { - // do nothing - } - - @Override - public void postFrame(FrameBuffer out) { - if (capture) { - capture = false; - - Camera curCamera = rm.getCurrentCamera(); - int viewX = (int) (curCamera.getViewPortLeft() * curCamera.getWidth()); - int viewY = (int) (curCamera.getViewPortBottom() * curCamera.getHeight()); - int viewWidth = (int) ((curCamera.getViewPortRight() - curCamera.getViewPortLeft()) * curCamera.getWidth()); - int viewHeight = (int) ((curCamera.getViewPortTop() - curCamera.getViewPortBottom()) * curCamera.getHeight()); - - renderer.setViewPort(0, 0, width, height); - renderer.readFrameBuffer(out, outBuf); - renderer.setViewPort(viewX, viewY, viewWidth, viewHeight); - - File file; - String filename; - if (numbered) { - shotIndex++; - filename = shotName + shotIndex; - } else { - filename = shotName; - } - - if (filePath == null) { - file = new File(JmeSystem.getStorageFolder() + File.separator + filename + ".png").getAbsoluteFile(); - } else { - file = new File(filePath + filename + ".png").getAbsoluteFile(); - } - - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Saving ScreenShot to: {0}", file.getAbsolutePath()); - } - - try { - writeImageFile(file); - } catch (IOException ex) { - logger.log(Level.SEVERE, "Error while saving screenshot", ex); - } - } - } - - @Override - public void setProfiler(AppProfiler profiler) { - // not implemented - } - - /** - * Called by postFrame() once the screen has been captured to outBuf. - * - * @param file the output file - * @throws IOException if an I/O error occurs - */ - protected void writeImageFile(File file) throws IOException { - OutputStream outStream = new FileOutputStream(file); - try { - JmeSystem.writeImageFile(outStream, "png", outBuf, width, height); - } finally { - outStream.close(); - } - } -} diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java index f207e73d27..c21af4523c 100644 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java @@ -105,6 +105,7 @@ public void run(){ settings.setResolution(resolution.getWidth(), resolution.getHeight()); settings.setAudioRenderer(null); // Disable audio (for headless) settings.setUseInput(false); //while it will run with inputs on it causes non-fatal errors. + settings.setRenderer(AppSettings.LWJGL_OPENGL45); String imageFilePrefix = baseImageFileName == null ? calculateImageFilePrefix() : baseImageFileName; diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java index 2d7220de14..9949a852e1 100644 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java @@ -39,12 +39,18 @@ import com.jme3.math.FastMath; import com.jme3.system.AppSettings; import com.jme3.system.JmeContext; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.file.Files; @@ -55,17 +61,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.fail; @@ -102,14 +104,18 @@ public class TestDriver extends BaseAppState{ Collection framesToTakeScreenshotsOn; - ScreenshotNoInputAppState screenshotAppState; - private CountDownLatch waitLatch; private final int tickToTerminateApp; - public TestDriver(ScreenshotNoInputAppState screenshotAppState, Collection framesToTakeScreenshotsOn){ - this.screenshotAppState = screenshotAppState; + OffScreenshotAppState offScreenshotAppState; + + ScenarioScreenshotRecorder screenshotsAtFrames = new ScenarioScreenshotRecorder(); + + private final String scenarioName; + + public TestDriver(String scenarioName, Collection framesToTakeScreenshotsOn){ + this.scenarioName = scenarioName; this.framesToTakeScreenshotsOn = framesToTakeScreenshotsOn; this.tickToTerminateApp = framesToTakeScreenshotsOn.stream().mapToInt(i -> i).max().orElse(0) + 1; } @@ -119,7 +125,15 @@ public void update(float tpf){ super.update(tpf); if(framesToTakeScreenshotsOn.contains(tick)){ - screenshotAppState.takeScreenshot(); + Path screenshotPath; + try { + screenshotPath = Files.createTempFile("screenshot_" + scenarioName + "_" + tick + "_", ".tmp"); + } catch (IOException e) { + throw new RuntimeException(e); + } + screenshotPath.toFile().deleteOnExit(); + screenshotsAtFrames.recordScreenshot(scenarioName, tick, screenshotPath); + offScreenshotAppState.takeScreenshot(screenshotPath); } if(tick >= tickToTerminateApp){ getApplication().stop(true); @@ -135,6 +149,25 @@ public void update(float tpf){ waitLatch.countDown(); }; + AppSettings settings = app.getContext().getSettings(); + int width = settings.getWidth(); + int height = settings.getHeight(); + Texture2D renderTexture = new Texture2D(width, height, Image.Format.RGBA8); + renderTexture.setMinFilter(Texture.MinFilter.BilinearNearestMipMap); + renderTexture.setMagFilter(Texture.MagFilter.Bilinear); + renderTexture.getImage().setColorSpace(ColorSpace.sRGB); + + FrameBuffer offBuffer = new FrameBuffer(width, height, 1); + offBuffer.setDepthTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.Depth)); + offBuffer.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(renderTexture)); + offBuffer.setSrgb(true); + + offScreenshotAppState = new OffScreenshotAppState(renderTexture, offBuffer); + + app.getRenderer().setMainFrameBufferOverride(offBuffer); + + + getStateManager().attach(offScreenshotAppState); } @Override protected void cleanup(Application app){} @@ -152,28 +185,14 @@ public void update(float tpf){ public static void bootAppForTest(TestType testType, AppSettings appSettings, String baseImageFileName, List framesToTakeScreenshotsOn, List scenarios){ Collections.sort(framesToTakeScreenshotsOn); - - List tempFolders = new ArrayList<>(); - Map> imageFilesPerScenario = new HashMap<>(); - + ScenarioScreenshotRecorder overallScreenshots = new ScenarioScreenshotRecorder(); // usually there is a single scenario, but the framework can be set up to expect multiple scenarios that give identical results for(Scenario scenario : scenarios) { FastMath.rand.setSeed(0); //try to make things deterministic by setting the random seed - Path imageTempDir; - try { - imageTempDir = Files.createTempDirectory("jmeSnapshotTest"); - } catch (IOException e) { - throw new RuntimeException(e); - } - tempFolders.add(imageTempDir); - ScreenshotNoInputAppState screenshotAppState = new ScreenshotNoInputAppState(imageTempDir.toString() + "/"); - String screenshotAppFileNamePrefix = "Screenshot-"; - screenshotAppState.setFileName(screenshotAppFileNamePrefix); List states = new ArrayList<>(Arrays.asList(scenario.states)); - TestDriver testDriver = new TestDriver(screenshotAppState, framesToTakeScreenshotsOn); - states.add(screenshotAppState); + TestDriver testDriver = new TestDriver(scenario.scenarioName, framesToTakeScreenshotsOn); states.add(testDriver); SimpleApplication app = new App(states.toArray(new AppState[0])); @@ -198,64 +217,34 @@ public static void bootAppForTest(TestType testType, AppSettings appSettings, St Thread.currentThread().interrupt(); throw new RuntimeException(e); } - - //search the imageTempDir - List imageFiles = new ArrayList<>(); - try (Stream paths = Files.list(imageTempDir)) { - paths.forEach(imageFiles::add); - } catch (IOException e) { - throw new RuntimeException(e); - } - - //this resorts with natural numeric ordering (so App10.png comes after App9.png) - imageFiles.sort(new Comparator() { - @Override - public int compare(Path p1, Path p2) { - return extractNumber(p1).compareTo(extractNumber(p2)); - } - - private Integer extractNumber(Path path) { - String name = path.getFileName().toString(); - int numStart = screenshotAppFileNamePrefix.length(); - int numEnd = name.lastIndexOf(".png"); - return Integer.parseInt(name.substring(numStart, numEnd)); - } - }); - if (imageFiles.isEmpty()) { - fail("No screenshot found in the temporary directory. Did the application crash?"); - } - if (imageFiles.size() != framesToTakeScreenshotsOn.size()) { - fail("Not all screenshots were taken, expected " + framesToTakeScreenshotsOn.size() + " but got " + imageFiles.size()); - } - - imageFilesPerScenario.put(scenario, imageFiles); + overallScreenshots.addAll(testDriver.screenshotsAtFrames); } String failureMessage = null; try { - List primeScenarioScreenshots = imageFilesPerScenario.get(scenarios.get(0)); - - if(imageFilesPerScenario.size()>1){ - String primeScenarioName = scenarios.get(0).scenarioName; + String primeScenarioName = scenarios.get(0).scenarioName; + if(scenarios.size()>1){ // check each scenario gave the same results (before checking a single scenario against the reference images - for(int i=1;i otherScenarioScreenshots = imageFilesPerScenario.get(scenarios.get(i)); - for(int screenshotIndex=0;screenshotIndex fail( + "Scenario " + primeScenarioName + " did not take screenshot on frame " + frame + )); + Path otherImage = overallScreenshots.getScreenshotsAtFrame(thisScenarioName, frame).orElseGet(() -> fail( + "Scenario " + thisScenarioName + " did not take screenshot on frame " + frame + )); BufferedImage img1 = ImageIO.read(primeImage.toFile()); BufferedImage img2 = ImageIO.read(otherImage.toFile()); - int frame = framesToTakeScreenshotsOn.get(screenshotIndex); - String thisFrameBaseImageFileName = baseImageFileName + "_f" + frame; - if (!imagesAreTheSame(img1, img2)) { - attachImage("Scenario " + primeScenarioName + " " + screenshotIndex, thisFrameBaseImageFileName + "_" + primeScenarioName + ".png", img1); - attachImage("Scenario " + thisScenarioName + " " + screenshotIndex, thisFrameBaseImageFileName + "_" + thisScenarioName + ".png", img2); + if (!imagesAreVerySimilar(img1, img2)) { + attachImage("Scenario " + primeScenarioName + " " + frame, thisFrameBaseImageFileName + "_" + primeScenarioName + ".png", img1); + attachImage("Scenario " + thisScenarioName + " " + frame, thisFrameBaseImageFileName + "_" + thisScenarioName + ".png", img2); attachImage("Diff (between above scenarios)", thisFrameBaseImageFileName + "_" + primeScenarioName + "_" + thisScenarioName + "_diff.png", createComparisonImage(img1, img2)); if(failureMessage==null){ //only want the first thing to go wrong as the junit test fail reason @@ -268,9 +257,10 @@ private Integer extractNumber(Path path) { } - for(int screenshotIndex=0;screenshotIndex fail( + "Scenario " + primeScenarioName + " did not take screenshot on frame " + frame + )); String thisFrameBaseImageFileName = baseImageFileName + "_f" + frame; @@ -294,7 +284,7 @@ private Integer extractNumber(Path path) { BufferedImage img1 = ImageIO.read(generatedImage.toFile()); BufferedImage img2 = ImageIO.read(expectedImage.toFile()); - if (imagesAreTheSame(img1, img2)) { + if (imagesAreVerySimilar(img1, img2)) { if(testType == TestType.KNOWN_TO_FAIL){ ExtentReportExtension.getCurrentTest().warning(KNOWN_BAD_TEST_IMAGES_SAME); } @@ -325,10 +315,6 @@ private Integer extractNumber(Path path) { } } catch (IOException e) { throw new RuntimeException("Error reading images", e); - } finally{ - for(Path imageTempDir : tempFolders){ - clearTemporaryFolder(imageTempDir); - } } if(failureMessage!=null){ @@ -336,20 +322,6 @@ private Integer extractNumber(Path path) { } } - private static void clearTemporaryFolder(Path temporaryFolder){ - try (Stream paths = Files.walk(temporaryFolder)) { - paths.sorted((a, b) -> b.getNameCount() - a.getNameCount()) - .forEach(path -> { - try { - Files.delete(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (IOException e) { - throw new RuntimeException(e); - } - } /** * Saves the image with the exact file name it needs to go into the resources directory to be a new reference image @@ -409,10 +381,11 @@ private static void attachImage(String title, String fileName, BufferedImage ori } /** - * Tests that the images are the same. If they are not the same it will return false (which may fail the test - * depending on the test type). Different sizes are so fatal that they will immediately fail the test. + * Tests that the images are the same for the purposes of the test. + * If they are not the same it will return false (which may fail the test depending on the test type). + * Different sizes are so fatal that they will immediately fail the test. */ - private static boolean imagesAreTheSame(BufferedImage img1, BufferedImage img2) { + private static boolean imagesAreVerySimilar(BufferedImage img1, BufferedImage img2) { if (img1.getWidth() != img2.getWidth() || img1.getHeight() != img2.getHeight()) { ExtentReportExtension.getCurrentTest().createNode("Image 1 size : " + img1.getWidth() + "x" + img1.getHeight()); ExtentReportExtension.getCurrentTest().createNode("Image 2 size : " + img2.getWidth() + "x" + img2.getHeight()); @@ -421,8 +394,26 @@ private static boolean imagesAreTheSame(BufferedImage img1, BufferedImage img2) for (int y = 0; y < img1.getHeight(); y++) { for (int x = 0; x < img1.getWidth(); x++) { - if (img1.getRGB(x, y) != img2.getRGB(x, y)){ - return false; + int rgb1 = img1.getRGB(x, y); + int rgb2 = img2.getRGB(x, y); + + if (rgb1 != rgb2){ + int r1 = (rgb1 >> 16) & 0xFF; + int g1 = (rgb1 >> 8) & 0xFF; + int b1 = rgb1 & 0xFF; + + int r2 = (rgb2 >> 16) & 0xFF; + int g2 = (rgb2 >> 8) & 0xFF; + int b2 = rgb2 & 0xFF; + + int dr = Math.abs(r1 - r2); + int dg = Math.abs(g1 - g2); + int db = Math.abs(b1 - b2); + + double largestPixelValueDifference = Math.max(dr, Math.max(dg, db)); + if(largestPixelValueDifference>PixelSamenessDegree.NEGLIGIBLY_DIFFERENT.getMaximumAllowedDifference()){ + return false; + } } } } diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_worldSpace_f45.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_worldSpace_f45.png index a5793cebc9..87ac0abf34 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_worldSpace_f45.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.effects.TestIssue1773.testIssue1773_worldSpace_f45.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png index cdbb2427a8..ada1b97374 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png index cdbb2427a8..ada1b97374 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png index 4bc480e4e3..f29bfddafb 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png index bc7bc3ca84..a7e8d90cc3 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromAbove_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromAbove_f1.png index 4a3e1f2af0..7bc5924ba8 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromAbove_f1.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromAbove_f1.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromFront_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromFront_f1.png index a798a16d34..d715ef8a8a 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromFront_f1.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromFront_f1.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromRight_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromRight_f1.png index 8be63bb6fa..e0dfe189c6 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromRight_f1.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.model.shape.TestBillboard.testBillboard_fromRight_f1.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png index aa8c62dcd4..f6942cb86d 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png index a7c42c0bcd..a79b8e7c04 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png index aa8c62dcd4..f6942cb86d 100644 Binary files a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png differ