diff --git a/cn1-maven-archetypes b/cn1-maven-archetypes
new file mode 160000
index 0000000000..639042f98b
--- /dev/null
+++ b/cn1-maven-archetypes
@@ -0,0 +1 @@
+Subproject commit 639042f98b738c73f7ace4bea413d4500c0eae49
diff --git a/scripts/DeviceRunnerTest/pom.xml b/scripts/DeviceRunnerTest/pom.xml
new file mode 100644
index 0000000000..d27754d4a7
--- /dev/null
+++ b/scripts/DeviceRunnerTest/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ com.mycompany
+ device-runner-test
+ 1.0-SNAPSHOT
+
+
+ 1.8
+ 1.8
+ 7.0.41
+ 7.0.41
+
+
+
+
+ com.codenameone
+ codenameone-core
+ ${codenameone.version}
+
+
+
+
+
+
+ com.codenameone
+ codenameone-maven-plugin
+ ${codenameone.plugin.version}
+
+
+
+ build
+
+
+
+
+
+
+
+
+ android
+
+ android
+
+
+
+ ios
+
+ ios
+
+
+
+
diff --git a/scripts/DeviceRunnerTest/src/codenameone_settings.properties b/scripts/DeviceRunnerTest/src/codenameone_settings.properties
new file mode 100644
index 0000000000..68bd7791cb
--- /dev/null
+++ b/scripts/DeviceRunnerTest/src/codenameone_settings.properties
@@ -0,0 +1,19 @@
+#
+#Mon Oct 27 12:12:51 IST 2025
+codename1.vendor=CodenameOne
+codename1.displayName=DeviceRunnerTest
+codename1.mainName=DeviceRunnerTest
+codename1.ios.release.provision=
+codename1.languageLevel=5
+codename1.ios.release.certificate=
+codename1.ios.version=1.0
+codename1.secondaryTitle=DeviceRunnerTest
+codename1.android.keystorePassword=
+codename1.android.keystoreAlias=
+codename1.android.keyPassword=
+codename1.packageName=com.mycompany.app
+codename1.ios.debug.certificate=
+codename1.version=1.0
+codename1.ios.debug.provision=
+codename1.android.keystore=
+codename1.icon=icon.png
diff --git a/scripts/DeviceRunnerTest/src/com/mycompany/app/DeviceRunnerTest.java b/scripts/DeviceRunnerTest/src/com/mycompany/app/DeviceRunnerTest.java
new file mode 100644
index 0000000000..d048be505a
--- /dev/null
+++ b/scripts/DeviceRunnerTest/src/com/mycompany/app/DeviceRunnerTest.java
@@ -0,0 +1,50 @@
+package com.mycompany.app;
+
+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.util.UITimer;
+import com.codename1.io.Log;
+import java.io.IOException;
+import com.codename1.ui.Image;
+import java.io.ByteArrayOutputStream;
+import com.codename1.io.Base64;
+
+public class DeviceRunnerTest {
+
+ private Form current;
+
+ public void init(Object context) {
+ }
+
+ public void start() {
+ if (current != null) {
+ current.show();
+ return;
+ }
+ Form hi = new Form("Hi World", new BorderLayout());
+ hi.add(BorderLayout.CENTER, new Label("Hello, World!"));
+ hi.show();
+ UITimer.timer(1000, false, () -> {
+ Image screenshot = Image.createImage(hi.getWidth(), hi.getHeight());
+ hi.paintComponent(screenshot.getGraphics(), true);
+ try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+ com.codename1.ui.ImageIO.getImageIO().save(screenshot, os, com.codename1.ui.Image.FORMAT_PNG, 1.0f);
+ System.out.println("CN1SS_BEGIN");
+ System.out.println(Base64.encode(os.toByteArray()));
+ System.out.println("CN1SS_END");
+ } catch (IOException e) {
+ Log.e(e);
+ }
+ Display.getInstance().exitApplication();
+ });
+ }
+
+ public void stop() {
+ current = Display.getInstance().getCurrent();
+ }
+
+ public void destroy() {
+ }
+}
diff --git a/scripts/android/lib/PatchGradleFiles.java b/scripts/android/lib/PatchGradleFiles.java
deleted file mode 100644
index 549da1dbf5..0000000000
--- a/scripts/android/lib/PatchGradleFiles.java
+++ /dev/null
@@ -1,319 +0,0 @@
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class PatchGradleFiles {
- private static final String REPOSITORIES_BLOCK = """
- repositories {
- google()
- mavenCentral()
- }
- """.stripTrailing();
-
- private static final Pattern REPOSITORIES_PATTERN = Pattern.compile("(?ms)^\\s*repositories\\s*\\{.*?\\}");
-
- private static final Pattern ANDROID_BLOCK_PATTERN = Pattern.compile("(?m)^\\s*android\\s*\\{");
- private static final Pattern DEFAULT_CONFIG_PATTERN = Pattern.compile("(?ms)^\\s*defaultConfig\\s*\\{.*?^\\s*\\}");
- private static final Pattern DEFAULT_CONFIG_HEADER_PATTERN = Pattern.compile("(?ms)^\\s*defaultConfig\\s*\\{");
- private static final Pattern COMPILE_SDK_PATTERN = Pattern.compile("(?m)^\\s*compileSdkVersion\\s+\\d+");
- private static final Pattern TARGET_SDK_PATTERN = Pattern.compile("(?m)^\\s*targetSdkVersion\\s+\\d+");
- private static final Pattern TEST_INSTRUMENTATION_PATTERN = Pattern.compile("(?m)^\\s*testInstrumentationRunner\\s*\".*?\"\\s*$");
- private static final Pattern USE_LIBRARY_PATTERN = Pattern.compile("(?m)^\\s*useLibrary\\s+'android\\.test\\.(?:base|mock|runner)'\\s*$");
- private static final Pattern DEPENDENCY_PATTERN = Pattern.compile("(?m)^\\s*(implementation|api|testImplementation|androidTestImplementation)\\b");
-
- public static void main(String[] args) throws Exception {
- Arguments arguments = Arguments.parse(args);
- if (arguments == null) {
- System.exit(2);
- return;
- }
-
- boolean modifiedRoot = patchRootBuildGradle(arguments.root);
- boolean modifiedApp = patchAppBuildGradle(arguments.app, arguments.compileSdk, arguments.targetSdk);
-
- if (modifiedRoot) {
- System.out.println("Patched " + arguments.root);
- }
- if (modifiedApp) {
- System.out.println("Patched " + arguments.app);
- }
- if (!modifiedRoot && !modifiedApp) {
- System.out.println("Gradle files already normalized");
- }
- }
-
- private static boolean patchRootBuildGradle(Path path) throws IOException {
- String content = Files.readString(path, StandardCharsets.UTF_8);
- Matcher matcher = REPOSITORIES_PATTERN.matcher(content);
- if (!matcher.find()) {
- if (!content.endsWith("\n")) {
- content += "\n";
- }
- content += REPOSITORIES_BLOCK;
- Files.writeString(path, ensureTrailingNewline(content), StandardCharsets.UTF_8);
- return true;
- }
-
- String block = matcher.group();
- boolean changed = false;
- if (!block.contains("google()") || !block.contains("mavenCentral()")) {
- String[] lines = block.split("\n");
- java.util.LinkedHashSet body = new java.util.LinkedHashSet<>();
- for (int i = 1; i < lines.length - 1; i++) {
- String line = lines[i].trim();
- if (!line.isEmpty()) {
- body.add(" " + line.trim());
- }
- }
- body.add(" google()");
- body.add(" mavenCentral()");
- StringBuilder newBlock = new StringBuilder();
- newBlock.append(lines[0]).append('\n');
- for (String line : body) {
- newBlock.append(line).append('\n');
- }
- newBlock.append(lines[lines.length - 1]);
- content = content.substring(0, matcher.start()) + newBlock + content.substring(matcher.end());
- changed = true;
- }
-
- if (changed) {
- Files.writeString(path, ensureTrailingNewline(content), StandardCharsets.UTF_8);
- }
- return changed;
- }
-
- private static boolean patchAppBuildGradle(Path path, int compileSdk, int targetSdk) throws IOException {
- String content = Files.readString(path, StandardCharsets.UTF_8);
- boolean changed = false;
-
- Result r = ensureAndroidBlock(content, compileSdk, targetSdk);
- content = r.content();
- changed |= r.changed();
-
- r = ensureInstrumentationRunner(content);
- content = r.content();
- changed |= r.changed();
-
- r = removeLegacyUseLibrary(content);
- content = r.content();
- changed |= r.changed();
-
- r = ensureTestDependencies(content);
- content = r.content();
- changed |= r.changed();
-
- if (changed) {
- Files.writeString(path, ensureTrailingNewline(content), StandardCharsets.UTF_8);
- }
- return changed;
- }
-
- private static Result ensureAndroidBlock(String content, int compileSdk, int targetSdk) {
- Matcher androidBlockMatcher = ANDROID_BLOCK_PATTERN.matcher(content);
- if (!androidBlockMatcher.find()) {
- if (!content.endsWith("\n")) {
- content += "\n";
- }
- String block = "\nandroid {\n" +
- " compileSdkVersion " + compileSdk + "\n" +
- " defaultConfig {\n" +
- " targetSdkVersion " + targetSdk + "\n" +
- " }\n}";
- return new Result(content + block, true);
- }
-
- boolean changed = false;
- Matcher compileMatcher = COMPILE_SDK_PATTERN.matcher(content);
- if (compileMatcher.find()) {
- String replacement = " compileSdkVersion " + compileSdk;
- String newContent = compileMatcher.replaceFirst(replacement);
- if (!newContent.equals(content)) {
- content = newContent;
- changed = true;
- }
- } else {
- Matcher insertMatcher = ANDROID_BLOCK_PATTERN.matcher(content);
- if (insertMatcher.find()) {
- int pos = insertMatcher.end();
- content = content.substring(0, pos) + "\n compileSdkVersion " + compileSdk + content.substring(pos);
- changed = true;
- }
- }
-
- Matcher defaultConfigMatcher = DEFAULT_CONFIG_PATTERN.matcher(content);
- if (defaultConfigMatcher.find()) {
- String block = defaultConfigMatcher.group();
- Matcher targetMatcher = TARGET_SDK_PATTERN.matcher(block);
- String replacement = " targetSdkVersion " + targetSdk;
- String updated;
- if (targetMatcher.find()) {
- updated = targetMatcher.replaceFirst(replacement);
- } else {
- int brace = block.indexOf('{');
- if (brace >= 0) {
- updated = block.substring(0, brace + 1) + "\n" + replacement + block.substring(brace + 1);
- } else {
- updated = block;
- }
- }
- if (!updated.equals(block)) {
- content = content.substring(0, defaultConfigMatcher.start()) + updated + content.substring(defaultConfigMatcher.end());
- changed = true;
- }
- } else {
- Matcher insertMatcher = ANDROID_BLOCK_PATTERN.matcher(content);
- if (insertMatcher.find()) {
- int pos = insertMatcher.end();
- String snippet = "\n defaultConfig {\n targetSdkVersion " + targetSdk + "\n }";
- content = content.substring(0, pos) + snippet + content.substring(pos);
- changed = true;
- }
- }
-
- return new Result(content, changed);
- }
-
- private static Result ensureInstrumentationRunner(String content) {
- String runner = "androidx.test.runner.AndroidJUnitRunner";
- if (content.contains(runner)) {
- return new Result(content, false);
- }
- Matcher matcher = TEST_INSTRUMENTATION_PATTERN.matcher(content);
- if (matcher.find()) {
- String replacement = " testInstrumentationRunner \"" + runner + "\"";
- String newContent = matcher.replaceAll(replacement);
- return new Result(newContent, !newContent.equals(content));
- }
-
- Matcher defaultConfigHeaderMatcher = DEFAULT_CONFIG_HEADER_PATTERN.matcher(content);
- if (defaultConfigHeaderMatcher.find()) {
- int pos = defaultConfigHeaderMatcher.end();
- String snippet = "\n testInstrumentationRunner \"" + runner + "\"";
- content = content.substring(0, pos) + snippet + content.substring(pos);
- return new Result(content, true);
- }
-
- Matcher androidMatcher = ANDROID_BLOCK_PATTERN.matcher(content);
- if (androidMatcher.find()) {
- int pos = androidMatcher.end();
- String snippet = "\n defaultConfig {\n testInstrumentationRunner \"" + runner + "\"\n }";
- content = content.substring(0, pos) + snippet + content.substring(pos);
- return new Result(content, true);
- }
- return new Result(content, false);
- }
-
- private static Result removeLegacyUseLibrary(String content) {
- Matcher matcher = USE_LIBRARY_PATTERN.matcher(content);
- String newContent = matcher.replaceAll("");
- return new Result(newContent, !newContent.equals(content));
- }
-
- private static Result ensureTestDependencies(String content) {
- String moduleView = content.replaceAll("(?ms)^\\s*(buildscript|pluginManagement)\\s*\\{.*?^\\s*\\}", "");
- boolean usesModern = DEPENDENCY_PATTERN.matcher(moduleView).find();
- String configuration = usesModern ? "androidTestImplementation" : "androidTestCompile";
- String[] 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"
- };
- boolean missing = false;
- for (String dep : dependencies) {
- if (!moduleView.contains(dep)) {
- missing = true;
- break;
- }
- }
- if (!missing) {
- return new Result(content, false);
- }
- StringBuilder block = new StringBuilder();
- block.append("\n\ndependencies {\n");
- for (String dep : dependencies) {
- if (!moduleView.contains(dep)) {
- block.append(" ").append(configuration).append(" \"").append(dep).append("\"\n");
- }
- }
- block.append("}\n");
- if (!content.endsWith("\n")) {
- content += "\n";
- }
- return new Result(content + block, true);
- }
-
- private static String ensureTrailingNewline(String content) {
- return content.endsWith("\n") ? content : content + "\n";
- }
-
- private record Result(String content, boolean changed) {
- }
-
- private static class Arguments {
- final Path root;
- final Path app;
- final int compileSdk;
- final int targetSdk;
-
- Arguments(Path root, Path app, int compileSdk, int targetSdk) {
- this.root = root;
- this.app = app;
- this.compileSdk = compileSdk;
- this.targetSdk = targetSdk;
- }
-
- static Arguments parse(String[] args) {
- Path root = null;
- Path app = null;
- int compileSdk = 33;
- int targetSdk = 33;
- for (int i = 0; i < args.length; i++) {
- String arg = args[i];
- switch (arg) {
- case "--root" -> {
- if (i + 1 >= args.length) {
- System.err.println("Missing value for --root");
- return null;
- }
- root = Path.of(args[++i]);
- }
- case "--app" -> {
- if (i + 1 >= args.length) {
- System.err.println("Missing value for --app");
- return null;
- }
- app = Path.of(args[++i]);
- }
- case "--compile-sdk" -> {
- if (i + 1 >= args.length) {
- System.err.println("Missing value for --compile-sdk");
- return null;
- }
- compileSdk = Integer.parseInt(args[++i]);
- }
- case "--target-sdk" -> {
- if (i + 1 >= args.length) {
- System.err.println("Missing value for --target-sdk");
- return null;
- }
- targetSdk = Integer.parseInt(args[++i]);
- }
- default -> {
- System.err.println("Unknown argument: " + arg);
- return null;
- }
- }
- }
- if (root == null || app == null) {
- System.err.println("--root and --app are required");
- return null;
- }
- return new Arguments(root, app, compileSdk, targetSdk);
- }
- }
-}
diff --git a/scripts/ios/create-shared-scheme.py b/scripts/ios/create-shared-scheme.py
deleted file mode 100755
index 12b9f63efa..0000000000
--- a/scripts/ios/create-shared-scheme.py
+++ /dev/null
@@ -1,270 +0,0 @@
-#!/usr/bin/env python3
-"""Ensure an Xcode scheme exists that wires the UI test bundle for Codename One CI.
-
-The Codename One iOS template historically only shipped a user-specific scheme,
-which means fresh CI machines don't see any test actions when invoking
-``xcodebuild test``. This helper inspects the generated project, discovers the
-primary application target and any associated unit/UI test bundles, and emits a
-shared scheme that drives them.
-
-Usage:
- create-shared-scheme.py [scheme_name]
-
-The script writes the shared scheme into both the .xcodeproj and any sibling
-.xcworkspace directories so either entry point exposes the test action.
-"""
-
-from __future__ import annotations
-
-import argparse
-import re
-import sys
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Iterable, List, Optional
-
-
-@dataclass
-class Target:
- identifier: str
- name: str
- product_type: str
- product_name: Optional[str]
-
- @property
- def buildable_name(self) -> str:
- if self.product_name:
- return self.product_name
- if self.product_type.endswith(".application"):
- return f"{self.name}.app"
- if self.product_type.endswith(".bundle.ui-testing") or self.product_type.endswith(".bundle.unit-test"):
- return f"{self.name}.xctest"
- return self.name
-
-
-TARGET_BLOCK_RE = re.compile(
- r"""
- ^\s*(?P[0-9A-F]{24})\s+/\*\s+(?P[^*]+)\s+\*/\s+=\s+\{
- (?P.*?)
- ^\s*\};
- """,
- re.MULTILINE | re.DOTALL | re.VERBOSE,
-)
-
-
-def parse_targets(project_file: Path) -> List[Target]:
- content = project_file.read_text(encoding="utf-8")
- targets: List[Target] = []
- for match in TARGET_BLOCK_RE.finditer(content):
- body = match.group("body")
- if "isa = PBXNativeTarget;" not in body:
- continue
- name = _search_value(body, r"name = (?P[^;]+);")
- product_type = _search_value(body, r"productType = \"(?P[^\"]+)\";")
- product_name = _search_value(body, r"productReference = [0-9A-F]{24} /\* (?P[^*]+) \*/;")
- if not name or not product_type:
- continue
- targets.append(
- Target(
- identifier=match.group("identifier"),
- name=name.strip().strip('"'),
- product_type=product_type.strip(),
- product_name=product_name.strip() if product_name else None,
- )
- )
- return targets
-
-
-def _search_value(text: str, pattern: str) -> Optional[str]:
- m = re.search(pattern, text)
- return m.group("value") if m else None
-
-
-def choose_targets(targets: Iterable[Target]) -> tuple[Optional[Target], Optional[Target], Optional[Target]]:
- app = None
- ui = None
- unit = None
- for target in targets:
- if target.product_type.endswith(".application") and app is None:
- app = target
- elif target.product_type.endswith(".bundle.ui-testing") and ui is None:
- ui = target
- elif target.product_type.endswith(".bundle.unit-test") and unit is None:
- unit = target
- return app, unit, ui
-
-
-def render_testable(target: Target, container: str) -> str:
- return f"""
-
-
- """
-
-
-def render_scheme(
- scheme_name: str,
- project_container: str,
- app: Target,
- testables: List[str],
-) -> str:
- testables_block = "\n".join(testables) if testables else ""
- return f"""
-
-
-
-
-
-
-
-
-
-
-
-{testables_block}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
-
-
-def ensure_scheme(destination: Path, scheme_name: str, xml: str) -> None:
- destination.mkdir(parents=True, exist_ok=True)
- scheme_path = destination / f"{scheme_name}.xcscheme"
- scheme_path.write_text(xml, encoding="utf-8")
-
-
-def main(argv: List[str]) -> int:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("project_dir", type=Path)
- parser.add_argument("scheme_name", nargs="?", help="Override the generated scheme name")
- args = parser.parse_args(argv[1:])
-
- project_dir: Path = args.project_dir.resolve()
- if not project_dir.is_dir():
- print(f"error: project directory not found: {project_dir}", file=sys.stderr)
- return 1
-
- try:
- xcodeproj = next(project_dir.glob("*.xcodeproj"))
- except StopIteration:
- print(f"error: unable to locate an .xcodeproj under {project_dir}", file=sys.stderr)
- return 1
-
- project_container = xcodeproj.name
- project_file = xcodeproj / "project.pbxproj"
- if not project_file.is_file():
- print(f"error: missing project file: {project_file}", file=sys.stderr)
- return 1
-
- targets = parse_targets(project_file)
- if not targets:
- print(f"error: no build targets discovered in {project_file}", file=sys.stderr)
- return 1
-
- app, unit, ui = choose_targets(targets)
- if not app:
- print("error: unable to find application target", file=sys.stderr)
- return 1
-
- scheme_name = args.scheme_name or app.name
-
- # Prefer UI tests only. Include unit tests only if there is no UI test target.
- testables: List[str] = []
- if ui is not None:
- testables.append(render_testable(ui, project_container))
- elif unit is not None:
- testables.append(render_testable(unit, project_container))
-
- if not testables:
- print("warning: no unit or UI test targets discovered; emitting app-only scheme", file=sys.stderr)
-
- xml = render_scheme(scheme_name, project_container, app, testables)
-
- destinations = [xcodeproj / "xcshareddata" / "xcschemes"]
- destinations.extend(ws / "xcshareddata" / "xcschemes" for ws in project_dir.glob("*.xcworkspace"))
-
- for dest in destinations:
- ensure_scheme(dest, scheme_name, xml)
-
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main(sys.argv))
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.. {
+ try {
+ ProcessBuilder adbLogcat = new ProcessBuilder("adb", "logcat");
+ Process logcat = adbLogcat.start();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(logcat.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ System.out.println(line);
+ }
+ }
+ logcat.destroy();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+
+ System.out.println("Starting app");
+ ProcessBuilder amStart = new ProcessBuilder("adb", "shell", "am", "start", "-n", "com.mycompany.app/com.mycompany.app.DeviceRunnerTestStub");
+ amStart.inheritIO();
+ amStart.start().waitFor();
+ System.out.println("App started");
+
+ executor.shutdown();
+ executor.awaitTermination(1, TimeUnit.MINUTES);
+ }
+
+ private static void runIOS() throws IOException, InterruptedException {
+ System.out.println("Running iOS test");
+ Path xcodeProject = Paths.get("scripts/DeviceRunnerTest/target/ios/DeviceRunnerTest.xcodeproj");
+ System.out.println("Found Xcode project: " + xcodeProject);
+ new ProcessBuilder("xcrun", "simctl", "boot", "iPhone 15 Pro").inheritIO().start().waitFor();
+
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.submit(() -> {
+ try {
+ ProcessBuilder logStream = new ProcessBuilder("xcrun", "simctl", "spawn", "iPhone 15 Pro", "log", "stream");
+ Process log = logStream.start();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(log.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ System.out.println(line);
+ }
+ }
+ log.destroy();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+
+ System.out.println("Running xcodebuild");
+ new ProcessBuilder("xcodebuild", "-project", xcodeProject.toString(), "-scheme", "DeviceRunnerTest", "-destination", "platform=iOS Simulator,name=iPhone 15 Pro", "test").inheritIO().start().waitFor();
+ System.out.println("xcodebuild finished");
+
+ executor.shutdown();
+ executor.awaitTermination(1, TimeUnit.MINUTES);
+ }
+
+ private static Path findApk(Path dir) throws IOException {
+ try (Stream stream = Files.walk(dir)) {
+ return stream.filter(p -> p.toString().endsWith(".apk")).findFirst().orElseThrow(() -> new RuntimeException("APK not found"));
+ }
+ }
+}
diff --git a/scripts/run-android-instrumentation-tests.sh b/scripts/run-android-instrumentation-tests.sh
index 56f8de7bc8..9fd05bb9a3 100755
--- a/scripts/run-android-instrumentation-tests.sh
+++ b/scripts/run-android-instrumentation-tests.sh
@@ -74,12 +74,8 @@ cn1ss_setup "$JAVA17_BIN" "$CN1SS_HELPER_SOURCE_DIR"
set -o pipefail
ra_log "Running instrumentation tests (stdout -> $TEST_LOG; stderr -> terminal)"
(
- cd "$GRADLE_PROJECT_DIR"
- ORIG_JAVA_HOME="${JAVA_HOME:-}"
- export JAVA_HOME="${JAVA17_HOME:?JAVA17_HOME not set}"
- ./gradlew --no-daemon --console=plain connectedDebugAndroidTest | tee "$TEST_LOG"
- export JAVA_HOME="$ORIG_JAVA_HOME"
-) || { ra_log "STAGE:GRADLE_TEST_FAILED (see $TEST_LOG)"; exit 10; }
+ "$JAVA17_HOME/bin/java" -cp scripts/java BuildAndRun android | tee "$TEST_LOG"
+) || { ra_log "STAGE:TEST_FAILED (see $TEST_LOG)"; exit 10; }
echo
ra_log "==== Begin connectedAndroidTest.log (tail -n 200) ===="
diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh
index a9cc289f70..27f0390fff 100755
--- a/scripts/run-ios-ui-tests.sh
+++ b/scripts/run-ios-ui-tests.sh
@@ -199,19 +199,8 @@ XCODE_TEST_FILTERS=(
)
set -o pipefail
-if ! xcodebuild \
- -workspace "$WORKSPACE_PATH" \
- -scheme "$SCHEME" \
- -sdk iphonesimulator \
- -configuration Debug \
- -destination "$SIM_DESTINATION" \
- -derivedDataPath "$DERIVED_DATA_DIR" \
- -resultBundlePath "$RESULT_BUNDLE" \
- "${XCODE_TEST_FILTERS[@]}" \
- CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
- GENERATE_INFOPLIST_FILE=YES \
- test | tee "$TEST_LOG"; then
- ri_log "STAGE:XCODE_TEST_FAILED -> See $TEST_LOG"
+if ! "$JAVA17_HOME/bin/java" -cp scripts/java BuildAndRun ios | tee "$TEST_LOG"; then
+ ri_log "STAGE:TEST_FAILED -> See $TEST_LOG"
exit 10
fi
set +o pipefail