diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index ca97aaf07a..7bc43cbaf7 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -100,7 +100,6 @@ jobs: target: google_apis script: | ./scripts/run-android-instrumentation-tests.sh "${{ steps.build-android-app.outputs.gradle_project_dir }}" - ./scripts/generate-android-coverage-report.sh "${{ steps.build-android-app.outputs.gradle_project_dir }}" - name: Upload emulator screenshot if: always() # still collect it if tests fail uses: actions/upload-artifact@v4 @@ -110,12 +109,3 @@ jobs: if-no-files-found: warn retention-days: 14 compression-level: 6 - - name: Upload Android Jacoco coverage report - if: always() - uses: actions/upload-artifact@v4 - with: - name: android-jacoco-coverage - path: artifacts/android-coverage-report - if-no-files-found: warn - retention-days: 14 - compression-level: 6 diff --git a/scripts/android/lib/PatchGradleFiles.java b/scripts/android/lib/PatchGradleFiles.java index 3bf2ef2643..549da1dbf5 100644 --- a/scripts/android/lib/PatchGradleFiles.java +++ b/scripts/android/lib/PatchGradleFiles.java @@ -106,10 +106,6 @@ private static boolean patchAppBuildGradle(Path path, int compileSdk, int target content = r.content(); changed |= r.changed(); - r = ensureJacocoConfiguration(content); - content = r.content(); - changed |= r.changed(); - if (changed) { Files.writeString(path, ensureTrailingNewline(content), StandardCharsets.UTF_8); } @@ -251,88 +247,6 @@ private static Result ensureTestDependencies(String content) { return new Result(content + block, true); } - private static Result ensureJacocoConfiguration(String content) { - if (content.contains("jacocoAndroidReport")) { - return new Result(content, false); - } - - String jacocoBlock = """ -apply plugin: 'jacoco' - -android { - buildTypes { - debug { - testCoverageEnabled true - } - } -} - -jacoco { - toolVersion = "0.8.11" -} - - tasks.register("jacocoAndroidReport", JacocoReport) { - group = "verification" - description = "Generates Jacoco coverage report for the debug variant" - outputs.upToDateWhen { false } - - reports { - xml.required = true - html.required = true - html.outputLocation = layout.buildDirectory.dir("reports/jacoco/jacocoAndroidReport/html") - } - - def coverageFiles = fileTree(dir: "$buildDir", includes: [ - "outputs/code_coverage/**/*coverage.ec", - "jacoco/*.exec", - "outputs/unit_test_code_coverage/**/*coverage.ec", - "**/*.ec", - "**/*.exec" - ]) - - def excludes = [ - '**/R.class', - '**/R$*.class', - '**/BuildConfig.*', - '**/Manifest*.*', - '**/*Test*.*', - '**/androidx/**/*', - '**/com/google/**/*' - ] - - def javaClasses = fileTree(dir: "$buildDir/intermediates/javac/debug/classes", exclude: excludes) - def kotlinClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", exclude: excludes) - def aarMainJar = file("$buildDir/intermediates/aar_main_jar/debug/classes.jar") - def aarTrees = aarMainJar.exists() ? [zipTree(aarMainJar)] : [] - def runtimeJars = configurations.debugRuntimeClasspath.filter { it.name.endsWith('.jar') }.collect { zipTree(it) } - - classDirectories.setFrom(files(javaClasses, kotlinClasses, aarTrees, runtimeJars).asFileTree.matching { - include 'com/codename1/impl/android/**' - }) - - sourceDirectories.setFrom(files("src/main/java")) - - executionData.setFrom(coverageFiles) - - doFirst { - def existing = coverageFiles.files.findAll { it.exists() } - if (existing.isEmpty()) { - throw new GradleException("No Jacoco coverage data found. Ensure connectedDebugAndroidTest runs with coverage enabled.") - } - logger.lifecycle("Jacoco coverage inputs: ${existing}") - } -} - -afterEvaluate { - tasks.matching { it.name == "connectedDebugAndroidTest" }.configureEach { - finalizedBy(tasks.named("jacocoAndroidReport")) - } -} -""".stripTrailing(); - - return new Result(ensureTrailingNewline(content) + "\n" + jacocoBlock + "\n", true); - } - private static String ensureTrailingNewline(String content) { return content.endsWith("\n") ? content : content + "\n"; } diff --git a/scripts/android/tests/Cn1ssChunkTools.java b/scripts/android/tests/Cn1ssChunkTools.java index 7fd03c0ab8..1cf9c52225 100644 --- a/scripts/android/tests/Cn1ssChunkTools.java +++ b/scripts/android/tests/Cn1ssChunkTools.java @@ -129,7 +129,7 @@ private static void runTests(String[] args) throws IOException { } Path path = Path.of(args[0]); List names = new ArrayList<>(); - for (Chunk chunk : iterateChunks(path, Optional.empty(), Optional.of(DEFAULT_CHANNEL))) { + for (Chunk chunk : iterateChunks(path, Optional.empty(), Optional.empty())) { if (!names.contains(chunk.testName)) { names.add(chunk.testName); } @@ -139,7 +139,6 @@ private static void runTests(String[] args) throws IOException { System.out.println(name); } } - private static Iterable iterateChunks(Path path, Optional testFilter, Optional channelFilter) throws IOException { String text = Files.readString(path, StandardCharsets.UTF_8); List result = new ArrayList<>(); diff --git a/scripts/android/tests/HelloCodenameOneInstrumentedTest.java b/scripts/android/tests/HelloCodenameOneInstrumentedTest.java deleted file mode 100644 index a72a94d9b1..0000000000 --- a/scripts/android/tests/HelloCodenameOneInstrumentedTest.java +++ /dev/null @@ -1,382 +0,0 @@ -package @PACKAGE@; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.util.Base64; -import android.util.DisplayMetrics; -import android.view.View; - -import androidx.test.core.app.ActivityScenario; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.codename1.ui.Container; -import com.codename1.ui.BrowserComponent; -import com.codename1.ui.Display; -import com.codename1.ui.Form; -import com.codename1.ui.Label; -import com.codename1.ui.layouts.BorderLayout; -import com.codename1.ui.layouts.BoxLayout; - -import org.junit.Assert; -import org.junit.Assume; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.ByteArrayOutputStream; -import java.util.Locale; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import com.codename1.io.Log; - -@RunWith(AndroidJUnit4.class) -public class HelloCodenameOneInstrumentedTest { - - private static final int CHUNK_SIZE = 2000; - private static final String PREVIEW_CHANNEL = "PREVIEW"; - private static final int[] PREVIEW_JPEG_QUALITIES = - new int[] {60, 50, 40, 35, 30, 25, 20, 18, 16, 14, 12, 10, 8, 6, 5, 4, 3, 2, 1}; - private static final int MAX_PREVIEW_BYTES = 20 * 1024; // 20 KiB target keeps comment payloads small - private static final String MAIN_SCREEN_TEST = "MainActivity"; - private static final String BROWSER_TEST = "BrowserComponent"; - - private static void println(String s) { - System.out.println(s); - } - - private static void settle(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } - - private static ActivityScenario launchMainActivity(Context ctx) { - String pkg = "@PACKAGE@"; - Intent launch = ctx.getPackageManager().getLaunchIntentForPackage(pkg); - if (launch == null) { - Intent q = new Intent(Intent.ACTION_MAIN); - q.addCategory(Intent.CATEGORY_LAUNCHER); - q.setPackage(pkg); - launch = q; - } - launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - println("CN1SS:INFO: launching activity for test"); - return ActivityScenario.launch(launch); - } - - private static final class ScreenshotCapture { - final byte[] png; - final byte[] previewJpeg; - final int previewQuality; - - ScreenshotCapture(byte[] png, byte[] previewJpeg, int previewQuality) { - this.png = png; - this.previewJpeg = previewJpeg; - this.previewQuality = previewQuality; - } - } - - private static ScreenshotCapture captureScreenshot(ActivityScenario scenario, String testName) { - final byte[][] holder = new byte[2][]; - final int[] qualityHolder = new int[1]; - final CountDownLatch latch = new CountDownLatch(1); - - scenario.onActivity(activity -> Display.getInstance().callSerially(() -> { - try { - // Use Codename One screenshot API which properly captures PeerComponents - final com.codename1.ui.Image[] screenshotHolder = new com.codename1.ui.Image[1]; - Display.getInstance().screenshot(img -> { - screenshotHolder[0] = img; - }); - - // Wait for screenshot to complete using invokeAndBlock to yield EDT - // This allows the screenshot callback to execute on the EDT - final long startTime = System.currentTimeMillis(); - Display.getInstance().invokeAndBlock(() -> { - while (screenshotHolder[0] == null) { - try { - Thread.sleep(50); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - break; - } - if (System.currentTimeMillis() - startTime > 5000) { - println("CN1SS:ERR:test=" + testName + " screenshot timeout"); - break; - } - } - }); - - if (screenshotHolder[0] == null) { - println("CN1SS:ERR:test=" + testName + " screenshot returned null"); - return; - } - - // Convert CN1 Image to Android Bitmap - com.codename1.ui.Image screenshot = screenshotHolder[0]; - Object nativeImage = screenshot.getImage(); - Bitmap bmp; - - if (nativeImage instanceof Bitmap) { - bmp = (Bitmap) nativeImage; - } else { - println("CN1SS:ERR:test=" + testName + " unexpected native image type: " + - (nativeImage != null ? nativeImage.getClass().getName() : "null")); - return; - } - - int w = bmp.getWidth(); - int h = bmp.getHeight(); - println("CN1SS:INFO:test=" + testName + " screenshot size " + w + "x" + h); - - ByteArrayOutputStream pngOut = new ByteArrayOutputStream(Math.max(1024, w * h / 2)); - if (!bmp.compress(Bitmap.CompressFormat.PNG, 100, pngOut)) { - throw new RuntimeException("Bitmap.compress returned false"); - } - holder[0] = pngOut.toByteArray(); - println( - "CN1SS:INFO:test=" - + testName - + " png_bytes=" - + holder[0].length); - - int chosenQuality = 0; - byte[] chosenPreview = null; - int smallestBytes = Integer.MAX_VALUE; - - for (int quality : PREVIEW_JPEG_QUALITIES) { - ByteArrayOutputStream jpegOut = new ByteArrayOutputStream(Math.max(1024, w * h / 2)); - if (!bmp.compress(Bitmap.CompressFormat.JPEG, quality, jpegOut)) { - continue; - } - byte[] jpegBytes = jpegOut.toByteArray(); - int length = jpegBytes.length; - if (length < smallestBytes) { - smallestBytes = length; - chosenQuality = quality; - chosenPreview = jpegBytes; - } - if (length <= MAX_PREVIEW_BYTES) { - break; - } - } - - holder[1] = chosenPreview; - qualityHolder[0] = chosenQuality; - if (chosenPreview != null) { - println( - "CN1SS:INFO:test=" - + testName - + " preview_jpeg_bytes=" - + chosenPreview.length - + " preview_quality=" - + chosenQuality); - if (chosenPreview.length > MAX_PREVIEW_BYTES) { - println( - "CN1SS:WARN:test=" - + testName - + " preview_exceeds_limit_bytes=" - + chosenPreview.length - + " max_preview_bytes=" - + MAX_PREVIEW_BYTES); - } - } else { - println("CN1SS:INFO:test=" + testName + " preview_jpeg_bytes=0 preview_quality=0"); - } - } catch (Throwable t) { - println("CN1SS:ERR:test=" + testName + " " + t); - Log.e(t); - } finally { - latch.countDown(); - } - })); - - try { - if (!latch.await(10, TimeUnit.SECONDS)) { - println("CN1SS:ERR:test=" + testName + " latch timeout"); - } - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - println("CN1SS:ERR:test=" + testName + " interrupted"); - } - - if (holder[0] == null) { - return new ScreenshotCapture(null, null, 0); - } - return new ScreenshotCapture(holder[0], holder[1], qualityHolder[0]); - } - - private static String sanitizeTestName(String testName) { - return testName.replaceAll("[^A-Za-z0-9_.-]", "_"); - } - - private static void emitScreenshot(ScreenshotCapture capture, String testName) { - if (capture == null || capture.png == null || capture.png.length == 0) { - println("CN1SS:END:" + sanitizeTestName(testName)); - Assert.fail("Screenshot capture produced 0 bytes for " + testName); - return; - } - emitScreenshotChannel(capture.png, testName, ""); - if (capture.previewJpeg != null && capture.previewJpeg.length > 0) { - emitScreenshotChannel(capture.previewJpeg, testName, PREVIEW_CHANNEL); - } - } - - private static void emitScreenshotChannel(byte[] bytes, String testName, String channel) { - String safeName = sanitizeTestName(testName); - String prefix = "CN1SS"; - if (channel != null && channel.length() > 0) { - prefix += channel; - } - if (bytes == null || bytes.length == 0) { - println(prefix + ":END:" + safeName); - return; - } - String b64 = Base64.encodeToString(bytes, Base64.NO_WRAP); - int count = 0; - for (int pos = 0; pos < b64.length(); pos += CHUNK_SIZE) { - int end = Math.min(pos + CHUNK_SIZE, b64.length()); - String chunk = b64.substring(pos, end); - System.out.println( - prefix - + ":" - + safeName - + ":" - + String.format(Locale.US, "%06d", pos) - + ":" - + chunk); - count++; - } - println("CN1SS:INFO:test=" + safeName + " chunks=" + count + " total_b64_len=" + b64.length()); - System.out.println(prefix + ":END:" + safeName); - System.out.flush(); - } - - private static void prepareBrowserComponentContent(ActivityScenario scenario) throws InterruptedException { - final CountDownLatch supportLatch = new CountDownLatch(1); - final boolean[] supported = new boolean[1]; - - scenario.onActivity(activity -> Display.getInstance().callSerially(() -> { - try { - supported[0] = BrowserComponent.isNativeBrowserSupported(); - } finally { - supportLatch.countDown(); - } - })); - - if (!supportLatch.await(5, TimeUnit.SECONDS)) { - Assert.fail("Timed out while verifying BrowserComponent support"); - } - - Assume.assumeTrue("BrowserComponent native support required for this test", supported[0]); - - final CountDownLatch loadLatch = new CountDownLatch(1); - final String html = "" - + "" - + "

Codename One

" - + "

BrowserComponent instrumentation test content.

"; - - scenario.onActivity(activity -> Display.getInstance().callSerially(() -> { - Form current = Display.getInstance().getCurrent(); - if (current == null) { - current = new Form("Browser Test", new BorderLayout()); - current.show(); - } else { - current.setLayout(new BorderLayout()); - current.setTitle("Browser Test"); - current.removeAll(); - } - - BrowserComponent browser = new BrowserComponent(); - browser.addWebEventListener(BrowserComponent.onLoad, evt -> loadLatch.countDown()); - browser.setPage(html, null); - current.add(BorderLayout.CENTER, browser); - current.revalidate(); - })); - - if (!loadLatch.await(10, TimeUnit.SECONDS)) { - Assert.fail("Timed out waiting for BrowserComponent to load content"); - } - } - - private static void prepareMainActivityContent(ActivityScenario scenario) throws InterruptedException { - final CountDownLatch latch = new CountDownLatch(1); - scenario.onActivity(activity -> Display.getInstance().callSerially(() -> { - try { - Form current = Display.getInstance().getCurrent(); - if (current == null) { - current = new Form("Main Screen", new BorderLayout()); - current.show(); - } else { - current.setLayout(new BorderLayout()); - current.setTitle("Main Screen"); - current.removeAll(); - } - - Container content = new Container(BoxLayout.y()); - content.getAllStyles().setBgColor(0x1f2937); - content.getAllStyles().setBgTransparency(255); - content.getAllStyles().setPadding(6, 6, 6, 6); - content.getAllStyles().setFgColor(0xf9fafb); - - Label heading = new Label("Hello Codename One"); - heading.getAllStyles().setFgColor(0x38bdf8); - heading.getAllStyles().setMargin(0, 4, 0, 0); - - Label body = new Label("Instrumentation main activity preview"); - body.getAllStyles().setFgColor(0xf9fafb); - - content.add(heading); - content.add(body); - - current.add(BorderLayout.CENTER, content); - current.revalidate(); - } finally { - latch.countDown(); - } - })); - - if (!latch.await(5, TimeUnit.SECONDS)) { - Assert.fail("Timed out preparing main activity content"); - } - } - - @Test - public void testUseAppContext_andEmitScreenshot() throws Exception { - Context ctx = ApplicationProvider.getApplicationContext(); - String pkg = "@PACKAGE@"; - Assert.assertEquals("Package mismatch", pkg, ctx.getPackageName()); - - ScreenshotCapture capture; - try (ActivityScenario scenario = launchMainActivity(ctx)) { - settle(750); - prepareMainActivityContent(scenario); - settle(500); - capture = captureScreenshot(scenario, MAIN_SCREEN_TEST); - } - - emitScreenshot(capture, MAIN_SCREEN_TEST); - } - - @Test - public void testBrowserComponentScreenshot() throws Exception { - Context ctx = ApplicationProvider.getApplicationContext(); - ScreenshotCapture capture; - - try (ActivityScenario scenario = launchMainActivity(ctx)) { - settle(750); - prepareBrowserComponentContent(scenario); - settle(500); - capture = captureScreenshot(scenario, BROWSER_TEST); - } - - emitScreenshot(capture, BROWSER_TEST); - } -} diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 57e4009e44..d7be421dff 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -270,22 +270,6 @@ fi ba_log "Normalizing Android Gradle project in $GRADLE_PROJECT_DIR" -# --- Install Android instrumentation harness for coverage --- -ANDROID_TEST_SOURCE_DIR="$SCRIPT_DIR/device-runner-app/androidTest" -ANDROID_TEST_ROOT="$GRADLE_PROJECT_DIR/app/src/androidTest" -ANDROID_TEST_JAVA_DIR="$ANDROID_TEST_ROOT/java/${PACKAGE_PATH}" -if [ -d "$ANDROID_TEST_ROOT" ]; then - ba_log "Removing template Android instrumentation tests from $ANDROID_TEST_ROOT" - rm -rf "$ANDROID_TEST_ROOT" -fi -mkdir -p "$ANDROID_TEST_JAVA_DIR" -if [ ! -d "$ANDROID_TEST_SOURCE_DIR" ]; then - ba_log "Android instrumentation test sources not found: $ANDROID_TEST_SOURCE_DIR" >&2 - exit 1 -fi -cp "$ANDROID_TEST_SOURCE_DIR"/*.java "$ANDROID_TEST_JAVA_DIR"/ -ba_log "Installed Android instrumentation tests in $ANDROID_TEST_JAVA_DIR" - # Ensure AndroidX flags in gradle.properties # --- BEGIN: robust Gradle patch for AndroidX tests --- GRADLE_PROPS="$GRADLE_PROJECT_DIR/gradle.properties" diff --git a/scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java b/scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java deleted file mode 100644 index b884e7f64a..0000000000 --- a/scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.codenameone.examples.hellocodenameone; - -import android.app.UiAutomation; -import android.content.Context; -import android.content.Intent; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.InputStreamReader; - -import static org.junit.Assert.assertNotNull; - -@RunWith(AndroidJUnit4.class) -public class DeviceRunnerInstrumentationTest { - private static final String TAG = "DeviceRunnerTest"; - - @Test - public void launchMainActivityAndWaitForDeviceRunner() throws Exception { - Context context = ApplicationProvider.getApplicationContext(); - Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); - assertNotNull("Launch intent not found for package " + context.getPackageName(), intent); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - - boolean finished = waitForDeviceRunner(); - if (!finished) { - Log.w(TAG, "DeviceRunner did not emit completion marker; proceeding without hard failure"); - } - } - - private boolean waitForDeviceRunner() throws Exception { - final long timeoutMs = 300_000L; - final String endMarker = "CN1SS:SUITE:FINISHED"; - - long deadline = System.currentTimeMillis() + timeoutMs; - UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); - ParcelFileDescriptor pfd = automation.executeShellCommand("logcat -v brief"); - try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor()); - BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) { - String line; - while (System.currentTimeMillis() < deadline) { - if (reader.ready() && (line = reader.readLine()) != null) { - if (line.contains(endMarker)) { - return true; - } - } else { - Thread.sleep(200); - } - } - } finally { - try { - pfd.close(); - } catch (Exception ignored) { - } - } - return false; - } -} diff --git a/scripts/device-runner-app/main/HelloCodenameOne.java b/scripts/device-runner-app/main/HelloCodenameOne.java index 92aec4afd7..340199d6fd 100644 --- a/scripts/device-runner-app/main/HelloCodenameOne.java +++ b/scripts/device-runner-app/main/HelloCodenameOne.java @@ -10,6 +10,7 @@ import com.codename1.ui.Label; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.CN; import com.codenameone.examples.hellocodenameone.tests.Cn1ssDeviceRunner; import com.codenameone.examples.hellocodenameone.tests.Cn1ssDeviceRunnerReporter; @@ -30,9 +31,10 @@ public void start() { } if (!deviceRunnerExecuted) { deviceRunnerExecuted = true; - new Cn1ssDeviceRunner().runSuite(); + CN.callSerially(() -> new Cn1ssDeviceRunner().runSuite()); } - showMainForm(); + new Form("Fallback").show(); + //showMainForm(); } public void stop() { @@ -42,59 +44,4 @@ public void stop() { public void destroy() { // Nothing to clean up for this sample } - - private void showMainForm() { - if (mainForm == null) { - mainForm = new Form("Main Screen", new BorderLayout()); - - Container content = new Container(BoxLayout.y()); - content.getAllStyles().setBgColor(0x1f2937); - content.getAllStyles().setBgTransparency(255); - content.getAllStyles().setPadding(6, 6, 6, 6); - content.getAllStyles().setFgColor(0xf9fafb); - - Label heading = new Label("Hello Codename One"); - heading.getAllStyles().setFgColor(0x38bdf8); - heading.getAllStyles().setMargin(0, 4, 0, 0); - - Label body = new Label("Instrumentation main activity preview"); - body.getAllStyles().setFgColor(0xf9fafb); - - Button openBrowser = new Button("Open Browser Screen"); - openBrowser.addActionListener(evt -> showBrowserForm()); - - content.add(heading); - content.add(body); - content.add(openBrowser); - - mainForm.add(BorderLayout.CENTER, content); - } - current = mainForm; - mainForm.show(); - } - - private void showBrowserForm() { - Form browserForm = new Form("Browser Screen", new BorderLayout()); - - BrowserComponent browser = new BrowserComponent(); - browser.setPage(buildBrowserHtml(), null); - browserForm.add(BorderLayout.CENTER, browser); - browserForm.getToolbar().addMaterialCommandToLeftBar( - "Back", - FontImage.MATERIAL_ARROW_BACK, - evt -> showMainForm() - ); - - current = browserForm; - browserForm.show(); - } - - private String buildBrowserHtml() { - return "" - + "" - + "

Codename One

" - + "

BrowserComponent instrumentation test content.

"; - } } diff --git a/scripts/device-runner-app/tests/Cn1ssDeviceRunner.java b/scripts/device-runner-app/tests/Cn1ssDeviceRunner.java index 1de8bfe878..911a758cfe 100644 --- a/scripts/device-runner-app/tests/Cn1ssDeviceRunner.java +++ b/scripts/device-runner-app/tests/Cn1ssDeviceRunner.java @@ -4,26 +4,42 @@ import com.codename1.testing.TestReporting; import com.codename1.ui.Display; import com.codename1.ui.Form; +import com.codename1.testing.AbstractTest; public final class Cn1ssDeviceRunner extends DeviceRunner { - private static final String[] TEST_CLASSES = new String[] { - MainScreenScreenshotTest.class.getName(), - BrowserComponentScreenshotTest.class.getName(), - MediaPlaybackScreenshotTest.class.getName(), - GraphicsPipelineScreenshotTest.class.getName(), - GraphicsShapesAndGradientsScreenshotTest.class.getName(), - GraphicsStateAndTextScreenshotTest.class.getName(), - GraphicsTransformationsScreenshotTest.class.getName(), - GraphicsMethodsScreenshotTest.class.getName() + private static final AbstractTest[] TEST_CLASSES = new AbstractTest[] { + new MainScreenScreenshotTest(), + new GraphicsPipelineScreenshotTest(), + new GraphicsShapesAndGradientsScreenshotTest(), + new GraphicsStateAndTextScreenshotTest(), + new GraphicsTransformationsScreenshotTest(), + new GraphicsMethodsScreenshotTest(), + new BrowserComponentScreenshotTest(), + new MediaPlaybackScreenshotTest() }; public void runSuite() { - for (String testClass : TEST_CLASSES) { - runTest(testClass); + for (AbstractTest testClass : TEST_CLASSES) { + log("CN1SS:INFO:suite starting test=" + testClass); + try { + testClass.prepare(); + testClass.runTest(); + testClass.cleanup(); + log("CN1SS:INFO:suite finished test=" + testClass); + } catch (Throwable t) { + log("CN1SS:ERR:suite test=" + testClass + " failed=" + t); + t.printStackTrace(); + // continue with next test instead of aborting + } } + log("CN1SS:SUITE:FINISHED"); TestReporting.getInstance().testExecutionFinished(getClass().getName()); } + private static void log(String msg) { + System.out.println(msg); + } + @Override protected void startApplicationInstance() { Cn1ssDeviceRunnerHelper.runOnEdtSync(() -> { diff --git a/scripts/generate-android-coverage-report.sh b/scripts/generate-android-coverage-report.sh index e0da4d6285..a1c84d178e 100755 --- a/scripts/generate-android-coverage-report.sh +++ b/scripts/generate-android-coverage-report.sh @@ -1,244 +1,5 @@ #!/usr/bin/env bash -# Generate a Jacoco coverage report for the Android sample app set -euo pipefail -cov_log() { echo "[generate-android-coverage] $1"; } - -publish_coverage_preview() { - local source_dir="$1" html_index="$2" - local server_url="${GITHUB_SERVER_URL:-}" repository="${GITHUB_REPOSITORY:-}" token="${GITHUB_TOKEN:-${GH_TOKEN:-}}" - local run_id="${GITHUB_RUN_ID:-local}" run_attempt="${GITHUB_RUN_ATTEMPT:-1}" actor="${GITHUB_ACTOR:-github-actions[bot]}" - - if [ "$server_url" != "https://github.com" ]; then - cov_log "Skipping coverage preview publish (unsupported server: ${server_url:-})" - return 1 - fi - if [ -z "$repository" ] || [ -z "$token" ]; then - cov_log "Skipping coverage preview publish (missing repository or token)" - return 1 - fi - if [ ! -d "$source_dir" ]; then - cov_log "Skipping coverage preview publish (source directory missing: $source_dir)" - return 1 - fi - if [ -z "$html_index" ] || [ ! -f "$source_dir/$html_index" ]; then - cov_log "Skipping coverage preview publish (HTML index not found: $source_dir/$html_index)" - return 1 - fi - - local tmp_dir run_dir dest_dir remote_url commit_sha raw_base preview_base - tmp_dir="$(mktemp -d)" - run_dir="runs/${run_id}-${run_attempt}/android-coverage" - dest_dir="${tmp_dir}/${run_dir}" - mkdir -p "$dest_dir" - - cp -R "$source_dir"/. "$dest_dir"/ - - if ! git -C "$tmp_dir" init -b previews >/dev/null 2>&1; then - cov_log "Failed to initialize preview git repository" - rm -rf "$tmp_dir" - return 1 - fi - git -C "$tmp_dir" config user.name "$actor" >/dev/null - git -C "$tmp_dir" config user.email "github-actions@users.noreply.github.com" >/dev/null - git -C "$tmp_dir" add . >/dev/null - if ! git -C "$tmp_dir" commit -m "Publish Android coverage preview for run ${run_id} (attempt ${run_attempt})" >/dev/null 2>&1; then - cov_log "No changes to commit for coverage preview" - rm -rf "$tmp_dir" - return 1 - fi - - remote_url="${server_url}/${repository}.git" - remote_url="${remote_url/https:\/\//https://x-access-token:${token}@}" - if ! git -C "$tmp_dir" push --force "$remote_url" previews:quality-report-previews >/dev/null 2>&1; then - cov_log "Failed to push coverage preview to quality-report-previews" - rm -rf "$tmp_dir" - return 1 - fi - - commit_sha="$(git -C "$tmp_dir" rev-parse HEAD)" - raw_base="https://raw.githubusercontent.com/${repository}/${commit_sha}/${run_dir}" - preview_base="https://htmlpreview.github.io/?${raw_base}" - echo "${preview_base}/${html_index}" - - rm -rf "$tmp_dir" - return 0 -} - -if [ $# -lt 1 ]; then - cov_log "Usage: $0 " >&2 - exit 2 -fi - -GRADLE_PROJECT_DIR="$1" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$REPO_ROOT" - -TMPDIR="${TMPDIR:-/tmp}"; TMPDIR="${TMPDIR%/}" -DOWNLOAD_DIR="${TMPDIR}/codenameone-tools" -ENV_DIR="$DOWNLOAD_DIR/tools" -ENV_FILE="$ENV_DIR/env.sh" - -ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$REPO_ROOT}/artifacts}" -mkdir -p "$ARTIFACTS_DIR" -REPORT_DEST_DIR="$ARTIFACTS_DIR/android-coverage-report" - -cov_log "Loading workspace environment from $ENV_FILE" -if [ -f "$ENV_FILE" ]; then - # shellcheck disable=SC1090 - source "$ENV_FILE" - cov_log "Loaded environment: JAVA_HOME=${JAVA_HOME:-} JAVA17_HOME=${JAVA17_HOME:-}" -else - cov_log "Workspace tools not found. Run scripts/setup-workspace.sh before this script." >&2 - exit 1 -fi - -if [ ! -d "$GRADLE_PROJECT_DIR" ]; then - cov_log "Gradle project directory not found: $GRADLE_PROJECT_DIR" >&2 - exit 3 -fi - -if [ ! -x "$GRADLE_PROJECT_DIR/gradlew" ]; then - cov_log "Gradle wrapper missing at $GRADLE_PROJECT_DIR/gradlew" >&2 - exit 3 -fi - -COVERAGE_SCAN=$(find "$GRADLE_PROJECT_DIR/app/build" -type f \( -name "*.ec" -o -name "*.exec" \) 2>/dev/null | sed 's/^/[generate-android-coverage] coverage-file: /') -if [ -n "$COVERAGE_SCAN" ]; then - echo "$COVERAGE_SCAN" -else - cov_log "No coverage data files detected under $GRADLE_PROJECT_DIR/app/build" -fi - -REPORT_SOURCE_DIR="$GRADLE_PROJECT_DIR/app/build/reports/jacoco/jacocoAndroidReport" -if [ ! -d "$REPORT_SOURCE_DIR" ]; then - cov_log "Coverage report directory not found: $REPORT_SOURCE_DIR" >&2 - exit 4 -fi - -rm -rf "$REPORT_DEST_DIR" -mkdir -p "$REPORT_DEST_DIR" -cp -R "$REPORT_SOURCE_DIR"/ "${REPORT_DEST_DIR}"/ - -SUMMARY_OUT="$REPORT_DEST_DIR/coverage-summary.json" -ARTIFACT_NAME="android-coverage-report" -HTML_INDEX="jacocoAndroidReport/html/index.html" -REPORT_XML_PATH="$REPORT_DEST_DIR/jacocoAndroidReport.xml" - -if [ ! -f "$REPORT_XML_PATH" ]; then - alt_xml="$(find "$REPORT_DEST_DIR" -maxdepth 3 -type f -name '*.xml' | head -n1)" - if [ -n "$alt_xml" ]; then - cov_log "Using fallback coverage XML: $alt_xml" - REPORT_XML_PATH="$alt_xml" - fi -fi - -if preview_url=$(publish_coverage_preview "$REPORT_DEST_DIR" "$HTML_INDEX"); then - export ANDROID_COVERAGE_HTML_URL="$preview_url" - cov_log "Published coverage HTML preview: $ANDROID_COVERAGE_HTML_URL" -fi - -python3 - "$REPORT_XML_PATH" "$SUMMARY_OUT" "$ARTIFACT_NAME" "$HTML_INDEX" <<'PY' -import json -import sys -import os -from pathlib import Path -from xml.etree import ElementTree as ET - -xml_path = Path(sys.argv[1]) -summary_path = Path(sys.argv[2]) -artifact_name = sys.argv[3] -html_index = sys.argv[4] - -data = { - "artifact": artifact_name, - "html_index": html_index, - "counters": {}, - "top_classes": [], -} - -if not xml_path.is_file(): - json.dump(data, summary_path.open("w", encoding="utf-8"), indent=2) - sys.exit(0) - -def safe_int(value, default=0): - try: - return int(value) - except Exception: - return default - - -def format_class_name(package, class_name): - pkg = package.replace("/", ".").strip(".") - cls = class_name.replace("/", ".") - if pkg: - return f"{pkg}.{cls}" - return cls - - -def parse_class_coverage(root): - classes = [] - for package in root.findall("package"): - pkg_name = package.get("name", "") - for cls in package.findall("class"): - class_name = cls.get("name", "") - line_counter = None - for counter in cls.findall("counter"): - if counter.get("type") == "LINE": - line_counter = counter - break - if line_counter is None: - continue - missed = safe_int(line_counter.get("missed", 0)) - covered = safe_int(line_counter.get("covered", 0)) - total = missed + covered - if total <= 0: - continue - pct = covered / total * 100.0 - classes.append( - { - "name": format_class_name(pkg_name, class_name), - "missed": missed, - "covered": covered, - "total": total, - "coverage": pct, - } - ) - classes.sort(key=lambda c: (c["coverage"], -c["total"])) - return classes[:10] - - -try: - tree = ET.parse(xml_path) -except ET.ParseError: - json.dump(data, summary_path.open("w", encoding="utf-8"), indent=2) - sys.exit(0) - -root = tree.getroot() -for counter in root.findall("counter"): - ctype = counter.get("type") - missed = safe_int(counter.get("missed", 0)) - covered = safe_int(counter.get("covered", 0)) - total = missed + covered - pct = (covered / total * 100.0) if total else 0.0 - data["counters"][ctype] = { - "missed": missed, - "covered": covered, - "total": total, - "coverage": pct, - } - -data["top_classes"] = parse_class_coverage(root) - -html_url_env = os.environ.get("ANDROID_COVERAGE_HTML_URL") or os.environ.get("COVERAGE_HTML_URL") -if html_url_env: - data["html_url"] = html_url_env - -json.dump(data, summary_path.open("w", encoding="utf-8"), indent=2) -PY - -cov_log "Copied Jacoco coverage report to $REPORT_DEST_DIR" -if [ -f "$SUMMARY_OUT" ]; then - cov_log "Wrote coverage summary to $SUMMARY_OUT" -fi +echo "[generate-android-coverage] Android coverage is disabled; skipping." +exit 0 \ No newline at end of file diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl deleted file mode 100644 index 75a90b9d10..0000000000 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ /dev/null @@ -1,159 +0,0 @@ -import XCTest -import UIKit - -final class HelloCodenameOneUITests: XCTestCase { - private var app: XCUIApplication! - private var outputDirectory: URL! - private let chunkSize = 2000 - private let previewChannel = "PREVIEW" - private let previewQualities: [CGFloat] = [0.60, 0.50, 0.40, 0.35, 0.30, 0.25, 0.20, 0.18, 0.16, 0.14, 0.12, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01] - private let maxPreviewBytes = 20 * 1024 - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - - // Locale for determinism - app.launchArguments += ["-AppleLocale", "en_US", "-AppleLanguages", "(en)"] - // Tip: force light mode or content size if you need pixel-stable shots - // app.launchArguments += ["-uiuserInterfaceStyle", "Light"] - - // IMPORTANT: write to the app's sandbox, not a host path - let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - if let tag = ProcessInfo.processInfo.environment["CN1SS_OUTPUT_DIR"], !tag.isEmpty { - outputDirectory = tmp.appendingPathComponent(tag, isDirectory: true) - } else { - outputDirectory = tmp.appendingPathComponent("cn1screens", isDirectory: true) - } - try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - - app.launch() - waitForStableFrame() - } - - override func tearDownWithError() throws { - app?.terminate() - app = nil - } - - private func captureScreenshot(named name: String) throws { - let shot = XCUIScreen.main.screenshot() - - // Save into sandbox tmp (optional – mainly for local debugging) - let pngURL = outputDirectory.appendingPathComponent("\(name).png") - do { try shot.pngRepresentation.write(to: pngURL) } catch { /* ignore */ } - - // ALWAYS attach so we can export from the .xcresult - let att = XCTAttachment(screenshot: shot) - att.name = name - att.lifetime = .keepAlways - add(att) - - emitScreenshotPayloads(for: shot, name: name) - } - - /// Wait for foreground + a short settle time - private func waitForStableFrame(timeout: TimeInterval = 30, settle: TimeInterval = 1.2) { - _ = app.wait(for: .runningForeground, timeout: timeout) - RunLoop.current.run(until: Date(timeIntervalSinceNow: settle)) - } - - /// Tap using normalized coordinates (0...1) - private func tapNormalized(_ dx: CGFloat, _ dy: CGFloat) { - let origin = app.coordinate(withNormalizedOffset: .zero) - let target = origin.withOffset(.init(dx: app.frame.size.width * dx, - dy: app.frame.size.height * dy)) - target.tap() - } - - func testMainScreenScreenshot() throws { - waitForStableFrame() - try captureScreenshot(named: "MainActivity") - } - - func testBrowserComponentScreenshot() throws { - waitForStableFrame() - tapNormalized(0.5, 0.70) - // tiny retry to allow BrowserComponent to render - RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0)) - try captureScreenshot(named: "BrowserComponent") - } - - private func sanitizeTestName(_ name: String) -> String { - let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-") - let underscore: UnicodeScalar = "_" - var scalars: [UnicodeScalar] = [] - scalars.reserveCapacity(name.unicodeScalars.count) - for scalar in name.unicodeScalars { - scalars.append(allowed.contains(scalar) ? scalar : underscore) - } - return String(String.UnicodeScalarView(scalars)) - } - - private func emitScreenshotPayloads(for shot: XCUIScreenshot, name: String) { - let safeName = sanitizeTestName(name) - let pngData = shot.pngRepresentation - print("CN1SS:INFO:test=\(safeName) png_bytes=\(pngData.count)") - emitScreenshotChannel(data: pngData, name: safeName, channel: "") - - if let preview = makePreviewJPEG(from: shot, pngData: pngData) { - print("CN1SS:INFO:test=\(safeName) preview_jpeg_bytes=\(preview.data.count) preview_quality=\(preview.quality)") - if preview.data.count > maxPreviewBytes { - print("CN1SS:WARN:test=\(safeName) preview_exceeds_limit_bytes=\(preview.data.count) max_preview_bytes=\(maxPreviewBytes)") - } - emitScreenshotChannel(data: preview.data, name: safeName, channel: previewChannel) - } else { - print("CN1SS:INFO:test=\(safeName) preview_jpeg_bytes=0 preview_quality=0") - } - } - - private func makePreviewJPEG(from shot: XCUIScreenshot, pngData: Data) -> (data: Data, quality: Int)? { - guard let image = UIImage(data: pngData) else { - return nil - } - var chosenData: Data? - var chosenQuality = 0 - var smallest = Int.max - for quality in previewQualities { - guard let jpeg = image.jpegData(compressionQuality: quality) else { continue } - let length = jpeg.count - if length < smallest { - smallest = length - chosenData = jpeg - chosenQuality = Int((quality * 100).rounded()) - } - if length <= maxPreviewBytes { - break - } - } - guard let finalData = chosenData, !finalData.isEmpty else { - return nil - } - return (finalData, chosenQuality) - } - - private func emitScreenshotChannel(data: Data, name: String, channel: String) { - var prefix = "CN1SS" - if !channel.isEmpty { - prefix += channel - } - guard !data.isEmpty else { - print("\(prefix):END:\(name)") - return - } - let base64 = data.base64EncodedString() - var current = base64.startIndex - var position = 0 - var chunkCount = 0 - while current < base64.endIndex { - let next = base64.index(current, offsetBy: chunkSize, limitedBy: base64.endIndex) ?? base64.endIndex - let chunk = base64[current../dev/null || echo "${TMPDIR}/cn1ss-tmp")" ensure_dir "$SCREENSHOT_TMP_DIR" SCREENSHOT_PREVIEW_DIR="$SCREENSHOT_TMP_DIR/previews" -COVERAGE_SUMMARY="$ARTIFACTS_DIR/android-coverage-report/coverage-summary.json" ra_log "Loading workspace environment from $ENV_FILE" [ -f "$ENV_FILE" ] || { ra_log "Missing env file: $ENV_FILE"; exit 3; } @@ -114,17 +113,30 @@ sleep 2 GRADLEW="$GRADLE_PROJECT_DIR/gradlew" [ -x "$GRADLEW" ] || chmod +x "$GRADLEW" -GRADLE_CMD=("$GRADLEW" --no-daemon connectedDebugAndroidTest) -ra_log "Executing connectedDebugAndroidTest via Gradle" -if ! ( +# 1) Make sure the app is installed on the emulator +ra_log "Installing debug APK via Gradle installDebug" +( cd "$GRADLE_PROJECT_DIR" - JAVA_HOME="$JAVA17_HOME" "${GRADLE_CMD[@]}" -); then - ra_log "FATAL: connectedDebugAndroidTest failed" - exit 10 + JAVA_HOME="$JAVA17_HOME" "$GRADLEW" --no-daemon installDebug +) + +# 2) Launch the app +ra_log "Launching app main activity on device" + +# Easiest way that doesn’t care about the exact Activity name: +if ! "$ADB_BIN" shell monkey -p "$PACKAGE_NAME" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; then + ra_log "monkey launch failed; falling back to am start" + # If you know the main activity, you can be more explicit here: + "$ADB_BIN" shell am start -a android.intent.action.MAIN \ + -c android.intent.category.LAUNCHER "$PACKAGE_NAME" >/dev/null 2>&1 || true fi +# 3) Give the app a little head-start before we start enforcing timeouts +STARTUP_SLEEP=10 +ra_log "Sleeping ${STARTUP_SLEEP}s to let app start" +sleep "$STARTUP_SLEEP" + END_MARKER="CN1SS:SUITE:FINISHED" TIMEOUT_SECONDS=60 START_TIME="$(date +%s)" @@ -144,17 +156,6 @@ done sleep 3 -ra_log "STAGE:COVERAGE -> Collecting Jacoco coverage report" -if ARTIFACTS_DIR="$ARTIFACTS_DIR" "$SCRIPT_DIR/generate-android-coverage-report.sh" "$GRADLE_PROJECT_DIR"; then - if [ -f "$COVERAGE_SUMMARY" ]; then - ra_log " -> Coverage summary detected at $COVERAGE_SUMMARY" - else - ra_log " -> Coverage summary not found after report generation" - fi -else - ra_log "WARNING: Coverage report generation failed; continuing without coverage details" -fi - declare -a CN1SS_SOURCES=("LOGCAT:$TEST_LOG") @@ -247,7 +248,6 @@ render_args=( --compare-json "$COMPARE_JSON" --comment-out "$COMMENT_FILE" --summary-out "$SUMMARY_FILE" - --coverage-summary "$COVERAGE_SUMMARY" ) if [ -n "${ANDROID_COVERAGE_HTML_URL:-}" ]; then render_args+=(--coverage-html-url "${ANDROID_COVERAGE_HTML_URL}")