public static void bootAppForTest(TestType testType, AppSettings appSettings, String baseImageFileName, List<Integer> framesToTakeScreenshotsOn, AppState... initialStates){
        FastMath.rand.setSeed(0); //try to make things deterministic by setting the random seed
        Collections.sort(framesToTakeScreenshotsOn);

        Path imageTempDir;

        try{
            imageTempDir = Files.createTempDirectory("jmeSnapshotTest");
        } catch(IOException e){
            throw new RuntimeException(e);
        }

        ScreenshotNoInputAppState screenshotAppState = new ScreenshotNoInputAppState(imageTempDir.toString() + "/");
        String screenshotAppFileNamePrefix = "Screenshot-";
        screenshotAppState.setFileName(screenshotAppFileNamePrefix);

        List<AppState> states = new ArrayList<>(Arrays.asList(initialStates));
        TestDriver testDriver = new TestDriver(screenshotAppState, framesToTakeScreenshotsOn);
        states.add(screenshotAppState);
        states.add(testDriver);

        SimpleApplication app = new App(states.toArray(new AppState[0]));
        app.setSettings(appSettings);
        app.setShowSettings(false);

        executor.execute(() -> app.start(JmeContext.Type.Display));

        synchronized (testDriver.waitLock) {
            try {
                testDriver.waitLock.wait(10000); // Wait for the screenshot to be taken and application to stop
                Thread.sleep(200); //give time for openGL is fully released before starting a new test (get random JVM crashes without this)
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }

        //search the imageTempDir
        List<Path> imageFiles = new ArrayList<>();
        try(Stream<Path> 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<Path>(){
            @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());
        }

        String failureMessage = null;

        try {
            for(int screenshotIndex=0;screenshotIndex<framesToTakeScreenshotsOn.size();screenshotIndex++){
                Path generatedImage = imageFiles.get(screenshotIndex);
                int frame = framesToTakeScreenshotsOn.get(screenshotIndex);

                String thisFrameBaseImageFileName = baseImageFileName + "_f" + frame;

                Path expectedImage = Paths.get("src/test/resources/" + thisFrameBaseImageFileName + ".png");

                if(!Files.exists(expectedImage)){
                    try{
                        Path savedImage = saveGeneratedImageToChangedImages(generatedImage, thisFrameBaseImageFileName);
                        attachImage("New image:", thisFrameBaseImageFileName + ".png", savedImage);
                        String message = "Expected image not found, is this a new test? If so collect the new image from the step artefacts (on github). If running locally you can see them at build/changed-images but those should not be committed";
                        if(failureMessage==null){ //only want the first thing to go wrong as the junit test fail reason
                            failureMessage = message;
                        }
                        ExtentReportExtension.getCurrentTest().fail(message);
                        continue;
                    } catch(IOException e){
                        throw new RuntimeException(e);
                    }
                }

                BufferedImage img1 = ImageIO.read(generatedImage.toFile());
                BufferedImage img2 = ImageIO.read(expectedImage.toFile());

                if (imagesAreTheSame(img1, img2)) {
                    if(testType == TestType.KNOWN_TO_FAIL){
                        ExtentReportExtension.getCurrentTest().warning(KNOWN_BAD_TEST_IMAGES_SAME);
                    }
                } else {
                    //save the generated image to the build directory
                    Path savedImage = saveGeneratedImageToChangedImages(generatedImage, thisFrameBaseImageFileName);

                    attachImage("Expected", thisFrameBaseImageFileName + "_expected.png", expectedImage);
                    attachImage("Actual", thisFrameBaseImageFileName + "_actual.png", savedImage);
                    attachImage("Diff", thisFrameBaseImageFileName + "_diff.png", createComparisonImage(img1, img2));

                    switch(testType){
                        case MUST_PASS:
                            if(failureMessage==null){ //only want the first thing to go wrong as the junit test fail reason
                                failureMessage = IMAGES_ARE_DIFFERENT;
                            }
                            ExtentReportExtension.getCurrentTest().fail(IMAGES_ARE_DIFFERENT);
                            break;
                        case NON_DETERMINISTIC:
                            ExtentReportExtension.getCurrentTest().warning(NON_DETERMINISTIC_TEST);
                            break;
                        case KNOWN_TO_FAIL:
                            ExtentReportExtension.getCurrentTest().warning(KNOWN_BAD_TEST_IMAGES_DIFFERENT);
                            break;
                    }
                }

            }
        } catch (IOException e) {
            throw new RuntimeException("Error reading images", e);
        } finally{
            clearTemporaryFolder(imageTempDir);
        }

        if(failureMessage!=null){
            fail(failureMessage);
        }
    }
Boots up the application on a separate thread (blocks this thread) and then does the following:
- Takes screenshots on the requested frames
- After all the frames have been taken it stops the application
- Compares the screenshot to the expected screenshot (if any). Fails the test if they are different
jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java