diff --git a/.github/workflows/android-screenshot.yml b/.github/workflows/android-screenshot.yml new file mode 100644 index 0000000000..13d156462d --- /dev/null +++ b/.github/workflows/android-screenshot.yml @@ -0,0 +1,80 @@ +name: Android Screenshot Test + +on: + push: + branches: + - master + - v3.7 + - v3.6 + - v3.5 + - v3.4 + - v3.3 + - ios-2024_2 + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Gradle cache + uses: gradle/actions/setup-gradle@v5 + + - name: AVD cache + uses: actions/cache@v5 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-35 + - name: Run Android Screenshot Test + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 35 + avd-name: test_avd + target: google_apis + arch: x86_64 + force-avd-creation: true + ram-size: 4096M + heap-size: 2048M + profile: pixel_tablet + emulator-options: -no-window -gpu mesa -noaudio -no-boot-anim -camera-back none -no-snapshot -no-snapshot-save -no-snapshot-load + disable-animations: false + script: > + curl -L https://maven.google.com/androidx/test/services/test-services/1.6.0/test-services-1.6.0.apk --output test-services-1.6.0.apk; + adb install -r -g test-services-1.6.0.apk; + adb uninstall org.jmonkeyengine.screenshottests.android || true; + ./gradlew :jme3-screenshot-tests:jme3-screenshot-tests-android:connectedDebugAndroidTest; + exit_code=$?; + mkdir -p logcat; + adb logcat -d > logcat/logcat_full.txt || true; + adb logcat -d | grep org.jmonkeyengine.screenshottests.android > logcat/logcat.txt || true; + mkdir -p report; + adb pull /storage/emulated/0/googletest/test_outputfiles/report report/protoReport || true; + adb pull /storage/emulated/0/googletest/test_outputfiles/changed-images report/changed-images || true; + ./gradlew :jme3-screenshot-tests:jme3-screenshot-tests-proto-report:upgradeProtoReport --args="$(pwd)/report/protoReport $(pwd)/report/extentReport" || true; + echo "GRADLE_EXIT_CODE=$exit_code" >> $GITHUB_ENV; + exit $exit_code + - name: Upload logcat + uses: actions/upload-artifact@v4 + if: always() + with: + name: android-logcat + path: logcat + - name: Upload Screenshot + uses: actions/upload-artifact@v4 + if: always() + with: + name: android-screenshot-report + path: report + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/screenshot-test-comment.yml b/.github/workflows/screenshot-test-comment.yml index 39d259633f..a445615273 100644 --- a/.github/workflows/screenshot-test-comment.yml +++ b/.github/workflows/screenshot-test-comment.yml @@ -133,6 +133,6 @@ jobs: See https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-screenshot-tests/README.md for more information - Contact @richardTingle (aka richtea) for guidance if required + Contact %40richardTingle (aka richtea) for guidance if required edit-mode: replace comment-id: ${{ steps.existingCommentId.outputs.comment-id }} diff --git a/.gitignore b/.gitignore index 663ad075e7..c9ee05f2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,8 @@ javadoc_deploy.pub !.vscode/JME_style.xml !.vscode/extensions.json joysticks-*.txt +/jme3-screenshot-tests/jme3-screenshot-tests-desktop/build/ +/jme3-screenshot-tests/jme3-screenshot-tests-android/build/ +/jme3-screenshot-tests/jme3-screenshot-tests-shared/build/ + +/jme3-screenshot-tests/jme3-screenshot-tests-proto-report/build/ diff --git a/build.gradle b/build.gradle index 7525cafdcc..52115c83a2 100644 --- a/build.gradle +++ b/build.gradle @@ -36,10 +36,10 @@ allprojects { // This is applied to all sub projects subprojects { - if(!project.name.equals('jme3-android-examples')) { - apply from: rootProject.file('common.gradle') - } else { + if(project.name.equals('jme3-android-examples') || project.name.equals('jme3-screenshot-tests-android')) { apply from: rootProject.file('common-android-app.gradle') + } else { + apply from: rootProject.file('common.gradle') } def isAndroidApp = project.plugins.hasPlugin('com.android.application') diff --git a/gradle.properties b/gradle.properties index c664edf60b..422a93c8d3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,3 +33,6 @@ POM_LICENSE_URL=http://opensource.org/licenses/BSD-3-Clause POM_LICENSE_DISTRIBUTION=repo POM_INCEPTION_YEAR=2009 +systemProp.org.gradle.unsafe.repositories=true + +org.gradle.jvmargs=-Xmx2048m diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bad03ab3db..3b43f7d32e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,8 @@ saferalloc = "0.0.8" nifty = "1.4.3" spotbugs = "4.9.8" jmeAndroidNatives = "3.10.0-xt16kb" +googleMaterial = "1.4.0" +androidx-fragment-testing = "1.8.9" [libraries] @@ -19,9 +21,21 @@ androidx-fragment = "androidx.fragment:fragment:1.8.9" androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.7.0" android-build-gradle = "com.android.tools.build:gradle:9.1.0" android-support-appcompat = "com.android.support:appcompat-v7:28.0.0" +androidx-test-services = { module = "androidx.test.services:test-services", version = "1.6.0" } +androidx-storage = { module = "androidx.test.services:storage" } # used for peristent android test result storage, file version from androidx-fragment-testing-manifest +androidxAppcompat = { module = "androidx.appcompat:appcompat", version = "1.4.0" } +androidxConstraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.1" } +androidx-test-core = "androidx.test:core:1.6.1" +androidx-test-rules = "androidx.test:rules:1.6.1" +androidx-test-ext-junit = "androidx.test.ext:junit:1.2.1" +androidx-test-espresso-core = "androidx.test.espresso:espresso-core:3.6.1" +androidx-fragment-testing = { module ="androidx.fragment:fragment-testing", version.ref = "androidx-fragment-testing"} +androidx-fragment-testing-manifest = { module = "androidx.fragment:fragment-testing-manifest", version.ref = "androidx-fragment-testing"} +gradle-git = "org.ajoberstar:gradle-git:1.2.0" androidx-test-runner = "androidx.test:runner:1.7.0" groovy-test = "org.apache.groovy:groovy-test:4.0.31" gson = "com.google.code.gson:gson:2.14.0" +googleMaterial = { module = "com.google.android.material:material", version.ref = "googleMaterial" } j-ogg-vorbis = "com.github.stephengold:j-ogg-vorbis:1.0.6" jme3-android-natives = { module = "org.jmonkeyengine:jme3-android-native", version.ref = "jmeAndroidNatives" } jbullet = "com.github.stephengold:jbullet:1.0.3" @@ -74,8 +88,13 @@ saferalloc-natives-macos-x8664 = { module = "org.ngengine:saferalloc-natives-mac saferalloc-natives-macos-aarch64 = { module = "org.ngengine:saferalloc-natives-macos-aarch64", version.ref = "saferalloc" } saferalloc-natives-android = { module = "org.ngengine:saferalloc-natives-android", version.ref = "saferalloc" } +extent-reports = { module = "com.aventstack:extentreports", version = "5.1.2"} + +jackson = {module = "tools.jackson.core:jackson-databind", version ="3.1.3"} + [bundles] saferalloc = ["saferalloc", "saferalloc-natives-linux-x8664", "saferalloc-natives-linux-aarch64", "saferalloc-natives-windows-x8664", "saferalloc-natives-windows-aarch64", "saferalloc-natives-macos-x8664", "saferalloc-natives-macos-aarch64", "saferalloc-natives-android"] [plugins] jacoco = { id = "jacoco", version.ref = "jacoco" } + diff --git a/jme3-screenshot-tests/build.gradle b/jme3-screenshot-tests/build.gradle index 2d892ae089..ff0971ca4e 100644 --- a/jme3-screenshot-tests/build.gradle +++ b/jme3-screenshot-tests/build.gradle @@ -5,45 +5,3 @@ plugins { repositories { mavenCentral() } - -dependencies { - implementation project(':jme3-desktop') - implementation project(':jme3-core') - implementation project(':jme3-effects') - implementation project(':jme3-terrain') - implementation project(':jme3-lwjgl3') - implementation project(':jme3-plugins') - - implementation 'com.aventstack:extentreports:5.1.2' - implementation platform('org.junit:junit-bom:5.9.1') - implementation 'org.junit.jupiter:junit-jupiter' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testRuntimeOnly project(':jme3-testdata') -} - -testing { - suites { - test { - useJUnitJupiter('5.9.1') - } - } -} - -tasks.register("screenshotTest", Test) { - testClassesDirs = sourceSets.test.output.classesDirs - classpath = sourceSets.test.runtimeClasspath - useJUnitPlatform{ - filter{ - includeTags 'integration' - } - } -} - - -test { - useJUnitPlatform{ - filter{ - excludeTags 'integration' - } - } -} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/build.gradle b/jme3-screenshot-tests/jme3-screenshot-tests-android/build.gradle new file mode 100644 index 0000000000..63b576e894 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/build.gradle @@ -0,0 +1,68 @@ +plugins { + id "io.github.0ffz.github-packages" version "1.2.1" // Plugin for anonymous inclusion of artifacts hosted in github package registry +} + +apply plugin: 'com.android.application' + +android { + namespace "org.jmonkeyengine.screenshottests.android" + compileSdk 36 + + defaultConfig { + applicationId "org.jmonkeyengine.screenshottests.android" + minSdk 28 + targetSdk 36 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + configurations.all { + exclude group:"org.jmonkeyengine",module:"jme3-desktop" + } + + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + resources { + merges += "META-INF/LICENSE.md" + merges += "META-INF/LICENSE-notice.md" + } + } +} + +dependencies { + implementation libs.androidxAppcompat + implementation libs.googleMaterial + implementation libs.androidxConstraintlayout + + implementation project(":jme3-android") + implementation libs.jme3.android.natives + + implementation libs.androidx.test.core + implementation libs.androidx.test.ext.junit + implementation libs.androidx.test.rules + implementation libs.androidx.test.espresso.core + implementation libs.androidx.fragment.testing + implementation libs.jackson + debugImplementation libs.androidx.fragment.testing.manifest + androidTestImplementation libs.androidx.fragment.testing.manifest + debugImplementation libs.androidx.storage + + implementation project(':jme3-screenshot-tests:jme3-screenshot-tests-shared') + implementation project(':jme3-screenshot-tests:jme3-screenshot-tests-proto-report') + + implementation project(':jme3-testdata') + + // Allows the test to communicate with the orchestrator storage + androidTestImplementation libs.androidx.test.services + debugImplementation libs.androidx.test.services +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/proguard-rules.pro b/jme3-screenshot-tests/jme3-screenshot-tests-android/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/readme.md b/jme3-screenshot-tests/jme3-screenshot-tests-android/readme.md new file mode 100644 index 0000000000..0f40bbfc51 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/readme.md @@ -0,0 +1,45 @@ +# JMonkeyEngine Android Screenshot Tests + +This project contains screenshot tests specifically for Android. + +## Prerequisites + +To run these tests and collect the results, you need to install the Android Test Services APK on your device/emulator. This service allows the tests to write files (like screenshots and reports) to a persistent location that can be accessed via ADB. + +1. Download the Test Services APK: [test-services-1.6.0.apk](https://maven.google.com/androidx/test/services/test-services/1.6.0/test-services-1.6.0.apk) +2. Install it with necessary permissions: + ```bash + adb install -r -g test-services-1.6.0.apk + ``` + +## Collecting Test Results + +The tests are configured to use `androidx.test.services.storage.TestStorage` to save output files. After running the tests, you can pull the entire report folder (which includes the captured images and the proto-report JSON) using the following command: + +```bash +adb pull /storage/emulated/0/googletest/test_outputfiles/report/ +``` + +Individual files can also be pulled if needed: +```bash +adb pull /storage/emulated/0/googletest/test_outputfiles/report/screenshotProtoReport.json +``` + +## Generating Extent Reports + +The raw report is saved in a "proto-report" format (`screenshotProtoReport.json`). To convert this into a human-readable Extent Report (HTML), use the `upgradeProtoReport` Gradle task located in the `jme3-screenshot-tests-proto-report` module. + +### Usage + +Run the task and provide the input directory (where the pulled report is) and the desired output directory: + +```bash +./gradlew :jme3-screenshot-tests:jme3-screenshot-tests-proto-report:upgradeProtoReport --args="path/to/pulled/report path/to/output/extent-report" +``` + +## How it Works + +1. **Capture**: When tests run on Android, `ExtentReportExtensionJunit4` captures test status, logs, and screenshots. +2. **Storage**: Screenshots are saved as PNG files and test metadata is collected into a `ProtoReport` object. +3. **Persistence**: At the end of each test, the report metadata is serialized to `screenshotProtoReport.json` and saved to the device's persistent storage via `TestStorage`. +4. **Post-Processing**: Once pulled from the device, the `UpgradeProtoReportToExtentReport` tool processes the JSON and images to create a standalone HTML report with embedded screenshots. \ No newline at end of file diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/android/ExtentReportExtensionJunit4.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/android/ExtentReportExtensionJunit4.java new file mode 100644 index 0000000000..0efc76db01 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/android/ExtentReportExtensionJunit4.java @@ -0,0 +1,119 @@ +package org.jmonkeyengine.screenshottests.android.android; + +import androidx.test.services.storage.TestStorage; + +import com.jme3.system.JmeSystem; +import com.jme3.texture.Image; + +import org.jmonkeyengine.screenshottests.testframework.ExtentReportLogCapture; +import org.jmonkeyengine.screenshottests.testframework.TestReportCaptureBase; +import org.jmonkeyengine.screenshottests.testframework.protoreport.ProtoReport; +import org.jmonkeyengine.screenshottests.testframework.protoreport.ProtoReportTestItem; +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; + +import tools.jackson.databind.ObjectMapper; + +public class ExtentReportExtensionJunit4 extends TestReportCaptureBase implements org.junit.rules.TestRule { + + private static final String REPORT_DIRECTORY = "report"; + + static ProtoReport report = new ProtoReport("Screenshot Test Report - Android"); + ProtoReportTestItem testInProgress; + TestStorage testStorage = new TestStorage(); + + private final TestWatcher watcher = new TestWatcher() { + + @Override + protected void starting(Description description) { + ExtentReportLogCapture.initialize(); + testInProgress = new ProtoReportTestItem(description.getMethodName(), description.getTestClass().getSimpleName()); + } + + @Override + protected void succeeded(Description description) { + testInProgress.addStatus(ProtoReportTestItem.ReportStatus.PASSED, "Test passed"); + } + + @Override + protected void failed(Throwable e, Description description) { + testInProgress.addStatus(ProtoReportTestItem.ReportStatus.FAILED, getStackTraceAsString(e)); + } + + @Override + protected void skipped(AssumptionViolatedException e, Description description) { + testInProgress.addStatus(ProtoReportTestItem.ReportStatus.SKIPPED, e.getLocalizedMessage()); + } + + @Override + protected void finished(Description description) { + testInProgress.addLogs(ExtentReportLogCapture.getAndPurgeLogs()); + report.addTest(testInProgress); + ExtentReportLogCapture.restore(); + testInProgress = null; + persistReport(); // it sucks that we do this every test + } + }; + + @Override + public Statement apply(Statement base, Description description) { + TestReportCaptureBase.INSTANCE = this; + return watcher.apply(base, description); + } + + public static String getStackTraceAsString(Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + return sw.toString(); + } + + @Override + public void markFailInReport(String message) { + testInProgress.addStatus(ProtoReportTestItem.ReportStatus.FAILED, message); + } + + @Override + public void warning(String message) { + testInProgress.addStatus(ProtoReportTestItem.ReportStatus.WARNING, message); + } + + public OutputStream getPersistentFileOutputStream(String relativePath){ + try{ + return testStorage.openOutputFile(relativePath); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public void persistReport(){ + ObjectMapper mapper = new ObjectMapper(); + + String reportJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(report); + + try (OutputStream out = getPersistentFileOutputStream(REPORT_DIRECTORY + "/screenshotProtoReport.json")) { + out.write(reportJson.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void attachImage(String title, String fileName, Image image) { + try (OutputStream out = getPersistentFileOutputStream(REPORT_DIRECTORY + "/" + fileName)) { + JmeSystem.writeImageFile(out, "png",image.getData(0), image.getWidth(), image.getHeight()); + } catch (IOException e) { + throw new RuntimeException(e); + } + testInProgress.addImageReference(title, fileName); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/android/ScreenshotTestAndroidBase.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/android/ScreenshotTestAndroidBase.java new file mode 100644 index 0000000000..b6688fe6c0 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/android/ScreenshotTestAndroidBase.java @@ -0,0 +1,11 @@ +package org.jmonkeyengine.screenshottests.android.android; + +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; + +import org.junit.Rule; + +public abstract class ScreenshotTestAndroidBase extends ScreenshotTestBase { + + @Rule + public ExtentReportExtensionJunit4 extentReportExtension = new ExtentReportExtensionJunit4(); +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/animation/TestIssue2076.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/animation/TestIssue2076.java new file mode 100644 index 0000000000..e6f84ae3b7 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/animation/TestIssue2076.java @@ -0,0 +1,22 @@ +package org.jmonkeyengine.screenshottests.android.animation; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.animation.ScenarioIssue2076; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestIssue2076 extends ScreenshotTestAndroidBase { + + /** + * This test creates a scene with two Jaime models, one using the old animation system + * and one using the new animation system, both with software skinning and no vertex normals. + */ + @Test + public void testIssue2076() { + ScenarioIssue2076.testIssue2076().run(new AndroidRunner()); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/animation/TestMotionPath.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/animation/TestMotionPath.java new file mode 100644 index 0000000000..c11cebcb40 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/animation/TestMotionPath.java @@ -0,0 +1,21 @@ +package org.jmonkeyengine.screenshottests.android.animation; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.animation.ScenarioMotionPath; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestMotionPath extends ScreenshotTestAndroidBase { + + /** + * This test creates a scene with a teapot following a motion path. + */ + @Test + public void testMotionPath() { + ScenarioMotionPath.testMotionPath().run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/effects/TestExplosionEffect.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/effects/TestExplosionEffect.java new file mode 100644 index 0000000000..76db6a931c --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/effects/TestExplosionEffect.java @@ -0,0 +1,21 @@ +package org.jmonkeyengine.screenshottests.android.effects; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.effects.ScenarioExplosionEffect; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestExplosionEffect extends ScreenshotTestAndroidBase { + + /** + * This test's particle effects (using an explosion) + */ + @Test + public void testExplosionEffect() { + ScenarioExplosionEffect.testExplosionEffect().run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/effects/TestIssue1773.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/effects/TestIssue1773.java new file mode 100644 index 0000000000..9552b37ab9 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/effects/TestIssue1773.java @@ -0,0 +1,36 @@ +package org.jmonkeyengine.screenshottests.android.effects; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.effects.ScenarioIssue1773; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class TestIssue1773 extends ScreenshotTestAndroidBase { + + @Parameterized.Parameters(name = "worldSpace={0}") + public static Collection data() { + return Arrays.asList(new Object[][] { + { true }, { false } + }); + } + + private boolean worldSpace; + + public TestIssue1773(boolean worldSpace) { + this.worldSpace = worldSpace; + } + + @Test + public void testIssue1773() { + String imageName = getClass().getName() + ".testIssue1773" + (worldSpace ? "_worldSpace" : "_localSpace"); + ScenarioIssue1773.testIssue1773(worldSpace) + .setBaseImageFileName(imageName) + .run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/gui/TestBitmapText3D.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/gui/TestBitmapText3D.java new file mode 100644 index 0000000000..0ec916675c --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/gui/TestBitmapText3D.java @@ -0,0 +1,22 @@ +package org.jmonkeyengine.screenshottests.android.gui; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.gui.ScenarioBitmapText3D; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestBitmapText3D extends ScreenshotTestAndroidBase { + + /** + * This tests both that bitmap text is rendered correctly and that it is + * wrapped correctly. + */ + @Test + public void testBitmapText3D() { + ScenarioBitmapText3D.testBitmapText3D().run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/material/TestSimpleBumps.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/material/TestSimpleBumps.java new file mode 100644 index 0000000000..ab4bb40d2c --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/material/TestSimpleBumps.java @@ -0,0 +1,21 @@ +package org.jmonkeyengine.screenshottests.android.material; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.material.ScenarioSimpleBumps; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestSimpleBumps extends ScreenshotTestAndroidBase { + + /** + * This test creates a scene with a bump-mapped quad and an orbiting light. + */ + @Test + public void testSimpleBumps() { + ScenarioSimpleBumps.testSimpleBumps().run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/model/shape/TestBillboard.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/model/shape/TestBillboard.java new file mode 100644 index 0000000000..e841d517e8 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/model/shape/TestBillboard.java @@ -0,0 +1,41 @@ +package org.jmonkeyengine.screenshottests.android.model.shape; + +import com.jme3.math.Vector3f; +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.model.shape.ScenarioBillboard; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class TestBillboard extends ScreenshotTestAndroidBase { + + @Parameterized.Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList(new Object[][] { + { "fromFront", new Vector3f(0, 1, 15) }, + { "fromAbove", new Vector3f(0, 15, 6) }, + { "fromRight", new Vector3f(-15, 10, 5) } + }); + } + + private String testName; + private Vector3f cameraPosition; + + public TestBillboard(String testName, Vector3f cameraPosition) { + this.testName = testName; + this.cameraPosition = cameraPosition; + } + + @Test + public void testBillboard() { + String imageName = getClass().getName() + ".testBillboard_" + testName; + ScenarioBillboard.testBillboard(cameraPosition) + .setBaseImageFileName(imageName) + .run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/post/TestFog.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/post/TestFog.java new file mode 100644 index 0000000000..c1ba6e909a --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/post/TestFog.java @@ -0,0 +1,18 @@ +package org.jmonkeyengine.screenshottests.android.post; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.post.ScenarioFog; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestFog extends ScreenshotTestAndroidBase { + + @Test + public void testFog() { + ScenarioFog.testFog().run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/scene/instancing/TestInstanceNodeWithPbr.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/scene/instancing/TestInstanceNodeWithPbr.java new file mode 100644 index 0000000000..df7b20c84c --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/androidTest/java/org/jmonkeyengine/screenshottests/android/scene/instancing/TestInstanceNodeWithPbr.java @@ -0,0 +1,22 @@ +package org.jmonkeyengine.screenshottests.android.scene.instancing; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.jmonkeyengine.screenshottests.android.android.ScreenshotTestAndroidBase; +import org.jmonkeyengine.screenshottests.scenarios.scene.instancing.ScenarioInstanceNodeWithPbr; +import org.jmonkeyengine.screenshottests.testframework.AndroidRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TestInstanceNodeWithPbr extends ScreenshotTestAndroidBase { + + /** + * This test specifically validates the corrected PBR rendering when combined + * with instancing, as addressed in issue #2435. + */ + @Test + public void testInstanceNodeWithPbr() { + ScenarioInstanceNodeWithPbr.testInstanceNodeWithPbr().run(new AndroidRunner()); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/AndroidManifest.xml b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a880ca81da --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/java/org/jmonkeyengine/screenshottests/testframework/AndroidRunner.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/java/org/jmonkeyengine/screenshottests/testframework/AndroidRunner.java new file mode 100644 index 0000000000..c41e3b9ea6 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/java/org/jmonkeyengine/screenshottests/testframework/AndroidRunner.java @@ -0,0 +1,143 @@ +/* + * 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 android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.Environment; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentFactory; +import androidx.fragment.app.testing.FragmentScenario; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.services.storage.TestStorage; + +import com.jme3.system.JmeSystem; +import com.jme3.texture.Image; + +import org.junit.Assert; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +@SuppressLint("RestrictedApi") +public class AndroidRunner implements AppRunner { + + private static final Logger logger = Logger.getLogger(AndroidRunner.class.getName()); + + /** + * Any created files are normally deleted at the end of the test. Using TestStorage + * we can persist them (requires test-services to be installed, see the readme) + */ + TestStorage testStorage = new TestStorage(); + + @Override + public void runApplicationUntilScenarioCompletes(TestContainingApp application, CountDownLatch applicationFinishedLatch) { + + FragmentFactory fragmentFactory = new FragmentFactory(){ + @NonNull + @Override + public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) { + if (className.equals(AndroidTestHarness.class.getName())) { + return new AndroidTestHarness(application, applicationFinishedLatch); + } + return super.instantiate(classLoader, className); + } + }; + try (FragmentScenario scenario = FragmentScenario.launchInContainer( + AndroidTestHarness.class, + Bundle.EMPTY, + androidx.appcompat.R.style.Theme_AppCompat, + fragmentFactory + )) { + int maxWaitTimeMilliseconds = 45000; + + try { + boolean exitedProperly = applicationFinishedLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS); + + if (!exitedProperly) { + logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out"); + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + try{ + Thread.sleep(1000); //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); + } + } + + @Override + public Path getChangedImagesDirectory() { + return InstrumentationRegistry + .getInstrumentation() + .getTargetContext() + .getExternalFilesDir(null) + .toPath() + .resolve("changed-images"); + } + + public OutputStream getPersistentFileOutputStream(String relativePath){ + try{ + return testStorage.openOutputFile(relativePath); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public V fail(String s) { + Assert.fail(s); + return (V)null; + } + + @Override + public void saveGeneratedImageToChangedImages(Image generatedImage, String fileName) { + try (OutputStream out = getPersistentFileOutputStream("changed-images/" + fileName)) { + JmeSystem.writeImageFile(out, "png",generatedImage.getData(0), generatedImage.getWidth(), generatedImage.getHeight()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/java/org/jmonkeyengine/screenshottests/testframework/AndroidTestHarness.java b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/java/org/jmonkeyengine/screenshottests/testframework/AndroidTestHarness.java new file mode 100644 index 0000000000..67fab9759c --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/java/org/jmonkeyengine/screenshottests/testframework/AndroidTestHarness.java @@ -0,0 +1,44 @@ +package org.jmonkeyengine.screenshottests.testframework; + +import android.opengl.GLSurfaceView; + +import com.jme3.app.AndroidHarnessFragment; +import com.jme3.app.LegacyApplication; +import com.jme3.app.SimpleApplication; +import com.jme3.system.AppSettings; + +import java.util.concurrent.CountDownLatch; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AndroidTestHarness extends AndroidHarnessFragment { + + private static final Logger logger = Logger.getLogger(AndroidTestHarness.class.getName()); + + private final SimpleApplication application; + private final CountDownLatch applicationFinishedLatch; + + public AndroidTestHarness(SimpleApplication application, CountDownLatch applicationFinishedLatch) { + this.application = application; + this.applicationFinishedLatch = applicationFinishedLatch; + } + + @Override + protected LegacyApplication createApplication(){ + return application; + } + + @Override + protected void configureSettings(AppSettings settings) { + super.configureSettings(settings); + settings.setAudioRenderer(null); + } + + @Override + public void handleError(String errorMsg, Throwable throwable) { + logger.log(Level.WARNING, "Error in test application", throwable); + applicationFinishedLatch.countDown(); + + super.handleError(errorMsg, throwable); + } +} diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/drawable-v24/ic_launcher_foreground.xml b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..7706ab9e6d --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/drawable/ic_launcher_background.xml b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..07d5da9cbf --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/layout/activity_main.xml b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..b9439b9502 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-android/src/main/res/layout/activity_main.xml @@ -0,0 +1,31 @@ + + + + + + + +