diff --git a/scripts/android/lib/patch_gradle_files.py b/scripts/android/lib/patch_gradle_files.py new file mode 100755 index 0000000000..e6168a4854 --- /dev/null +++ b/scripts/android/lib/patch_gradle_files.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Utilities to normalize generated Gradle build files for Android tests.""" +from __future__ import annotations + +import argparse +import pathlib +import re +from typing import Tuple + +REPOSITORIES_BLOCK = """\ +repositories { + google() + mavenCentral() +} +""" + + +def _ensure_repositories(content: str) -> Tuple[str, bool]: + """Ensure a repositories block exists with google() and mavenCentral().""" + pattern = re.compile(r"(?ms)^\s*repositories\s*\{.*?\}") + match = pattern.search(content) + block_added = False + + if not match: + # Append a canonical repositories block to the end of the file. + if not content.endswith("\n"): + content += "\n" + content += REPOSITORIES_BLOCK + return content, True + + block = match.group(0) + if "google()" not in block or "mavenCentral()" not in block: + lines = block.splitlines() + header = lines[0] + body = [line for line in lines[1:-1] if line.strip()] + if "google()" not in block: + body.append(" google()") + if "mavenCentral()" not in block: + body.append(" mavenCentral()") + new_block = "\n".join([header, *sorted(set(body)), lines[-1]]) + content = content[: match.start()] + new_block + content[match.end() :] + block_added = True + return content, block_added + + +def _ensure_android_sdk(content: str, compile_sdk: int, target_sdk: int) -> Tuple[str, bool]: + changed = False + + def insert_or_replace(pattern: str, repl: str, search_scope: str) -> Tuple[str, bool]: + nonlocal content + match = re.search(pattern, content, re.MULTILINE) + if match: + new_content = re.sub(pattern, repl, content, count=1, flags=re.MULTILINE) + if new_content != content: + content = new_content + return content, True + return content, False + anchor = re.search(search_scope, content, re.MULTILINE) + if not anchor: + return content, False + start = anchor.end() + content = content[:start] + f"\n{repl}" + content[start:] + return content, True + + # Ensure android block exists + if re.search(r"(?m)^\s*android\s*\{", content) is None: + if not content.endswith("\n"): + content += "\n" + content += ( + "\nandroid {\n" + f" compileSdkVersion {compile_sdk}\n" + " defaultConfig {\n" + f" targetSdkVersion {target_sdk}\n" + " }\n}" + ) + return content, True + + new_content, changed_once = insert_or_replace( + pattern=rf"(?m)^\s*compileSdkVersion\s+\d+", + repl=f" compileSdkVersion {compile_sdk}", + search_scope=r"(?m)^\s*android\s*\{", + ) + changed = changed or changed_once + + default_config = re.search(r"(?ms)^\s*defaultConfig\s*\{.*?^\s*\}", content) + if default_config: + block = default_config.group(0) + if re.search(r"(?m)^\s*targetSdkVersion\s+\d+", block): + updated = re.sub( + r"(?m)^\s*targetSdkVersion\s+\d+", + f" targetSdkVersion {target_sdk}", + block, + ) + else: + updated = re.sub(r"{", "{\n targetSdkVersion %d" % target_sdk, block, count=1) + if updated != block: + content = content[: default_config.start()] + updated + content[default_config.end() :] + changed = True + else: + content, changed_once = insert_or_replace( + pattern=r"(?ms)(^\s*android\s*\{)", + repl=" defaultConfig {\n targetSdkVersion %d\n }" % target_sdk, + search_scope=r"(?m)^\s*android\s*\{", + ) + changed = changed or changed_once + + return content, changed + + +def _ensure_instrumentation_runner(content: str) -> Tuple[str, bool]: + runner = "androidx.test.runner.AndroidJUnitRunner" + if runner in content: + return content, False + changed = False + pattern = re.compile(r"(?m)^\s*testInstrumentationRunner\s*\".*?\"\s*$") + if pattern.search(content): + new_content = pattern.sub(f" testInstrumentationRunner \"{runner}\"", content) + return new_content, True + default_config = re.search(r"(?ms)^\s*defaultConfig\s*\{", content) + if default_config: + insert_point = default_config.end() + content = ( + content[:insert_point] + + f"\n testInstrumentationRunner \"{runner}\"" + + content[insert_point:] + ) + changed = True + else: + android_block = re.search(r"(?ms)^\s*android\s*\{", content) + if android_block: + insert_point = android_block.end() + snippet = ( + "\n defaultConfig {\n" + f" testInstrumentationRunner \"{runner}\"\n" + " }" + ) + content = content[:insert_point] + snippet + content[insert_point:] + changed = True + return content, changed + + +def _remove_legacy_use_library(content: str) -> Tuple[str, bool]: + new_content, count = re.subn( + r"(?m)^\s*useLibrary\s+'android\.test\.(?:base|mock|runner)'\s*$", + "", + content, + ) + return new_content, bool(count) + + +def _ensure_test_dependencies(content: str) -> Tuple[str, bool]: + module_view = re.sub(r"(?ms)^\s*(buildscript|pluginManagement)\s*\{.*?^\s*\}", "", content) + uses_modern = re.search( + r"(?m)^\s*(implementation|api|testImplementation|androidTestImplementation)\b", + module_view, + ) + configuration = "androidTestImplementation" if uses_modern else "androidTestCompile" + dependencies = [ + "androidx.test.ext:junit:1.1.5", + "androidx.test:runner:1.5.2", + "androidx.test:core:1.5.0", + "androidx.test.services:storage:1.4.2", + ] + missing = [dep for dep in dependencies if dep not in module_view] + if not missing: + return content, False + block = "\n\ndependencies {\n" + "".join( + f" {configuration} \"{dep}\"\n" for dep in missing + ) + "}\n" + if not content.endswith("\n"): + content += "\n" + return content + block, True + + +def patch_app_build_gradle(path: pathlib.Path, compile_sdk: int, target_sdk: int) -> bool: + text = path.read_text(encoding="utf-8") + changed = False + + for transform in ( + lambda c: _ensure_android_sdk(c, compile_sdk, target_sdk), + _ensure_instrumentation_runner, + _remove_legacy_use_library, + _ensure_test_dependencies, + ): + text, modified = transform(text) + changed = changed or modified + + if changed: + path.write_text(text if text.endswith("\n") else text + "\n", encoding="utf-8") + return changed + + +def patch_root_build_gradle(path: pathlib.Path) -> bool: + text = path.read_text(encoding="utf-8") + text, changed = _ensure_repositories(text) + if changed: + path.write_text(text if text.endswith("\n") else text + "\n", encoding="utf-8") + return changed + + +def main() -> int: + parser = argparse.ArgumentParser(description="Normalize Gradle build files") + parser.add_argument("--root", required=True, type=pathlib.Path, help="Path to root build.gradle") + parser.add_argument("--app", required=True, type=pathlib.Path, help="Path to app/build.gradle") + parser.add_argument("--compile-sdk", type=int, default=33) + parser.add_argument("--target-sdk", type=int, default=33) + args = parser.parse_args() + + modified_root = patch_root_build_gradle(args.root) + modified_app = patch_app_build_gradle(args.app, args.compile_sdk, args.target_sdk) + + if modified_root: + print(f"Patched {args.root}") + if modified_app: + print(f"Patched {args.app}") + if not (modified_root or modified_app): + print("Gradle files already normalized") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/android/tests/HelloCodenameOneInstrumentedTest.java b/scripts/android/tests/HelloCodenameOneInstrumentedTest.java new file mode 100644 index 0000000000..92de85de8c --- /dev/null +++ b/scripts/android/tests/HelloCodenameOneInstrumentedTest.java @@ -0,0 +1,112 @@ +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 org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; + +@RunWith(AndroidJUnit4.class) +public class HelloCodenameOneInstrumentedTest { + + private static void println(String s) { + System.out.println(s); + } + + @Test + public void testUseAppContext_andEmitScreenshot() throws Exception { + Context ctx = ApplicationProvider.getApplicationContext(); + String pkg = "@PACKAGE@"; + Assert.assertEquals("Package mismatch", pkg, ctx.getPackageName()); + + 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: about to launch Activity"); + byte[] pngBytes = null; + + try (ActivityScenario scenario = ActivityScenario.launch(launch)) { + Thread.sleep(750); + + println("CN1SS:INFO: activity launched"); + + final byte[][] holder = new byte[1][]; + scenario.onActivity(activity -> { + try { + View root = activity.getWindow().getDecorView().getRootView(); + int w = root.getWidth(); + int h = root.getHeight(); + if (w <= 0 || h <= 0) { + DisplayMetrics dm = activity.getResources().getDisplayMetrics(); + w = Math.max(1, dm.widthPixels); + h = Math.max(1, dm.heightPixels); + int sw = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY); + int sh = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY); + root.measure(sw, sh); + root.layout(0, 0, w, h); + println("CN1SS:INFO: forced layout to " + w + "x" + h); + } else { + println("CN1SS:INFO: natural layout " + w + "x" + h); + } + + Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bmp); + root.draw(c); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.max(1024, w * h / 2)); + boolean ok = bmp.compress(Bitmap.CompressFormat.PNG, 100, baos); + if (!ok) { + throw new RuntimeException("Bitmap.compress returned false"); + } + holder[0] = baos.toByteArray(); + println("CN1SS:INFO: png_bytes=" + holder[0].length); + } catch (Throwable t) { + println("CN1SS:ERR: onActivity " + t); + t.printStackTrace(System.out); + } + }); + + pngBytes = holder[0]; + } catch (Throwable t) { + println("CN1SS:ERR: launch " + t); + t.printStackTrace(System.out); + } + + if (pngBytes == null || pngBytes.length == 0) { + println("CN1SS:END"); + Assert.fail("Screenshot capture produced 0 bytes"); + return; + } + + String b64 = Base64.encodeToString(pngBytes, Base64.NO_WRAP); + final int chunkSize = 2000; + int count = 0; + for (int pos = 0; pos < b64.length(); pos += chunkSize) { + int end = Math.min(pos + chunkSize, b64.length()); + System.out.println("CN1SS:" + String.format("%06d", pos) + ":" + b64.substring(pos, end)); + count++; + } + println("CN1SS:INFO: chunks=" + count + " total_b64_len=" + b64.length()); + System.out.println("CN1SS:END"); + System.out.flush(); + } +} diff --git a/scripts/android/tests/cn1ss_chunk_tools.py b/scripts/android/tests/cn1ss_chunk_tools.py new file mode 100755 index 0000000000..026bae0192 --- /dev/null +++ b/scripts/android/tests/cn1ss_chunk_tools.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Helpers for extracting CN1SS chunked screenshot payloads.""" +from __future__ import annotations + +import argparse +import base64 +import pathlib +import re +import sys +from typing import Iterable, List, Tuple + +CHUNK_PATTERN = re.compile(r"CN1SS:(\d{6}):(.*)") + + +def _iter_chunk_lines(path: pathlib.Path) -> Iterable[Tuple[int, str]]: + text = path.read_text(encoding="utf-8", errors="ignore") + for line in text.splitlines(): + match = CHUNK_PATTERN.search(line) + if not match: + continue + index = int(match.group(1)) + payload = re.sub(r"[^A-Za-z0-9+/=]", "", match.group(2)) + if payload: + yield index, payload + + +def count_chunks(path: pathlib.Path) -> int: + return sum(1 for _ in _iter_chunk_lines(path)) + + +def concatenate_chunks(path: pathlib.Path) -> str: + ordered = sorted(_iter_chunk_lines(path), key=lambda item: item[0]) + return "".join(payload for _, payload in ordered) + + +def decode_chunks(path: pathlib.Path) -> bytes: + data = concatenate_chunks(path) + if not data: + return b"" + try: + return base64.b64decode(data) + except Exception: + return b"" + + +def main(argv: List[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + p_count = subparsers.add_parser("count", help="Count CN1SS chunks in a file") + p_count.add_argument("path", type=pathlib.Path) + + p_extract = subparsers.add_parser("extract", help="Concatenate CN1SS payload chunks") + p_extract.add_argument("path", type=pathlib.Path) + p_extract.add_argument("--decode", action="store_true", help="Decode payload to binary PNG") + + args = parser.parse_args(argv) + + if args.command == "count": + print(count_chunks(args.path)) + return 0 + + if args.command == "extract": + if args.decode: + sys.stdout.buffer.write(decode_chunks(args.path)) + else: + sys.stdout.write(concatenate_chunks(args.path)) + return 0 + + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 79e1374f96..2d90cdc52c 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -276,99 +276,18 @@ grep -q '^android.enableJetifier=' "$GRADLE_PROPS" 2>/dev/null || echo 'android. APP_BUILD_GRADLE="$GRADLE_PROJECT_DIR/app/build.gradle" ROOT_BUILD_GRADLE="$GRADLE_PROJECT_DIR/build.gradle" +PATCH_GRADLE_SCRIPT="$SCRIPT_DIR/android/lib/patch_gradle_files.py" -# Ensure repos in both root and app -for F in "$ROOT_BUILD_GRADLE" "$APP_BUILD_GRADLE"; do - if [ -f "$F" ]; then - if ! grep -qE '^\s*repositories\s*{' "$F"; then - cat >> "$F" <<'EOS' +if [ ! -x "$PATCH_GRADLE_SCRIPT" ]; then + ba_log "Missing gradle patch helper: $PATCH_GRADLE_SCRIPT" >&2 + exit 1 +fi -repositories { - google() - mavenCentral() -} -EOS - else - grep -q 'google()' "$F" || sed -E -i '0,/repositories[[:space:]]*\{/s//repositories {\n google()\n mavenCentral()/' "$F" - grep -q 'mavenCentral()' "$F" || sed -E -i '0,/repositories[[:space:]]*\{/s//repositories {\n google()\n mavenCentral()/' "$F" - fi - fi -done - -# Edit app/build.gradle -python3 - "$APP_BUILD_GRADLE" <<'PY' -import sys, re, pathlib -p = pathlib.Path(sys.argv[1]); txt = p.read_text(); orig = txt; changed = False - -def strip_block(name, s): - return re.sub(rf'(?ms)^\s*{name}\s*\{{.*?\}}\s*', '', s) - -module_view = strip_block('buildscript', strip_block('pluginManagement', txt)) - -# 1) android { compileSdkVersion/targetSdkVersion } -def ensure_sdk(body): - # If android { ... } exists, update/insert inside defaultConfig and the android block - if re.search(r'(?m)^\s*android\s*\{', body): - # compileSdkVersion - if re.search(r'(?m)^\s*compileSdkVersion\s+\d+', body) is None: - body = re.sub(r'(?m)(^\s*android\s*\{)', r'\1\n compileSdkVersion 33', body, count=1) - else: - body = re.sub(r'(?m)^\s*compileSdkVersion\s+\d+', ' compileSdkVersion 33', body) - # targetSdkVersion - if re.search(r'(?ms)^\s*defaultConfig\s*\{.*?^\s*\}', body): - dc = re.search(r'(?ms)^\s*defaultConfig\s*\{.*?^\s*\}', body) - block = dc.group(0) - if re.search(r'(?m)^\s*targetSdkVersion\s+\d+', block): - block2 = re.sub(r'(?m)^\s*targetSdkVersion\s+\d+', ' targetSdkVersion 33', block) - else: - block2 = re.sub(r'(\{\s*)', r'\1\n targetSdkVersion 33', block, count=1) - body = body[:dc.start()] + block2 + body[dc.end():] - else: - body = re.sub(r'(?m)(^\s*android\s*\{)', r'\1\n defaultConfig {\n targetSdkVersion 33\n }', body, count=1) - else: - # No android block at all: add minimal - body += '\n\nandroid {\n compileSdkVersion 33\n defaultConfig { targetSdkVersion 33 }\n}\n' - return body - -txt2 = ensure_sdk(txt) -if txt2 != txt: txt = txt2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True - -# 2) testInstrumentationRunner -> AndroidX -if "androidx.test.runner.AndroidJUnitRunner" not in module_view: - t2, n = re.subn(r'(?m)^\s*testInstrumentationRunner\s*".*?"\s*$', ' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"', txt) - if n == 0: - t2, n = re.subn(r'(?m)(^\s*defaultConfig\s*\{)', r'\1\n testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"', txt, count=1) - if n == 0: - t2, n = re.subn(r'(?ms)(^\s*android\s*\{)', r'\1\n defaultConfig {\n testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"\n }', txt, count=1) - if n: txt = t2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True - -# 3) remove legacy useLibrary lines -t2, n = re.subn(r'(?m)^\s*useLibrary\s+\'android\.test\.(base|mock|runner)\'\s*$', '', txt) -if n: txt = t2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True - -# 4) deps: choose androidTestImplementation vs androidTestCompile -uses_modern = re.search(r'(?m)^\s*(implementation|api|testImplementation|androidTestImplementation)\b', module_view) is not None -conf = "androidTestImplementation" if uses_modern else "androidTestCompile" -need = [ - ("androidx.test.ext:junit:1.1.5", conf), # AndroidJUnit4 - ("androidx.test:runner:1.5.2", conf), - ("androidx.test:core:1.5.0", conf), - ("androidx.test.services:storage:1.4.2", conf), -] -to_add = [(c, k) for (c, k) in need if c not in module_view] - -if to_add: - block = "\n\ndependencies {\n" + "".join([f" {k} \"{c}\"\n" for c, k in to_add]) + "}\n" - txt = txt.rstrip() + block - changed = True - -if changed and txt != orig: - if not txt.endswith("\n"): txt += "\n" - p.write_text(txt) - print(f"Patched app/build.gradle (SDK=33; deps via {conf})") -else: - print("No changes needed in app/build.gradle") -PY +python3 "$PATCH_GRADLE_SCRIPT" \ + --root "$ROOT_BUILD_GRADLE" \ + --app "$APP_BUILD_GRADLE" \ + --compile-sdk 33 \ + --target-sdk 33 # --- END: robust Gradle patch --- echo "----- app/build.gradle tail -----" @@ -378,121 +297,14 @@ echo "---------------------------------" TEST_SRC_DIR="$GRADLE_PROJECT_DIR/app/src/androidTest/java/${PACKAGE_PATH}" mkdir -p "$TEST_SRC_DIR" TEST_CLASS="$TEST_SRC_DIR/HelloCodenameOneInstrumentedTest.java" -cat >"$TEST_CLASS" <<'EOF' -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 org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.ByteArrayOutputStream; - -@RunWith(AndroidJUnit4.class) -public class HelloCodenameOneInstrumentedTest { - - private static void println(String s) { System.out.println(s); } - - @Test - public void testUseAppContext_andEmitScreenshot() throws Exception { - Context ctx = ApplicationProvider.getApplicationContext(); - String pkg = "@PACKAGE@"; - Assert.assertEquals("Package mismatch", pkg, ctx.getPackageName()); - - // Resolve real launcher intent (don’t hard-code activity) - Intent launch = ctx.getPackageManager().getLaunchIntentForPackage(pkg); - if (launch == null) { - // Fallback MAIN/LAUNCHER inside this package - 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: about to launch Activity"); - byte[] pngBytes = null; - - try (ActivityScenario scenario = ActivityScenario.launch(launch)) { - // give the activity a tiny moment to layout - Thread.sleep(750); - - println("CN1SS:INFO: activity launched"); - - final byte[][] holder = new byte[1][]; - scenario.onActivity(activity -> { - try { - View root = activity.getWindow().getDecorView().getRootView(); - int w = root.getWidth(); - int h = root.getHeight(); - if (w <= 0 || h <= 0) { - DisplayMetrics dm = activity.getResources().getDisplayMetrics(); - w = Math.max(1, dm.widthPixels); - h = Math.max(1, dm.heightPixels); - int sw = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY); - int sh = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY); - root.measure(sw, sh); - root.layout(0, 0, w, h); - println("CN1SS:INFO: forced layout to " + w + "x" + h); - } else { - println("CN1SS:INFO: natural layout " + w + "x" + h); - } - - Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(bmp); - root.draw(c); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.max(1024, w * h / 2)); - boolean ok = bmp.compress(Bitmap.CompressFormat.PNG, 100, baos); - if (!ok) throw new RuntimeException("Bitmap.compress returned false"); - holder[0] = baos.toByteArray(); - println("CN1SS:INFO: png_bytes=" + holder[0].length); - } catch (Throwable t) { - println("CN1SS:ERR: onActivity " + t); - t.printStackTrace(System.out); - } - }); - - pngBytes = holder[0]; - } catch (Throwable t) { - println("CN1SS:ERR: launch " + t); - t.printStackTrace(System.out); - } - - if (pngBytes == null || pngBytes.length == 0) { - println("CN1SS:END"); // terminator for the runner parser - Assert.fail("Screenshot capture produced 0 bytes"); - return; - } - - // Chunk & emit (safe for Gradle/logcat capture) - String b64 = Base64.encodeToString(pngBytes, Base64.NO_WRAP); - final int CHUNK = 2000; - int count = 0; - for (int pos = 0; pos < b64.length(); pos += CHUNK) { - int end = Math.min(pos + CHUNK, b64.length()); - System.out.println("CN1SS:" + String.format("%06d", pos) + ":" + b64.substring(pos, end)); - count++; - } - println("CN1SS:INFO: chunks=" + count + " total_b64_len=" + b64.length()); - System.out.println("CN1SS:END"); - System.out.flush(); - } -} -EOF -sed -i "s|@PACKAGE@|$PACKAGE_NAME|g" "$TEST_CLASS" +TEST_TEMPLATE="$SCRIPT_DIR/android/tests/HelloCodenameOneInstrumentedTest.java" + +if [ ! -f "$TEST_TEMPLATE" ]; then + ba_log "Missing instrumentation test template: $TEST_TEMPLATE" >&2 + exit 1 +fi + +sed "s|@PACKAGE@|$PACKAGE_NAME|g" "$TEST_TEMPLATE" > "$TEST_CLASS" ba_log "Created instrumentation test at $TEST_CLASS" DEFAULT_ANDROID_TEST="$GRADLE_PROJECT_DIR/app/src/androidTest/java/com/example/myapplication2/ExampleInstrumentedTest.java" diff --git a/scripts/run-android-instrumentation-tests.sh b/scripts/run-android-instrumentation-tests.sh index 72de2769d9..1bac3c92cc 100755 --- a/scripts/run-android-instrumentation-tests.sh +++ b/scripts/run-android-instrumentation-tests.sh @@ -8,44 +8,42 @@ ra_log() { echo "[run-android-instrumentation-tests] $1"; } ensure_dir() { mkdir -p "$1" 2>/dev/null || true; } -# Count CN1SS chunk lines in a file +# CN1SS helpers are implemented in Python for easier maintenance +CN1SS_TOOL="" + count_chunks() { - local f="${1:-}" n="0" - if [ -n "$f" ] && [ -r "$f" ]; then - n="$(grep -cE 'CN1SS:[0-9]{6}:' "$f" 2>/dev/null || echo 0)" + local f="${1:-}" + if [ -z "$CN1SS_TOOL" ] || [ ! -x "$CN1SS_TOOL" ]; then + echo 0 + return + fi + if [ -z "$f" ] || [ ! -r "$f" ]; then + echo 0 + return fi - n="${n//[^0-9]/}"; [ -z "$n" ] && n="0" - printf '%s\n' "$n" + python3 "$CN1SS_TOOL" count "$f" 2>/dev/null || echo 0 } -# Extract ordered CN1SS payload (by 6-digit index) and decode to PNG -# Usage: extract_cn1ss_stream > -# Extract ordered CN1SS payload (by 6-digit index) from ANYWHERE in the line -# Usage: extract_cn1ss_stream > -extract_cn1ss_stream() { - local f="$1" - awk ' - { - # Find CN1SS:<6digits>: anywhere in the line - if (match($0, /CN1SS:[0-9][0-9][0-9][0-9][0-9][0-9]:/)) { - # Extract the 6-digit index just after CN1SS: - idx = substr($0, RSTART + 6, 6) + 0 - # Payload is everything after the matched token - payload = substr($0, RSTART + RLENGTH) - gsub(/[ \t\r\n]/, "", payload) - gsub(/[^A-Za-z0-9+\/=]/, "", payload) - printf "%06d %s\n", idx, payload - next - } - # Pass through CN1SS meta lines for debugging (INFO/END/etc), even if prefixed - if (index($0, "CN1SS:") > 0) { - print "#META " $0 > "/dev/stderr" - } - } - ' "$f" \ - | sort -n \ - | awk '{ $1=""; sub(/^ /,""); printf "%s", $0 }' \ - | tr -d "\r\n" +extract_cn1ss_base64() { + local f="${1:-}" + if [ -z "$CN1SS_TOOL" ] || [ ! -x "$CN1SS_TOOL" ]; then + return 1 + fi + if [ -z "$f" ] || [ ! -r "$f" ]; then + return 1 + fi + python3 "$CN1SS_TOOL" extract "$f" +} + +decode_cn1ss_png() { + local f="${1:-}" + if [ -z "$CN1SS_TOOL" ] || [ ! -x "$CN1SS_TOOL" ]; then + return 1 + fi + if [ -z "$f" ] || [ ! -r "$f" ]; then + return 1 + fi + python3 "$CN1SS_TOOL" extract "$f" --decode } # Verify PNG signature + non-zero size @@ -67,6 +65,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" +CN1SS_TOOL="$SCRIPT_DIR/android/tests/cn1ss_chunk_tools.py" +if [ ! -x "$CN1SS_TOOL" ]; then + ra_log "Missing CN1SS helper: $CN1SS_TOOL" >&2 + exit 3 +fi + TMPDIR="${TMPDIR:-/tmp}"; TMPDIR="${TMPDIR%/}" DOWNLOAD_DIR="${TMPDIR}/codenameone-tools" ENV_DIR="$DOWNLOAD_DIR/tools" @@ -174,7 +178,7 @@ if [ "${#XMLS[@]}" -gt 0 ] && [ "${XML_CHUNKS_TOTAL:-0}" -gt 0 ]; then c="$(count_chunks "$x")"; c="${c//[^0-9]/}"; : "${c:=0}" [ "$c" -gt 0 ] || continue ra_log "Reassembling from XML: $x (chunks=$c)" - if extract_cn1ss_stream "$x" | base64 -d > "$SCREENSHOT_OUT" 2>/dev/null; then + if decode_cn1ss_png "$x" > "$SCREENSHOT_OUT" 2>/dev/null; then if verify_png "$SCREENSHOT_OUT"; then SOURCE="XML"; break; fi fi done @@ -182,14 +186,14 @@ fi if [ -z "$SOURCE" ] && [ "${LOGCAT_CHUNKS:-0}" -gt 0 ]; then ra_log "Reassembling from logcat: $LOGCAT_FILE (chunks=$LOGCAT_CHUNKS)" - if extract_cn1ss_stream "$LOGCAT_FILE" | base64 -d > "$SCREENSHOT_OUT" 2>/dev/null; then + if decode_cn1ss_png "$LOGCAT_FILE" > "$SCREENSHOT_OUT" 2>/dev/null; then if verify_png "$SCREENSHOT_OUT"; then SOURCE="LOGCAT"; fi fi fi if [ -z "$SOURCE" ] && [ -n "${TEST_EXEC_LOG:-}" ] && [ "${EXECLOG_CHUNKS:-0}" -gt 0 ]; then ra_log "Reassembling from test-results.log: $TEST_EXEC_LOG (chunks=$EXECLOG_CHUNKS)" - if extract_cn1ss_stream "$TEST_EXEC_LOG" | base64 -d > "$SCREENSHOT_OUT" 2>/dev/null; then + if decode_cn1ss_png "$TEST_EXEC_LOG" > "$SCREENSHOT_OUT" 2>/dev/null; then if verify_png "$SCREENSHOT_OUT"; then SOURCE="EXECLOG"; fi fi fi @@ -202,15 +206,15 @@ if [ -z "$SOURCE" ]; then RAW_B64_OUT="${SCREENSHOT_OUT}.raw.b64" { # Try to emit concatenated base64 from whichever had chunks (priority logcat, then XML, then exec) - if [ "${LOGCAT_CHUNKS:-0}" -gt 0 ]; then extract_cn1ss_stream "$LOGCAT_FILE"; fi + if [ "${LOGCAT_CHUNKS:-0}" -gt 0 ]; then extract_cn1ss_base64 "$LOGCAT_FILE"; fi if [ "${XML_CHUNKS_TOTAL:-0}" -gt 0 ] && [ "${LOGCAT_CHUNKS:-0}" -eq 0 ]; then # concatenate all XMLs for x in "${XMLS[@]}"; do - if [ "$(count_chunks "$x")" -gt 0 ]; then extract_cn1ss_stream "$x"; fi + if [ "$(count_chunks "$x")" -gt 0 ]; then extract_cn1ss_base64 "$x"; fi done fi if [ -n "${TEST_EXEC_LOG:-}" ] && [ "${EXECLOG_CHUNKS:-0}" -gt 0 ] && [ "${LOGCAT_CHUNKS:-0}" -eq 0 ] && [ "${XML_CHUNKS_TOTAL:-0}" -eq 0 ]; then - extract_cn1ss_stream "$TEST_EXEC_LOG" + extract_cn1ss_base64 "$TEST_EXEC_LOG" fi } > "$RAW_B64_OUT" 2>/dev/null || true if [ -s "$RAW_B64_OUT" ]; then @@ -238,4 +242,4 @@ for x in "${XMLS[@]}"; do done [ -n "${TEST_EXEC_LOG:-}" ] && cp -f "$TEST_EXEC_LOG" "$ARTIFACTS_DIR/test-results.log" 2>/dev/null || true -exit 0 \ No newline at end of file +exit 0