Skip to content

Commit 889fec7

Browse files
committed
Merge remote-tracking branch 'richardTingle/multi-scenario-screenshot-test' into fullscreen_triangle
2 parents b89fa7c + e1f0a78 commit 889fec7

4 files changed

Lines changed: 144 additions & 63 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.jmonkeyengine.screenshottests.testframework;
2+
3+
import com.jme3.app.state.AppState;
4+
5+
public class Scenario {
6+
String scenarioName;
7+
AppState[] states;
8+
9+
public Scenario(String scenarioName, AppState... states) {
10+
this.scenarioName = scenarioName;
11+
this.states = states;
12+
}
13+
}

jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ public class ScreenshotTest{
4747

4848
TestType testType = TestType.MUST_PASS;
4949

50-
AppState[] states;
50+
/**
51+
* Usually there will be a single scenario but sometimes it will be desirable to test that two ways
52+
* of doing something produce the same result. In that case there will be multiple scenarios.
53+
*/
54+
List<Scenario> scenarios = new ArrayList<>();
5155

5256
List<Integer> framesToTakeScreenshotsOn = new ArrayList<>();
5357

@@ -56,7 +60,11 @@ public class ScreenshotTest{
5660
String baseImageFileName = null;
5761

5862
public ScreenshotTest(AppState... initialStates){
59-
states = initialStates;
63+
scenarios.add(new Scenario("SimpleSingleScenario", initialStates));
64+
framesToTakeScreenshotsOn.add(1); //default behaviour is to take a screenshot on the first frame
65+
}
66+
public ScreenshotTest(Scenario... scenarios){
67+
this.scenarios.addAll(Arrays.asList(scenarios));
6068
framesToTakeScreenshotsOn.add(1); //default behaviour is to take a screenshot on the first frame
6169
}
6270

@@ -100,7 +108,7 @@ public void run(){
100108

101109
String imageFilePrefix = baseImageFileName == null ? calculateImageFilePrefix() : baseImageFileName;
102110

103-
TestDriver.bootAppForTest(testType,settings,imageFilePrefix, framesToTakeScreenshotsOn, states);
111+
TestDriver.bootAppForTest(testType,settings,imageFilePrefix, framesToTakeScreenshotsOn, scenarios);
104112
}
105113

106114

jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTestBase.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,22 @@ public abstract class ScreenshotTestBase{
4747
/**
4848
* Initialises a screenshot test. The resulting object should be configured (if neccessary) and then started
4949
* by calling {@link ScreenshotTest#run()}.
50-
* @param initialStates
50+
* @param initialStates the states that will create the JME environment
5151
* @return
5252
*/
5353
public ScreenshotTest screenshotTest(AppState... initialStates){
5454
return new ScreenshotTest(initialStates);
5555
}
56+
57+
/**
58+
* Permits multiple scenarios to be tested in a single test. Each scenario should give identical results and
59+
* will have a screenshot taken on the same frame.
60+
*
61+
* <p>
62+
* This is intended for testing migrations where the old and new approach should both give identical results.
63+
* </p>
64+
*/
65+
public ScreenshotTest screenshotMultiScenarioTest(Scenario... scenarios){
66+
return new ScreenshotTest(scenarios);
67+
}
5668
}

jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java

Lines changed: 107 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
import java.util.Collection;
5757
import java.util.Collections;
5858
import java.util.Comparator;
59+
import java.util.HashMap;
5960
import java.util.List;
61+
import java.util.Map;
6062
import java.util.concurrent.CountDownLatch;
6163
import java.util.concurrent.Executor;
6264
import java.util.concurrent.Executors;
@@ -78,7 +80,9 @@ public class TestDriver extends BaseAppState{
7880

7981
private static final Logger logger = Logger.getLogger(TestDriver.class.getName());
8082

81-
public static final String IMAGES_ARE_DIFFERENT = "Images are different. (If you are running the test locally this is expected, images only reproducible on github CI infrastructure)";
83+
public static final String IMAGES_ARE_DIFFERENT = "Generated images is different from committed image. (If you are running the test locally this is expected, images only reproducible on github CI infrastructure)";
84+
85+
public static final String IMAGES_ARE_DIFFERENT_BETWEEN_SCENARIOS = "Images are different between scenarios.";
8286

8387
public static final String IMAGES_ARE_DIFFERENT_SIZES = "Images are different sizes.";
8488

@@ -145,85 +149,127 @@ public void update(float tpf){
145149
* - After all the frames have been taken it stops the application
146150
* - Compares the screenshot to the expected screenshot (if any). Fails the test if they are different
147151
*/
148-
public static void bootAppForTest(TestType testType, AppSettings appSettings, String baseImageFileName, List<Integer> framesToTakeScreenshotsOn, AppState... initialStates){
149-
FastMath.rand.setSeed(0); //try to make things deterministic by setting the random seed
152+
public static void bootAppForTest(TestType testType, AppSettings appSettings, String baseImageFileName, List<Integer> framesToTakeScreenshotsOn, List<Scenario> scenarios){
153+
150154
Collections.sort(framesToTakeScreenshotsOn);
151155

152-
Path imageTempDir;
156+
List<Path> tempFolders = new ArrayList<>();
157+
Map<Scenario, List<Path>> imageFilesPerScenario = new HashMap<>();
158+
159+
// usually there is a single scenario, but the framework can be set up to expect multiple scenarios that give identical results
160+
for(Scenario scenario : scenarios) {
161+
FastMath.rand.setSeed(0); //try to make things deterministic by setting the random seed
162+
Path imageTempDir;
163+
try {
164+
imageTempDir = Files.createTempDirectory("jmeSnapshotTest");
165+
} catch (IOException e) {
166+
throw new RuntimeException(e);
167+
}
168+
tempFolders.add(imageTempDir);
153169

154-
try{
155-
imageTempDir = Files.createTempDirectory("jmeSnapshotTest");
156-
} catch(IOException e){
157-
throw new RuntimeException(e);
158-
}
170+
ScreenshotNoInputAppState screenshotAppState = new ScreenshotNoInputAppState(imageTempDir.toString() + "/");
171+
String screenshotAppFileNamePrefix = "Screenshot-";
172+
screenshotAppState.setFileName(screenshotAppFileNamePrefix);
159173

160-
ScreenshotNoInputAppState screenshotAppState = new ScreenshotNoInputAppState(imageTempDir.toString() + "/");
161-
String screenshotAppFileNamePrefix = "Screenshot-";
162-
screenshotAppState.setFileName(screenshotAppFileNamePrefix);
174+
List<AppState> states = new ArrayList<>(Arrays.asList(scenario.states));
175+
TestDriver testDriver = new TestDriver(screenshotAppState, framesToTakeScreenshotsOn);
176+
states.add(screenshotAppState);
177+
states.add(testDriver);
163178

164-
List<AppState> states = new ArrayList<>(Arrays.asList(initialStates));
165-
TestDriver testDriver = new TestDriver(screenshotAppState, framesToTakeScreenshotsOn);
166-
states.add(screenshotAppState);
167-
states.add(testDriver);
179+
SimpleApplication app = new App(states.toArray(new AppState[0]));
180+
app.setSettings(appSettings);
181+
app.setShowSettings(false);
168182

169-
SimpleApplication app = new App(states.toArray(new AppState[0]));
170-
app.setSettings(appSettings);
171-
app.setShowSettings(false);
183+
testDriver.waitLatch = new CountDownLatch(1);
184+
executor.execute(() -> app.start(JmeContext.Type.Display));
172185

173-
testDriver.waitLatch = new CountDownLatch(1);
174-
executor.execute(() -> app.start(JmeContext.Type.Display));
186+
int maxWaitTimeMilliseconds = 45000;
175187

176-
int maxWaitTimeMilliseconds = 45000;
188+
try {
189+
boolean exitedProperly = testDriver.waitLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS);
177190

178-
try {
179-
boolean exitedProperly = testDriver.waitLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS);
191+
if (!exitedProperly) {
192+
logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out");
193+
app.stop(true);
194+
}
180195

181-
if(!exitedProperly){
182-
logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out");
183-
app.stop(true);
196+
Thread.sleep(1000); //give time for openGL is fully released before starting a new test (get random JVM crashes without this)
197+
} catch (InterruptedException e) {
198+
Thread.currentThread().interrupt();
199+
throw new RuntimeException(e);
184200
}
185201

186-
Thread.sleep(1000); //give time for openGL is fully released before starting a new test (get random JVM crashes without this)
187-
} catch (InterruptedException e) {
188-
Thread.currentThread().interrupt();
189-
throw new RuntimeException(e);
190-
}
202+
//search the imageTempDir
203+
List<Path> imageFiles = new ArrayList<>();
204+
try (Stream<Path> paths = Files.list(imageTempDir)) {
205+
paths.forEach(imageFiles::add);
206+
} catch (IOException e) {
207+
throw new RuntimeException(e);
208+
}
191209

192-
//search the imageTempDir
193-
List<Path> imageFiles = new ArrayList<>();
194-
try(Stream<Path> paths = Files.list(imageTempDir)){
195-
paths.forEach(imageFiles::add);
196-
} catch(IOException e){
197-
throw new RuntimeException(e);
198-
}
210+
//this resorts with natural numeric ordering (so App10.png comes after App9.png)
211+
imageFiles.sort(new Comparator<Path>() {
212+
@Override
213+
public int compare(Path p1, Path p2) {
214+
return extractNumber(p1).compareTo(extractNumber(p2));
215+
}
199216

200-
//this resorts with natural numeric ordering (so App10.png comes after App9.png)
201-
imageFiles.sort(new Comparator<Path>(){
202-
@Override
203-
public int compare(Path p1, Path p2){
204-
return extractNumber(p1).compareTo(extractNumber(p2));
217+
private Integer extractNumber(Path path) {
218+
String name = path.getFileName().toString();
219+
int numStart = screenshotAppFileNamePrefix.length();
220+
int numEnd = name.lastIndexOf(".png");
221+
return Integer.parseInt(name.substring(numStart, numEnd));
222+
}
223+
});
224+
if (imageFiles.isEmpty()) {
225+
fail("No screenshot found in the temporary directory. Did the application crash?");
205226
}
206-
207-
private Integer extractNumber(Path path){
208-
String name = path.getFileName().toString();
209-
int numStart = screenshotAppFileNamePrefix.length();
210-
int numEnd = name.lastIndexOf(".png");
211-
return Integer.parseInt(name.substring(numStart, numEnd));
227+
if (imageFiles.size() != framesToTakeScreenshotsOn.size()) {
228+
fail("Not all screenshots were taken, expected " + framesToTakeScreenshotsOn.size() + " but got " + imageFiles.size());
212229
}
213-
});
214230

215-
if(imageFiles.isEmpty()){
216-
fail("No screenshot found in the temporary directory. Did the application crash?");
231+
imageFilesPerScenario.put(scenario, imageFiles);
217232
}
218-
if(imageFiles.size() != framesToTakeScreenshotsOn.size()){
219-
fail("Not all screenshots were taken, expected " + framesToTakeScreenshotsOn.size() + " but got " + imageFiles.size());
220-
}
221-
222233
String failureMessage = null;
223234

224235
try {
236+
List<Path> primeScenarioScreenshots = imageFilesPerScenario.get(scenarios.get(0));
237+
238+
if(imageFilesPerScenario.size()>1){
239+
String primeScenarioName = scenarios.get(0).scenarioName;
240+
241+
// check each scenario gave the same results (before checking a single scenario against the reference images
242+
for(int i=1;i<imageFilesPerScenario.size();i++){
243+
String thisScenarioName = scenarios.get(i).scenarioName;
244+
List<Path> otherScenarioScreenshots = imageFilesPerScenario.get(scenarios.get(i));
245+
for(int screenshotIndex=0;screenshotIndex<framesToTakeScreenshotsOn.size();screenshotIndex++) {
246+
Path primeImage = primeScenarioScreenshots.get(screenshotIndex);
247+
Path otherImage = otherScenarioScreenshots.get(screenshotIndex);
248+
249+
BufferedImage img1 = ImageIO.read(primeImage.toFile());
250+
BufferedImage img2 = ImageIO.read(otherImage.toFile());
251+
252+
int frame = framesToTakeScreenshotsOn.get(screenshotIndex);
253+
254+
String thisFrameBaseImageFileName = baseImageFileName + "_f" + frame;
255+
256+
if (!imagesAreTheSame(img1, img2)) {
257+
attachImage("Scenario " + primeScenarioName + " " + screenshotIndex, thisFrameBaseImageFileName + "_" + primeScenarioName + ".png", img1);
258+
attachImage("Scenario " + thisScenarioName + " " + screenshotIndex, thisFrameBaseImageFileName + "_" + thisScenarioName + ".png", img2);
259+
attachImage("Diff (between above scenarios)", thisFrameBaseImageFileName + "_" + primeScenarioName + "_" + thisScenarioName + "_diff.png", createComparisonImage(img1, img2));
260+
261+
if(failureMessage==null){ //only want the first thing to go wrong as the junit test fail reason
262+
failureMessage = IMAGES_ARE_DIFFERENT_BETWEEN_SCENARIOS;
263+
}
264+
ExtentReportExtension.getCurrentTest().fail(IMAGES_ARE_DIFFERENT_BETWEEN_SCENARIOS);
265+
}
266+
}
267+
}
268+
}
269+
270+
225271
for(int screenshotIndex=0;screenshotIndex<framesToTakeScreenshotsOn.size();screenshotIndex++){
226-
Path generatedImage = imageFiles.get(screenshotIndex);
272+
Path generatedImage = primeScenarioScreenshots.get(screenshotIndex);
227273
int frame = framesToTakeScreenshotsOn.get(screenshotIndex);
228274

229275
String thisFrameBaseImageFileName = baseImageFileName + "_f" + frame;
@@ -280,7 +326,9 @@ private Integer extractNumber(Path path){
280326
} catch (IOException e) {
281327
throw new RuntimeException("Error reading images", e);
282328
} finally{
283-
clearTemporaryFolder(imageTempDir);
329+
for(Path imageTempDir : tempFolders){
330+
clearTemporaryFolder(imageTempDir);
331+
}
284332
}
285333

286334
if(failureMessage!=null){

0 commit comments

Comments
 (0)