Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/scripts-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ name: Test Android build scripts
- 'scripts/build-android-port.sh'
- 'scripts/build-android-app.sh'
- 'scripts/run-android-instrumentation-tests.sh'
- 'scripts/generate-android-coverage-report.sh'
- 'scripts/android/lib/**/*.java'
- 'scripts/android/tests/**/*.java'
- 'scripts/device-runner-app/**/*.java'
- 'scripts/android/screenshots/**'
- '!scripts/android/screenshots/**/*.md'
- 'scripts/templates/**'
Expand All @@ -33,8 +35,10 @@ name: Test Android build scripts
- 'scripts/build-android-port.sh'
- 'scripts/build-android-app.sh'
- 'scripts/run-android-instrumentation-tests.sh'
- 'scripts/generate-android-coverage-report.sh'
- 'scripts/android/lib/**/*.java'
- 'scripts/android/tests/**/*.java'
- 'scripts/device-runner-app/**/*.java'
- 'scripts/android/screenshots/**'
- '!scripts/android/screenshots/**/*.md'
- 'scripts/templates/**'
Expand Down Expand Up @@ -96,6 +100,7 @@ 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
Expand All @@ -105,3 +110,12 @@ 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
86 changes: 86 additions & 0 deletions scripts/android/lib/PatchGradleFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ 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);
}
Expand Down Expand Up @@ -247,6 +251,88 @@ 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";
}
Expand Down
16 changes: 16 additions & 0 deletions scripts/build-android-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,22 @@ 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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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;
}
}
63 changes: 63 additions & 0 deletions scripts/generate-android-coverage-report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Generate a Jacoco coverage report for the Android sample app
set -euo pipefail

cov_log() { echo "[generate-android-coverage] $1"; }

if [ $# -lt 1 ]; then
cov_log "Usage: $0 <gradle_project_dir>" >&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:-<unset>} JAVA17_HOME=${JAVA17_HOME:-<unset>}"
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}"/

cov_log "Copied Jacoco coverage report to $REPORT_DEST_DIR"
50 changes: 12 additions & 38 deletions scripts/run-android-instrumentation-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,6 @@ cn1ss_setup "$JAVA17_BIN" "$CN1SS_HELPER_SOURCE_DIR"
[ -x "$GRADLE_PROJECT_DIR/gradlew" ] || chmod +x "$GRADLE_PROJECT_DIR/gradlew"

# ---- Prepare app + emulator state -----------------------------------------

APK_PATH="${2:-}"
if [ -z "$APK_PATH" ]; then
APK_PATH="$(find "$GRADLE_PROJECT_DIR" -type f -path '*/outputs/apk/debug/*.apk' | head -n 1 || true)"
fi
if [ -z "$APK_PATH" ] || [ ! -f "$APK_PATH" ]; then
ra_log "FATAL: Unable to locate debug APK under $GRADLE_PROJECT_DIR" >&2
exit 10
fi
ra_log "Using APK: $APK_PATH"

MANIFEST="$GRADLE_PROJECT_DIR/app/src/main/AndroidManifest.xml"
if [ ! -f "$MANIFEST" ]; then
ra_log "FATAL: AndroidManifest.xml not found at $MANIFEST" >&2
Expand All @@ -104,18 +93,6 @@ ADB_BIN="$(command -v adb)"
ra_log "ADB connected devices:"
"$ADB_BIN" devices -l | sed 's/^/[run-android-instrumentation-tests] /'

ra_log "Installing APK onto device"
"$ADB_BIN" shell am force-stop "$PACKAGE_NAME" >/dev/null 2>&1 || true
"$ADB_BIN" uninstall "$PACKAGE_NAME" >/dev/null 2>&1 || true
if ! "$ADB_BIN" install -r "$APK_PATH"; then
ra_log "adb install failed; retrying after explicit uninstall"
"$ADB_BIN" uninstall "$PACKAGE_NAME" >/dev/null 2>&1 || true
if ! "$ADB_BIN" install "$APK_PATH"; then
ra_log "FATAL: adb install failed after retry"
exit 10
fi
fi

ra_log "Clearing logcat buffer"
"$ADB_BIN" logcat -c || true

Expand All @@ -134,24 +111,21 @@ ra_log "Capturing device logcat to $TEST_LOG"
LOGCAT_PID=$!
sleep 2

ra_log "Launching Codename One DeviceRunner"
"$ADB_BIN" shell pm clear "$PACKAGE_NAME" >/dev/null 2>&1 || true
if ! "$ADB_BIN" shell monkey -p "$PACKAGE_NAME" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; then
ra_log "monkey launch failed; attempting am start fallback"
MAIN_ACTIVITY="$("$ADB_BIN" shell cmd package resolve-activity --brief "$PACKAGE_NAME" 2>/dev/null | head -n 1 | tr -d '\r' | sed 's/ .*//')"
if [[ "$MAIN_ACTIVITY" == */* ]]; then
if ! "$ADB_BIN" shell am start -n "$MAIN_ACTIVITY" >/dev/null 2>&1; then
ra_log "FATAL: Failed to start application via am start"
exit 10
fi
else
ra_log "FATAL: Unable to determine launchable activity"
exit 10
fi
GRADLEW="$GRADLE_PROJECT_DIR/gradlew"
[ -x "$GRADLEW" ] || chmod +x "$GRADLEW"
GRADLE_CMD=("$GRADLEW" --no-daemon connectedDebugAndroidTest)

ra_log "Executing connectedDebugAndroidTest via Gradle"
if ! (
cd "$GRADLE_PROJECT_DIR"
JAVA_HOME="$JAVA17_HOME" "${GRADLE_CMD[@]}"
); then
ra_log "FATAL: connectedDebugAndroidTest failed"
exit 10
fi

END_MARKER="CN1SS:SUITE:FINISHED"
TIMEOUT_SECONDS=300
TIMEOUT_SECONDS=60
START_TIME="$(date +%s)"
ra_log "Waiting for DeviceRunner completion marker ($END_MARKER)"
while true; do
Expand Down
Loading