Skip to content

Commit 5d4990e

Browse files
authored
Merge pull request jMonkeyEngine#2542 from richardTingle/jMonkeyEngine#2541-move-screenshots-to-docker
jMonkeyEngine#2541 move screenshots to docker
2 parents 1fc6230 + 1505c79 commit 5d4990e

18 files changed

+198
-55
lines changed

.github/workflows/main.yml

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -59,46 +59,41 @@ jobs:
5959
ScreenshotTests:
6060
name: Run Screenshot Tests
6161
runs-on: ubuntu-latest
62+
container:
63+
image: ghcr.io/onemillionworlds/opengl-docker-image:v1
6264
permissions:
6365
contents: read
6466
steps:
65-
- uses: actions/checkout@v4
66-
- name: Set up JDK 17
67-
uses: actions/setup-java@v4
68-
with:
69-
java-version: '17'
70-
distribution: 'temurin'
71-
- name: Install Mesa3D
72-
run: |
73-
sudo apt-get update
74-
sudo apt-get install -y mesa-utils libgl1-mesa-dri libgl1 libglx-mesa0 xvfb
75-
- name: Set environment variables for Mesa3D
76-
run: |
77-
echo "LIBGL_ALWAYS_SOFTWARE=1" >> $GITHUB_ENV
78-
echo "MESA_LOADER_DRIVER_OVERRIDE=llvmpipe" >> $GITHUB_ENV
79-
- name: Start xvfb
80-
run: |
81-
sudo Xvfb :99 -ac -screen 0 1024x768x16 &
82-
export DISPLAY=:99
83-
echo "DISPLAY=:99" >> $GITHUB_ENV
84-
- name: Verify Mesa3D Installation
85-
run: |
86-
glxinfo | grep "OpenGL"
87-
- name: Validate the Gradle wrapper
88-
uses: gradle/actions/wrapper-validation@v3
89-
- name: Test with Gradle Wrapper
90-
run: |
91-
./gradlew :jme3-screenshot-test:screenshotTest
92-
- name: Upload Test Reports
93-
uses: actions/upload-artifact@master
94-
if: always()
95-
with:
96-
name: screenshot-test-report
97-
retention-days: 30
98-
path: |
99-
**/build/reports/**
100-
**/build/changed-images/**
101-
**/build/test-results/**
67+
- uses: actions/checkout@v4
68+
- name: Start xvfb
69+
run: |
70+
Xvfb :99 -ac -screen 0 1024x768x16 &
71+
export DISPLAY=:99
72+
echo "DISPLAY=:99" >> $GITHUB_ENV
73+
- name: Report GL/Vulkan
74+
run: |
75+
set -x
76+
echo "DISPLAY=$DISPLAY"
77+
glxinfo | grep -E "OpenGL version|OpenGL renderer|OpenGL vendor" || true
78+
vulkaninfo --summary || true
79+
echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES"
80+
echo "MESA_LOADER_DRIVER_OVERRIDE=$MESA_LOADER_DRIVER_OVERRIDE"
81+
echo "GALLIUM_DRIVER=$GALLIUM_DRIVER"
82+
- name: Validate the Gradle wrapper
83+
uses: gradle/actions/wrapper-validation@v3
84+
- name: Test with Gradle Wrapper
85+
run: |
86+
./gradlew :jme3-screenshot-test:screenshotTest
87+
- name: Upload Test Reports
88+
uses: actions/upload-artifact@master
89+
if: always()
90+
with:
91+
name: screenshot-test-report
92+
retention-days: 30
93+
path: |
94+
**/build/reports/**
95+
**/build/changed-images/**
96+
**/build/test-results/**
10297
# Build the natives on android
10398
BuildAndroidNatives:
10499
name: Build natives for android

.github/workflows/screenshot-test-comment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
contents: read
2222
steps:
2323
- name: Wait for GitHub to register the workflow run
24-
run: sleep 15
24+
run: sleep 120
2525

2626
- name: Wait for Screenshot Tests to complete
2727
uses: lewagon/[email protected]

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import com.jme3.app.state.VideoRecorderAppState;
3737
import com.jme3.math.ColorRGBA;
3838

39+
import java.util.function.Consumer;
40+
3941
/**
4042
* The app used for the tests. AppState(s) are used to inject the actual test code.
4143
* @author Richard Tingle (aka richtea)
@@ -46,10 +48,17 @@ public App(AppState... initialStates){
4648
super(initialStates);
4749
}
4850

51+
Consumer<Throwable> onError = (onError) -> {};
52+
4953
@Override
5054
public void simpleInitApp(){
5155
getViewPort().setBackgroundColor(ColorRGBA.Black);
5256
setTimer(new VideoRecorderAppState.IsoTimer(60));
5357
}
5458

59+
@Override
60+
public void handleError(String errMsg, Throwable t) {
61+
super.handleError(errMsg, t);
62+
onError.accept(t);
63+
}
5564
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
*/
5151
public class ExtentReportExtension implements BeforeAllCallback, AfterAllCallback, TestWatcher, BeforeTestExecutionCallback{
5252
private static ExtentReports extent;
53-
private static final ThreadLocal<ExtentTest> test = new ThreadLocal<>();
53+
private static ExtentTest currentTest;
5454

5555
@Override
5656
public void beforeAll(ExtensionContext context) {
@@ -62,6 +62,8 @@ public void beforeAll(ExtensionContext context) {
6262
extent = new ExtentReports();
6363
extent.attachReporter(spark);
6464
}
65+
// Initialize log capture to redirect console output to the report
66+
ExtentReportLogCapture.initialize();
6567
}
6668

6769
@Override
@@ -71,6 +73,9 @@ public void afterAll(ExtensionContext context) {
7173
* anywhere else I can hook into the lifecycle of the end of all tests to write the report.
7274
*/
7375
extent.flush();
76+
77+
// Restore the original System.out
78+
ExtentReportLogCapture.restore();
7479
}
7580

7681
@Override
@@ -96,10 +101,11 @@ public void testDisabled(ExtensionContext context, Optional<String> reason) {
96101
@Override
97102
public void beforeTestExecution(ExtensionContext context) {
98103
String testName = context.getDisplayName();
99-
test.set(extent.createTest(testName));
104+
String className = context.getRequiredTestClass().getSimpleName();
105+
currentTest = extent.createTest(className + "." + testName);
100106
}
101107

102108
public static ExtentTest getCurrentTest() {
103-
return test.get();
109+
return currentTest;
104110
}
105-
}
111+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2025 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package org.jmonkeyengine.screenshottests.testframework;
33+
34+
import com.aventstack.extentreports.ExtentTest;
35+
36+
import java.io.OutputStream;
37+
import java.io.PrintStream;
38+
39+
/**
40+
* This class captures console logs and adds them to the ExtentReport.
41+
* It redirects System.out to both the original console and the ExtentReport.
42+
*
43+
* @author Richard Tingle (aka richtea)
44+
*/
45+
public class ExtentReportLogCapture {
46+
47+
private static final PrintStream originalOut = System.out;
48+
private static final PrintStream originalErr = System.err;
49+
private static boolean initialized = false;
50+
51+
/**
52+
* Initializes the log capture system. This should be called once at the start of the test suite.
53+
*/
54+
public static void initialize() {
55+
if (!initialized) {
56+
// Redirect System.out and System.err
57+
System.setOut(new ExtentReportPrintStream(originalOut));
58+
System.setErr(new ExtentReportPrintStream(originalErr));
59+
60+
initialized = true;
61+
}
62+
}
63+
64+
/**
65+
* Restores the original System.out. This should be called at the end of the test suite.
66+
*/
67+
public static void restore() {
68+
if(initialized) {
69+
// Restore System.out and System.err
70+
System.setOut(originalOut);
71+
System.setErr(originalErr);
72+
initialized = false;
73+
}
74+
}
75+
76+
/**
77+
* A custom PrintStream that redirects output to both the original console and the ExtentReport.
78+
*/
79+
private static class ExtentReportPrintStream extends PrintStream {
80+
private StringBuilder buffer = new StringBuilder();
81+
82+
public ExtentReportPrintStream(OutputStream out) {
83+
super(out, true);
84+
}
85+
86+
@Override
87+
public void write(byte[] buf, int off, int len) {
88+
super.write(buf, off, len);
89+
90+
// Convert the byte array to a string and add to buffer
91+
String s = new String(buf, off, len);
92+
buffer.append(s);
93+
94+
// If we have a complete line (ends with newline), process it
95+
if (s.endsWith("\n") || s.endsWith("\r\n")) {
96+
String line = buffer.toString().trim();
97+
if (!line.isEmpty()) {
98+
addToExtentReport(line);
99+
}
100+
buffer.setLength(0); // Clear the buffer
101+
}
102+
}
103+
104+
private void addToExtentReport(String s) {
105+
try {
106+
ExtentTest currentTest = ExtentReportExtension.getCurrentTest();
107+
if (currentTest != null) {
108+
currentTest.info(s);
109+
}
110+
} catch (Exception e) {
111+
// If there's an error adding to the report, just continue
112+
// This ensures that console logs are still displayed even if there's an issue with the report
113+
}
114+
}
115+
}
116+
117+
}

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@
5757
import java.util.Collections;
5858
import java.util.Comparator;
5959
import java.util.List;
60+
import java.util.concurrent.CountDownLatch;
6061
import java.util.concurrent.Executor;
6162
import java.util.concurrent.Executors;
63+
import java.util.concurrent.TimeUnit;
64+
import java.util.logging.Level;
65+
import java.util.logging.Logger;
6266
import java.util.stream.Stream;
6367

6468
import static org.junit.jupiter.api.Assertions.fail;
@@ -72,6 +76,8 @@
7276
*/
7377
public class TestDriver extends BaseAppState{
7478

79+
private static final Logger logger = Logger.getLogger(TestDriver.class.getName());
80+
7581
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)";
7682

7783
public static final String IMAGES_ARE_DIFFERENT_SIZES = "Images are different sizes.";
@@ -94,7 +100,7 @@ public class TestDriver extends BaseAppState{
94100

95101
ScreenshotNoInputAppState screenshotAppState;
96102

97-
private final Object waitLock = new Object();
103+
private CountDownLatch waitLatch;
98104

99105
private final int tickToTerminateApp;
100106

@@ -113,23 +119,26 @@ public void update(float tpf){
113119
}
114120
if(tick >= tickToTerminateApp){
115121
getApplication().stop(true);
116-
synchronized (waitLock) {
117-
waitLock.notify(); // Release the wait
118-
}
122+
waitLatch.countDown();
119123
}
120124

121125
tick++;
122126
}
123127

124-
@Override protected void initialize(Application app){}
128+
@Override protected void initialize(Application app){
129+
((App)app).onError = error -> {
130+
logger.log(Level.WARNING, "Error in test application", error);
131+
waitLatch.countDown();
132+
};
133+
134+
}
125135

126136
@Override protected void cleanup(Application app){}
127137

128138
@Override protected void onEnable(){}
129139

130140
@Override protected void onDisable(){}
131141

132-
133142
/**
134143
* Boots up the application on a separate thread (blocks this thread) and then does the following:
135144
* - Takes screenshots on the requested frames
@@ -161,16 +170,23 @@ public static void bootAppForTest(TestType testType, AppSettings appSettings, St
161170
app.setSettings(appSettings);
162171
app.setShowSettings(false);
163172

173+
testDriver.waitLatch = new CountDownLatch(1);
164174
executor.execute(() -> app.start(JmeContext.Type.Display));
165175

166-
synchronized (testDriver.waitLock) {
167-
try {
168-
testDriver.waitLock.wait(10000); // Wait for the screenshot to be taken and application to stop
169-
Thread.sleep(200); //give time for openGL is fully released before starting a new test (get random JVM crashes without this)
170-
} catch (InterruptedException e) {
171-
Thread.currentThread().interrupt();
172-
throw new RuntimeException(e);
176+
int maxWaitTimeMilliseconds = 45000;
177+
178+
try {
179+
boolean exitedProperly = testDriver.waitLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS);
180+
181+
if(!exitedProperly){
182+
logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out");
183+
app.stop(true);
173184
}
185+
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);
174190
}
175191

176192
//search the imageTempDir
27 Bytes
Loading
333 Bytes
Loading
91 Bytes
Loading
-61 Bytes
Loading

0 commit comments

Comments
 (0)