diff --git a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl index e99b2fb0ae..75a90b9d10 100644 --- a/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl +++ b/scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl @@ -1,8 +1,13 @@ 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 @@ -43,6 +48,8 @@ final class HelloCodenameOneUITests: XCTestCase { att.name = name att.lifetime = .keepAlways add(att) + + emitScreenshotPayloads(for: shot, name: name) } /// Wait for foreground + a short settle time @@ -71,4 +78,82 @@ final class HelloCodenameOneUITests: XCTestCase { RunLoop.current.run(until: Date(timeIntervalSinceNow: 2.0)) try captureScreenshot(named: "BrowserComponent") } -} \ No newline at end of file + + 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 2>&1; then + declare -a CN1SS_JAVA_OPTS=() +fi +if [ "${#CN1SS_JAVA_OPTS[@]}" -eq 0 ]; then + CN1SS_JAVA_OPTS+=(-Djava.awt.headless=true) +fi + +cn1ss_setup() { + CN1SS_JAVA_BIN="$1" + CN1SS_SOURCE_PATH="$2" + local cache_override="${3:-}" tmp_root + + if [ -z "$CN1SS_JAVA_BIN" ] || [ ! -x "$CN1SS_JAVA_BIN" ]; then + cn1ss_log "CN1SS setup failed: java binary not executable ($CN1SS_JAVA_BIN)" + return 1 + fi + if [ -z "$CN1SS_SOURCE_PATH" ] || [ ! -d "$CN1SS_SOURCE_PATH" ]; then + cn1ss_log "CN1SS setup failed: source directory missing ($CN1SS_SOURCE_PATH)" + return 1 + fi + + if [ -z "$CN1SS_JAVAC_BIN" ]; then + local java_dir + java_dir="$(dirname "$CN1SS_JAVA_BIN")" + if [ -x "$java_dir/javac" ]; then + CN1SS_JAVAC_BIN="$java_dir/javac" + elif command -v javac >/dev/null 2>&1; then + CN1SS_JAVAC_BIN="$(command -v javac)" + else + cn1ss_log "CN1SS setup failed: unable to locate javac" + return 1 + fi + fi + + tmp_root="${TMPDIR:-/tmp}" + tmp_root="${tmp_root%/}" + CN1SS_CACHE_ROOT="${cache_override:-${CN1SS_CACHE_DIR:-$tmp_root/cn1ss-java-cache}}" + CN1SS_CLASS_DIR="$CN1SS_CACHE_ROOT/classes" + CN1SS_STAMP_FILE="$CN1SS_CACHE_ROOT/.stamp" + + if [ "$CN1SS_INITIALIZED" -eq 1 ] && [ -n "$CN1SS_JAVA_CLASSPATH" ] && [ -d "$CN1SS_JAVA_CLASSPATH" ]; then + return 0 + fi + + local need_compile=1 + if [ -d "$CN1SS_CLASS_DIR" ] && [ -f "$CN1SS_STAMP_FILE" ]; then + if ! find "$CN1SS_SOURCE_PATH" -type f -name '*.java' -newer "$CN1SS_STAMP_FILE" -print -quit | grep -q .; then + need_compile=0 + fi + fi + + if [ "$need_compile" -eq 1 ]; then + mkdir -p "$CN1SS_CACHE_ROOT" + rm -rf "$CN1SS_CLASS_DIR" + mkdir -p "$CN1SS_CLASS_DIR" + local -a sources=() + while IFS= read -r -d '' src; do + if grep -q '@PACKAGE@' "$src" 2>/dev/null; then + cn1ss_log "Skipping template source $src" + continue + fi + sources+=("$src") + done < <(find "$CN1SS_SOURCE_PATH" -type f -name '*.java' -print0 | sort -z) + if [ "${#sources[@]}" -eq 0 ]; then + cn1ss_log "CN1SS setup failed: no Java sources found under $CN1SS_SOURCE_PATH" + return 1 + fi + cn1ss_log "Compiling CN1SS helpers -> $CN1SS_CLASS_DIR" + local src display + for src in "${sources[@]}"; do + display="${src#$CN1SS_SOURCE_PATH/}" + display="${display:-$(basename "$src")}" + cn1ss_log " javac $display" + if ! "$CN1SS_JAVAC_BIN" -d "$CN1SS_CLASS_DIR" -cp "$CN1SS_CLASS_DIR" "$src"; then + cn1ss_log "CN1SS setup failed: javac returned non-zero status ($display)" + return 1 + fi + done + touch "$CN1SS_STAMP_FILE" 2>/dev/null || true + else + cn1ss_log "Reusing CN1SS helpers in $CN1SS_CLASS_DIR" + fi + + CN1SS_JAVA_CLASSPATH="$CN1SS_CLASS_DIR" + CN1SS_INITIALIZED=1 +} + +cn1ss_log() { + echo "[cn1ss] $*" +} + +cn1ss_java_run() { + local class_name="$1"; shift + if [ -z "${CN1SS_JAVA_BIN:-}" ] || [ ! -x "$CN1SS_JAVA_BIN" ]; then + cn1ss_log "CN1SS_JAVA_BIN is not configured" + return 1 + fi + if [ -z "${CN1SS_JAVA_CLASSPATH:-}" ] || [ ! -d "$CN1SS_JAVA_CLASSPATH" ]; then + cn1ss_log "CN1SS Java helpers not initialized; call cn1ss_setup first" + return 1 + fi + "$CN1SS_JAVA_BIN" "${CN1SS_JAVA_OPTS[@]}" -cp "$CN1SS_JAVA_CLASSPATH" "$class_name" "$@" +} + +cn1ss_count_chunks() { + local file="$1" + local test="${2:-}" + local channel="${3:-}" + if [ -z "$file" ] || [ ! -r "$file" ]; then + echo 0 + return + fi + local args=("count" "$file") + if [ -n "$test" ]; then + args+=("--test" "$test") + fi + if [ -n "$channel" ]; then + args+=("--channel" "$channel") + fi + cn1ss_java_run "$CN1SS_MAIN_CLASS" "${args[@]}" 2>/dev/null || echo 0 +} + +cn1ss_extract_base64() { + local file="$1" + local test="${2:-}" + local channel="${3:-}" + if [ -z "$file" ] || [ ! -r "$file" ]; then + return 1 + fi + local args=("extract" "$file") + if [ -n "$test" ]; then + args+=("--test" "$test") + fi + if [ -n "$channel" ]; then + args+=("--channel" "$channel") + fi + cn1ss_java_run "$CN1SS_MAIN_CLASS" "${args[@]}" +} + +cn1ss_decode_binary() { + local file="$1" + local test="${2:-}" + local channel="${3:-}" + if [ -z "$file" ] || [ ! -r "$file" ]; then + return 1 + fi + local args=("extract" "$file" "--decode") + if [ -n "$test" ]; then + args+=("--test" "$test") + fi + if [ -n "$channel" ]; then + args+=("--channel" "$channel") + fi + cn1ss_java_run "$CN1SS_MAIN_CLASS" "${args[@]}" +} + +cn1ss_list_tests() { + local file="$1" + if [ -z "$file" ] || [ ! -r "$file" ]; then + return 1 + fi + cn1ss_java_run "$CN1SS_MAIN_CLASS" tests "$file" +} + +cn1ss_verify_png() { + local file="$1" + [ -s "$file" ] || return 1 + head -c 8 "$file" | od -An -t x1 | tr -d ' \n' | grep -qi '^89504e470d0a1a0a$' +} + +cn1ss_verify_jpeg() { + local file="$1" + [ -s "$file" ] || return 1 + local header trailer + header="$(head -c 2 "$file" | od -An -t x1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')" + trailer="$(tail -c 2 "$file" | od -An -t x1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')" + [ "$header" = "FFD8" ] && [ "$trailer" = "FFD9" ] +} + +cn1ss_decode_test_asset() { + local test="$1"; shift + local dest="$1"; shift + local channel="$1"; shift + local verifier="$1"; shift + local entry source_type source_path count + + rm -f "$dest" 2>/dev/null || true + for entry in "$@"; do + source_type="${entry%%:*}" + source_path="${entry#*:}" + [ -s "$source_path" ] || continue + count="$(cn1ss_count_chunks "$source_path" "$test" "$channel")" + count="${count//[^0-9]/}"; : "${count:=0}" + [ "$count" -gt 0 ] || continue + cn1ss_log "Reassembling test '$test' from ${source_type} source: $source_path (chunks=$count)" + if cn1ss_decode_binary "$source_path" "$test" "$channel" > "$dest" 2>/dev/null; then + if [ -z "$verifier" ] || "$verifier" "$dest"; then + echo "${source_type}:$(basename "$source_path")" + return 0 + fi + fi + done + rm -f "$dest" 2>/dev/null || true + return 1 +} + +cn1ss_decode_test_png() { + local test="$1"; shift + local dest="$1"; shift + cn1ss_decode_test_asset "$test" "$dest" "" cn1ss_verify_png "$@" +} + +cn1ss_decode_test_preview() { + local test="$1"; shift + local dest="$1"; shift + cn1ss_decode_test_asset "$test" "$dest" "PREVIEW" cn1ss_verify_jpeg "$@" +} + +cn1ss_file_size() { + local file="$1" + if [ ! -f "$file" ]; then + echo 0 + return + fi + if stat --version >/dev/null 2>&1; then + stat --printf='%s' "$file" + elif stat -f '%z' "$file" >/dev/null 2>&1; then + stat -f '%z' "$file" + else + wc -c < "$file" 2>/dev/null | tr -d ' \n' + fi +} + +cn1ss_post_pr_comment() { + local body_file="$1" + local preview_dir="$2" + if [ -z "$body_file" ] || [ ! -s "$body_file" ]; then + cn1ss_log "Skipping PR comment post (no content)." + return 0 + fi + local comment_token="${GITHUB_TOKEN:-}"; local body_size + if [ -z "$comment_token" ] && [ -n "${GH_TOKEN:-}" ]; then + comment_token="${GH_TOKEN}" + cn1ss_log "PR comment auth using GH_TOKEN fallback" + fi + if [ -z "$comment_token" ]; then + cn1ss_log "PR comment skipped (no GitHub token available)" + return 0 + fi + if [ -z "${GITHUB_EVENT_PATH:-}" ] || [ ! -f "$GITHUB_EVENT_PATH" ]; then + cn1ss_log "PR comment skipped (GITHUB_EVENT_PATH unavailable)" + return 0 + fi + body_size=$(wc -c < "$body_file" 2>/dev/null || echo 0) + cn1ss_log "Attempting to post PR comment (payload bytes=${body_size})" + GITHUB_TOKEN="$comment_token" cn1ss_java_run "$CN1SS_POST_COMMENT_CLASS" \ + --body "$body_file" \ + --preview-dir "$preview_dir" + local rc=$? + if [ $rc -eq 0 ]; then + cn1ss_log "Posted screenshot comparison comment to PR" + else + cn1ss_log "STAGE:COMMENT_POST_FAILED (see stderr for details)" + if [ -n "${ARTIFACTS_DIR:-}" ]; then + local failure_flag="$ARTIFACTS_DIR/pr-comment-failed.txt" + printf 'Comment POST failed at %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > "$failure_flag" 2>/dev/null || true + fi + fi + return $rc +} diff --git a/scripts/run-android-instrumentation-tests.sh b/scripts/run-android-instrumentation-tests.sh index 081970f52f..56f8de7bc8 100755 --- a/scripts/run-android-instrumentation-tests.sh +++ b/scripts/run-android-instrumentation-tests.sh @@ -9,203 +9,11 @@ ra_log() { echo "[run-android-instrumentation-tests] $1"; } ensure_dir() { mkdir -p "$1" 2>/dev/null || true; } # CN1SS helpers are implemented in Java for easier maintenance -CN1SS_SOURCE_PATH="" CN1SS_MAIN_CLASS="Cn1ssChunkTools" POST_COMMENT_CLASS="PostPrComment" PROCESS_SCREENSHOTS_CLASS="ProcessScreenshots" RENDER_SCREENSHOT_REPORT_CLASS="RenderScreenshotReport" -count_chunks() { - local f="${1:-}" - local test="${2:-}" - local channel="${3:-}" - if [ ! -f "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" ]; then - echo 0 - return - fi - if [ -z "$f" ] || [ ! -r "$f" ]; then - echo 0 - return - fi - local args=("count" "$f") - if [ -n "$test" ]; then - args+=("--test" "$test") - fi - if [ -n "$channel" ]; then - args+=("--channel" "$channel") - fi - "$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" "${args[@]}" 2>/dev/null || echo 0 -} - -extract_cn1ss_base64() { - local f="${1:-}" - local test="${2:-}" - local channel="${3:-}" - if [ ! -f "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" ]; then - return 1 - fi - if [ -z "$f" ] || [ ! -r "$f" ]; then - return 1 - fi - local args=("extract" "$f") - if [ -n "$test" ]; then - args+=("--test" "$test") - fi - if [ -n "$channel" ]; then - args+=("--channel" "$channel") - fi - "$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" "${args[@]}" -} - -decode_cn1ss_binary() { - local f="${1:-}" - local test="${2:-}" - local channel="${3:-}" - if [ ! -f "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" ]; then - return 1 - fi - if [ -z "$f" ] || [ ! -r "$f" ]; then - return 1 - fi - local args=("extract" "$f" "--decode") - if [ -n "$test" ]; then - args+=("--test" "$test") - fi - if [ -n "$channel" ]; then - args+=("--channel" "$channel") - fi - "$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" "${args[@]}" -} - -list_cn1ss_tests() { - local f="${1:-}" - if [ ! -f "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" ]; then - return 1 - fi - if [ -z "$f" ] || [ ! -r "$f" ]; then - return 1 - fi - "$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" tests "$f" -} - - -post_pr_comment() { - local body_file="${1:-}" - local preview_dir="${2:-}" - if [ -z "$body_file" ] || [ ! -s "$body_file" ]; then - ra_log "Skipping PR comment post (no content)." - return 0 - fi - local comment_token="${GITHUB_TOKEN:-}" - if [ -z "$comment_token" ] && [ -n "${GH_TOKEN:-}" ]; then - comment_token="${GH_TOKEN}" - ra_log "PR comment auth using GH_TOKEN fallback" - fi - if [ -n "$comment_token" ]; then - ra_log "PR comment authentication token detected" - fi - if [ -z "$comment_token" ]; then - ra_log "PR comment skipped (no GitHub token available)" - return 0 - fi - if [ -z "${GITHUB_EVENT_PATH:-}" ] || [ ! -f "$GITHUB_EVENT_PATH" ]; then - ra_log "PR comment skipped (GITHUB_EVENT_PATH unavailable)" - return 0 - fi - local body_size - body_size=$(wc -c < "$body_file" 2>/dev/null || echo 0) - ra_log "Attempting to post PR comment (payload bytes=${body_size})" - GITHUB_TOKEN="$comment_token" "$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$POST_COMMENT_CLASS.java" \ - --body "$body_file" \ - --preview-dir "$preview_dir" - local rc=$? - if [ $rc -eq 0 ]; then - ra_log "Posted screenshot comparison comment to PR" - else - ra_log "STAGE:COMMENT_POST_FAILED (see stderr for details)" - if [ -n "${ARTIFACTS_DIR:-}" ]; then - local failure_flag="$ARTIFACTS_DIR/pr-comment-failed.txt" - printf 'Comment POST failed at %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > "$failure_flag" 2>/dev/null || true - fi - fi - return $rc -} - -decode_test_asset() { - local test_name="${1:-}" - local dest="${2:-}" - local channel="${3:-}" - local verifier="${4:-}" - local source="" - local count="0" - - if [ "${#XMLS[@]}" -gt 0 ]; then - for x in "${XMLS[@]}"; do - count="$(count_chunks "$x" "$test_name" "$channel")"; count="${count//[^0-9]/}"; : "${count:=0}" - [ "$count" -gt 0 ] || continue - ra_log "Reassembling test '$test_name' from XML: $x (chunks=$count)" - if decode_cn1ss_binary "$x" "$test_name" "$channel" > "$dest" 2>/dev/null; then - if [ -z "$verifier" ] || "$verifier" "$dest"; then source="XML:$(basename "$x")"; break; fi - fi - done - fi - - if [ -z "$source" ] && [ "${#LOGCAT_FILES[@]}" -gt 0 ]; then - for logcat in "${LOGCAT_FILES[@]}"; do - [ -s "$logcat" ] || continue - count="$(count_chunks "$logcat" "$test_name" "$channel")"; count="${count//[^0-9]/}"; : "${count:=0}" - [ "$count" -gt 0 ] || continue - ra_log "Reassembling test '$test_name' from logcat: $logcat (chunks=$count)" - if decode_cn1ss_binary "$logcat" "$test_name" "$channel" > "$dest" 2>/dev/null; then - if [ -z "$verifier" ] || "$verifier" "$dest"; then source="LOGCAT:$(basename "$logcat")"; break; fi - fi - done - fi - - if [ -z "$source" ] && [ -n "${TEST_EXEC_LOG:-}" ] && [ -s "$TEST_EXEC_LOG" ]; then - count="$(count_chunks "$TEST_EXEC_LOG" "$test_name" "$channel")"; count="${count//[^0-9]/}"; : "${count:=0}" - if [ "$count" -gt 0 ]; then - ra_log "Reassembling test '$test_name' from test-results.log: $TEST_EXEC_LOG (chunks=$count)" - if decode_cn1ss_binary "$TEST_EXEC_LOG" "$test_name" "$channel" > "$dest" 2>/dev/null; then - if [ -z "$verifier" ] || "$verifier" "$dest"; then source="EXECLOG:$(basename "$TEST_EXEC_LOG")"; fi - fi - fi - fi - - if [ -n "$source" ]; then - printf '%s' "$source" - return 0 - fi - - rm -f "$dest" 2>/dev/null || true - return 1 -} - -decode_test_png() { - decode_test_asset "$1" "$2" "" verify_png -} - -decode_test_preview() { - decode_test_asset "$1" "$2" "PREVIEW" verify_jpeg -} - -# Verify PNG signature + non-zero size -verify_png() { - local f="$1" - [ -s "$f" ] || return 1 - head -c 8 "$f" | od -An -t x1 | tr -d ' \n' | grep -qi '^89504e470d0a1a0a$' -} - -verify_jpeg() { - local f="$1" - [ -s "$f" ] || return 1 - local header - header="$(head -c 2 "$f" | od -An -t x1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')" - local trailer - trailer="$(tail -c 2 "$f" | od -An -t x1 | tr -d ' \n' | tr '[:lower:]' '[:upper:]')" - [ "$header" = "FFD8" ] && [ "$trailer" = "FFD9" ] -} - # ---- Args & environment ---------------------------------------------------- if [ $# -lt 1 ]; then @@ -218,12 +26,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" -CN1SS_SOURCE_PATH="$SCRIPT_DIR/android/tests" -if [ ! -f "$CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" ]; then - ra_log "Missing CN1SS helper: $CN1SS_SOURCE_PATH/$CN1SS_MAIN_CLASS.java" >&2 +CN1SS_HELPER_SOURCE_DIR="$SCRIPT_DIR/android/tests" +if [ ! -f "$CN1SS_HELPER_SOURCE_DIR/$CN1SS_MAIN_CLASS.java" ]; then + ra_log "Missing CN1SS helper: $CN1SS_HELPER_SOURCE_DIR/$CN1SS_MAIN_CLASS.java" >&2 exit 3 fi +source "$SCRIPT_DIR/lib/cn1ss.sh" +cn1ss_log() { ra_log "$1"; } + TMPDIR="${TMPDIR:-/tmp}"; TMPDIR="${TMPDIR%/}" DOWNLOAD_DIR="${TMPDIR}/codenameone-tools" ENV_DIR="$DOWNLOAD_DIR/tools" @@ -253,6 +64,8 @@ if [ ! -x "$JAVA17_BIN" ]; then exit 3 fi +cn1ss_setup "$JAVA17_BIN" "$CN1SS_HELPER_SOURCE_DIR" + [ -d "$GRADLE_PROJECT_DIR" ] || { ra_log "Gradle project directory not found: $GRADLE_PROJECT_DIR"; exit 4; } [ -x "$GRADLE_PROJECT_DIR/gradlew" ] || chmod +x "$GRADLE_PROJECT_DIR/gradlew" @@ -300,6 +113,17 @@ mapfile -t LOGCAT_FILES < <( TEST_EXEC_LOG="$(find "$RESULTS_ROOT" -type f -path '*/testlog/test-results.log' -print -quit 2>/dev/null || true)" [ -n "${TEST_EXEC_LOG:-}" ] || TEST_EXEC_LOG="" +declare -a CN1SS_SOURCES=() +for x in "${XMLS[@]}"; do + CN1SS_SOURCES+=("XML:$x") +done +for logcat in "${LOGCAT_FILES[@]}"; do + CN1SS_SOURCES+=("LOGCAT:$logcat") +done +if [ -n "${TEST_EXEC_LOG:-}" ]; then + CN1SS_SOURCES+=("EXEC:$TEST_EXEC_LOG") +fi + if [ "${#XMLS[@]}" -gt 0 ]; then ra_log "Found ${#XMLS[@]} test result file(s). First candidate: ${XMLS[0]}" else @@ -316,15 +140,15 @@ fi XML_CHUNKS_TOTAL=0 for x in "${XMLS[@]}"; do - c="$(count_chunks "$x")"; c="${c//[^0-9]/}"; : "${c:=0}" + c="$(cn1ss_count_chunks "$x")"; c="${c//[^0-9]/}"; : "${c:=0}" XML_CHUNKS_TOTAL=$(( XML_CHUNKS_TOTAL + c )) done LOGCAT_CHUNKS=0 for logcat in "${LOGCAT_FILES[@]}"; do - c="$(count_chunks "$logcat")"; c="${c//[^0-9]/}"; : "${c:=0}" + c="$(cn1ss_count_chunks "$logcat")"; c="${c//[^0-9]/}"; : "${c:=0}" LOGCAT_CHUNKS=$(( LOGCAT_CHUNKS + c )) done -EXECLOG_CHUNKS="$(count_chunks "${TEST_EXEC_LOG:-}")"; EXECLOG_CHUNKS="${EXECLOG_CHUNKS//[^0-9]/}"; : "${EXECLOG_CHUNKS:=0}" +EXECLOG_CHUNKS="$(cn1ss_count_chunks "${TEST_EXEC_LOG:-}")"; EXECLOG_CHUNKS="${EXECLOG_CHUNKS//[^0-9]/}"; : "${EXECLOG_CHUNKS:=0}" ra_log "Chunk counts -> XML: ${XML_CHUNKS_TOTAL} | logcat: ${LOGCAT_CHUNKS} | test-results.log: ${EXECLOG_CHUNKS}" @@ -347,7 +171,7 @@ if [ "${#XMLS[@]}" -gt 0 ]; then while IFS= read -r name; do [ -n "$name" ] || continue TEST_NAME_SET["$name"]=1 - done < <(list_cn1ss_tests "$x" 2>/dev/null || true) + done < <(cn1ss_list_tests "$x" 2>/dev/null || true) done fi @@ -356,14 +180,14 @@ for logcat in "${LOGCAT_FILES[@]}"; do while IFS= read -r name; do [ -n "$name" ] || continue TEST_NAME_SET["$name"]=1 - done < <(list_cn1ss_tests "$logcat" 2>/dev/null || true) + done < <(cn1ss_list_tests "$logcat" 2>/dev/null || true) done if [ -n "${TEST_EXEC_LOG:-}" ] && [ -s "$TEST_EXEC_LOG" ]; then while IFS= read -r name; do [ -n "$name" ] || continue TEST_NAME_SET["$name"]=1 - done < <(list_cn1ss_tests "$TEST_EXEC_LOG" 2>/dev/null || true) + done < <(cn1ss_list_tests "$TEST_EXEC_LOG" 2>/dev/null || true) fi if [ "${#TEST_NAME_SET[@]}" -eq 0 ] && { [ "${LOGCAT_CHUNKS:-0}" -gt 0 ] || [ "${XML_CHUNKS_TOTAL:-0}" -gt 0 ] || [ "${EXECLOG_CHUNKS:-0}" -gt 0 ]; }; then @@ -391,14 +215,14 @@ ensure_dir "$SCREENSHOT_PREVIEW_DIR" for test in "${TEST_NAMES[@]}"; do dest="$SCREENSHOT_TMP_DIR/${test}.png" - if source_label="$(decode_test_png "$test" "$dest")"; then + if source_label="$(cn1ss_decode_test_png "$test" "$dest" "${CN1SS_SOURCES[@]}")"; then TEST_OUTPUTS["$test"]="$dest" TEST_SOURCES["$test"]="$source_label" - ra_log "Decoded screenshot for '$test' (source=${source_label}, size: $(stat -c '%s' "$dest") bytes)" + ra_log "Decoded screenshot for '$test' (source=${source_label}, size: $(cn1ss_file_size "$dest") bytes)" preview_dest="$SCREENSHOT_PREVIEW_DIR/${test}.jpg" - if preview_source="$(decode_test_preview "$test" "$preview_dest")"; then + if preview_source="$(cn1ss_decode_test_preview "$test" "$preview_dest" "${CN1SS_SOURCES[@]}")"; then PREVIEW_OUTPUTS["$test"]="$preview_dest" - ra_log "Decoded preview for '$test' (source=${preview_source}, size: $(stat -c '%s' "$preview_dest") bytes)" + ra_log "Decoded preview for '$test' (source=${preview_source}, size: $(cn1ss_file_size "$preview_dest") bytes)" else rm -f "$preview_dest" 2>/dev/null || true fi @@ -409,18 +233,18 @@ for test in "${TEST_NAMES[@]}"; do local count for logcat in "${LOGCAT_FILES[@]}"; do [ -s "$logcat" ] || continue - count="$(count_chunks "$logcat" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" - if [ "$count" -gt 0 ]; then extract_cn1ss_base64 "$logcat" "$test"; fi + count="$(cn1ss_count_chunks "$logcat" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" + if [ "$count" -gt 0 ]; then cn1ss_extract_base64 "$logcat" "$test"; fi done if [ "${#XMLS[@]}" -gt 0 ]; then for x in "${XMLS[@]}"; do - count="$(count_chunks "$x" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" - if [ "$count" -gt 0 ]; then extract_cn1ss_base64 "$x" "$test"; fi + count="$(cn1ss_count_chunks "$x" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" + if [ "$count" -gt 0 ]; then cn1ss_extract_base64 "$x" "$test"; fi done fi if [ -n "${TEST_EXEC_LOG:-}" ] && [ -s "$TEST_EXEC_LOG" ]; then - count="$(count_chunks "$TEST_EXEC_LOG" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" - if [ "$count" -gt 0 ]; then extract_cn1ss_base64 "$TEST_EXEC_LOG" "$test"; fi + count="$(cn1ss_count_chunks "$TEST_EXEC_LOG" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" + if [ "$count" -gt 0 ]; then cn1ss_extract_base64 "$TEST_EXEC_LOG" "$test"; fi fi } > "$RAW_B64_OUT" 2>/dev/null || true if [ -s "$RAW_B64_OUT" ]; then @@ -443,20 +267,26 @@ done COMPARE_JSON="$SCREENSHOT_TMP_DIR/screenshot-compare.json" export CN1SS_PREVIEW_DIR="$SCREENSHOT_PREVIEW_DIR" ra_log "STAGE:COMPARE -> Evaluating screenshots against stored references" -"$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$PROCESS_SCREENSHOTS_CLASS.java" \ +if ! cn1ss_java_run "$PROCESS_SCREENSHOTS_CLASS" \ --reference-dir "$SCREENSHOT_REF_DIR" \ --emit-base64 \ --preview-dir "$SCREENSHOT_PREVIEW_DIR" \ - "${COMPARE_ARGS[@]}" > "$COMPARE_JSON" + "${COMPARE_ARGS[@]}" > "$COMPARE_JSON"; then + ra_log "FATAL: Screenshot comparison helper failed" + exit 13 +fi SUMMARY_FILE="$SCREENSHOT_TMP_DIR/screenshot-summary.txt" COMMENT_FILE="$SCREENSHOT_TMP_DIR/screenshot-comment.md" ra_log "STAGE:COMMENT_BUILD -> Rendering summary and PR comment markdown" -"$JAVA17_BIN" "$CN1SS_SOURCE_PATH/$RENDER_SCREENSHOT_REPORT_CLASS.java" \ +if ! cn1ss_java_run "$RENDER_SCREENSHOT_REPORT_CLASS" \ --compare-json "$COMPARE_JSON" \ --comment-out "$COMMENT_FILE" \ - --summary-out "$SUMMARY_FILE" + --summary-out "$SUMMARY_FILE"; then + ra_log "FATAL: Failed to render screenshot summary/comment" + exit 14 +fi if [ -s "$SUMMARY_FILE" ]; then ra_log " -> Wrote summary entries to $SUMMARY_FILE ($(wc -l < "$SUMMARY_FILE" 2>/dev/null || echo 0) line(s))" @@ -494,7 +324,7 @@ fi ra_log "STAGE:COMMENT_POST -> Submitting PR feedback" comment_rc=0 -if ! post_pr_comment "$COMMENT_FILE" "$SCREENSHOT_PREVIEW_DIR"; then +if ! cn1ss_post_pr_comment "$COMMENT_FILE" "$SCREENSHOT_PREVIEW_DIR"; then comment_rc=$? fi diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index c5c99f7935..a9cc289f70 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -4,6 +4,8 @@ set -euo pipefail ri_log() { echo "[run-ios-ui-tests] $1"; } +ensure_dir() { mkdir -p "$1" 2>/dev/null || true; } + if [ $# -lt 1 ]; then ri_log "Usage: $0 [app_bundle] [scheme]" >&2 exit 2 @@ -32,6 +34,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" +CN1SS_MAIN_CLASS="Cn1ssChunkTools" +PROCESS_SCREENSHOTS_CLASS="ProcessScreenshots" +RENDER_SCREENSHOT_REPORT_CLASS="RenderScreenshotReport" +CN1SS_HELPER_SOURCE_DIR="$SCRIPT_DIR/android/tests" +if [ ! -f "$CN1SS_HELPER_SOURCE_DIR/$CN1SS_MAIN_CLASS.java" ]; then + ri_log "Missing CN1SS helper: $CN1SS_HELPER_SOURCE_DIR/$CN1SS_MAIN_CLASS.java" >&2 + exit 3 +fi + +source "$SCRIPT_DIR/lib/cn1ss.sh" +cn1ss_log() { ri_log "$1"; } + TMPDIR="${TMPDIR:-/tmp}"; TMPDIR="${TMPDIR%/}" DOWNLOAD_DIR="${TMPDIR}/codenameone-tools" ENV_DIR="$DOWNLOAD_DIR/tools" @@ -61,6 +75,8 @@ fi JAVA17_BIN="$JAVA17_HOME/bin/java" +cn1ss_setup "$JAVA17_BIN" "$CN1SS_HELPER_SOURCE_DIR" + ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$REPO_ROOT}/artifacts}" mkdir -p "$ARTIFACTS_DIR" TEST_LOG="$ARTIFACTS_DIR/xcodebuild-test.log" @@ -199,124 +215,154 @@ if ! xcodebuild \ exit 10 fi set +o pipefail +declare -a CN1SS_SOURCES=() +if [ -s "$TEST_LOG" ]; then + CN1SS_SOURCES+=("XCODELOG:$TEST_LOG") +else + ri_log "FATAL: Test log missing or empty at $TEST_LOG" + exit 11 +fi -# --- If no PNG files in $SCREENSHOT_RAW_DIR, export from the .xcresult attachments --- -if [ ! "$(find "$SCREENSHOT_RAW_DIR" -type f -name '*.png' -print -quit)" ] && [ -d "$RESULT_BUNDLE" ]; then - ri_log "No raw PNGs yet; exporting PNG attachments from $RESULT_BUNDLE" - - ATT_LIST="$SCREENSHOT_TMP_DIR/xcresult-attachments.txt" - - # Walk the result bundle and collect (attachment_id, file_name) - python3 - "$RESULT_BUNDLE" "$ATT_LIST" <<'PY' -import json, subprocess, sys, os, shlex - -bundle = sys.argv[1] -out_list = sys.argv[2] - -def norm_id(x): - # Accept strings or dicts like {"_value": "..."} or {"id": "..."} - if isinstance(x, dict): - v = x.get('_value') or x.get('id') or x.get('identifier') - if isinstance(v, dict): - v = v.get('_value') - return v - return x - -def xcget(obj_id=None): - # Xcode 16.4 wants --legacy even for "get object" - base = ["xcrun","xcresulttool","get","object","--legacy","--path",bundle,"--format","json"] - if obj_id: - base += ["--id", norm_id(obj_id)] - try: - out = subprocess.check_output(base) - return json.loads(out) - except subprocess.CalledProcessError as e: - # Last-ditch: try non-legacy (older toolchains) - alt = ["xcrun","xcresulttool","get","object","--path",bundle,"--format","json"] - if obj_id: - alt += ["--id", norm_id(obj_id)] - out = subprocess.check_output(alt) - return json.loads(out) - -def arr(node, key): - v = node.get(key) - if isinstance(v, dict) and "_values" in v: - return v["_values"] or [] - if isinstance(v, list): - return v - return [] - -def sval(node, key): - v = node.get(key) - if isinstance(v, dict) and "_value" in v: - return v["_value"] - if isinstance(v, str): - return v - return None - -def walk_tests(obj, hits): - for test in arr(obj, "tests"): - # Recurse into subtests - for st in arr(test, "subtests"): - walk_tests(st, hits) - # Activities → attachments - for act in arr(test, "activitySummaries"): - for att in arr(act, "attachments"): - if (att.get("_type",{}).get("_name") == "ActionTestAttachment"): - uti = sval(att, "uniformTypeIdentifier") or "" - name = sval(att, "filename") or "" - pref = (att.get("payloadRef") or {}).get("id") - if pref and (("png" in (uti or "").lower()) or ("jpeg" in (uti or "").lower()) or (name.lower().endswith((".png",".jpg",".jpeg")))): - if not name: name = "attachment.png" - hits.append((norm_id(pref), name)) - -root = xcget() -hits = [] - -# Dive: actions[] → actionResult.testsRef -> summaries -> testableSummaries -> tests... -for action in arr(root, "actions"): - result = action.get("actionResult") or {} - tests_id = norm_id(result.get("testsRef", {}).get("id")) - if not tests_id: - continue - plan_summaries = xcget(tests_id) # "ActionTestPlanRunSummaries" - for summ in arr(plan_summaries, "summaries"): - for testable in arr(summ, "testableSummaries"): - walk_tests(testable, hits) - -# De-dupe on id -seen=set() -with open(out_list, "w") as f: - for att_id, fname in hits: - if not att_id or att_id in seen: - continue - seen.add(att_id) - base, ext = os.path.splitext(fname) - if not ext: ext = ".png" - if not base: base = "attachment" - f.write(f"{att_id} {base}{ext}\n") -PY +LOG_CHUNKS="$(cn1ss_count_chunks "$TEST_LOG")"; LOG_CHUNKS="${LOG_CHUNKS//[^0-9]/}"; : "${LOG_CHUNKS:=0}" +ri_log "Chunk counts -> xcodebuild log: ${LOG_CHUNKS}" + +if [ "${LOG_CHUNKS:-0}" = "0" ]; then + ri_log "STAGE:MARKERS_NOT_FOUND -> xcodebuild output did not include CN1SS chunks" + ri_log "---- CN1SS lines (if any) ----" + (grep "CN1SS:" "$TEST_LOG" || true) | sed 's/^/[CN1SS] /' + exit 12 +fi - # Export each attachment id as a file - while IFS=$' \t' read -r ATT_ID FNAME; do - [ -n "$ATT_ID" ] || continue - OUT="$SCREENSHOT_RAW_DIR/$FNAME" - if [ -f "$OUT" ]; then - base="${FNAME%.*}"; ext="${FNAME##*.}" - n=2; while [ -f "$SCREENSHOT_RAW_DIR/${base}-${n}.${ext}" ]; do n=$((n+1)); done - OUT="$SCREENSHOT_RAW_DIR/${base}-${n}.${ext}" +TEST_NAMES_RAW="$(cn1ss_list_tests "$TEST_LOG" 2>/dev/null | awk 'NF' | sort -u || true)" +declare -a TEST_NAMES=() +if [ -n "$TEST_NAMES_RAW" ]; then + while IFS= read -r name; do + [ -n "$name" ] || continue + TEST_NAMES+=("$name") + done <<< "$TEST_NAMES_RAW" +else + TEST_NAMES+=("default") +fi +ri_log "Detected CN1SS test streams: ${TEST_NAMES[*]}" + +PAIR_SEP=$'\037' +declare -a TEST_OUTPUT_ENTRIES=() + +ensure_dir "$SCREENSHOT_PREVIEW_DIR" + +for test in "${TEST_NAMES[@]}"; do + dest="$SCREENSHOT_TMP_DIR/${test}.png" + if source_label="$(cn1ss_decode_test_png "$test" "$dest" "${CN1SS_SOURCES[@]}")"; then + TEST_OUTPUT_ENTRIES+=("${test}${PAIR_SEP}${dest}") + ri_log "Decoded screenshot for '$test' (source=${source_label}, size: $(cn1ss_file_size "$dest") bytes)" + preview_dest="$SCREENSHOT_PREVIEW_DIR/${test}.jpg" + if preview_source="$(cn1ss_decode_test_preview "$test" "$preview_dest" "${CN1SS_SOURCES[@]}")"; then + ri_log "Decoded preview for '$test' (source=${preview_source}, size: $(cn1ss_file_size "$preview_dest") bytes)" + else + rm -f "$preview_dest" 2>/dev/null || true + fi + else + ri_log "FATAL: Failed to extract/decode CN1SS payload for test '$test'" + RAW_B64_OUT="$SCREENSHOT_TMP_DIR/${test}.raw.b64" + { + for entry in "${CN1SS_SOURCES[@]}"; do + path="${entry#*:}" + [ -s "$path" ] || continue + count="$(cn1ss_count_chunks "$path" "$test")"; count="${count//[^0-9]/}"; : "${count:=0}" + if [ "$count" -gt 0 ]; then cn1ss_extract_base64 "$path" "$test"; fi + done + } > "$RAW_B64_OUT" 2>/dev/null || true + if [ -s "$RAW_B64_OUT" ]; then + head -c 64 "$RAW_B64_OUT" | sed 's/^/[CN1SS-B64-HEAD] /' + ri_log "Partial base64 saved at: $RAW_B64_OUT" fi - # Prefer --legacy on Xcode 16.4; fallback to non-legacy - if ! xcrun xcresulttool export --legacy --path "$RESULT_BUNDLE" --id "$ATT_ID" --output-path "$OUT" 2>/dev/null; then - xcrun xcresulttool export --path "$RESULT_BUNDLE" --id "$ATT_ID" --output-path "$OUT" || true + exit 12 + fi +done + +lookup_test_output() { + local key="$1" entry prefix + for entry in "${TEST_OUTPUT_ENTRIES[@]}"; do + prefix="${entry%%$PAIR_SEP*}" + if [ "$prefix" = "$key" ]; then + echo "${entry#*$PAIR_SEP}" + return 0 fi - [ -f "$OUT" ] && ri_log "Exported attachment -> $OUT" - done < "$ATT_LIST" + done + return 1 +} + +COMPARE_ARGS=() +for test in "${TEST_NAMES[@]}"; do + if dest="$(lookup_test_output "$test")"; then + [ -n "$dest" ] || continue + COMPARE_ARGS+=("--actual" "${test}=${dest}") + fi +done + +COMPARE_JSON="$SCREENSHOT_TMP_DIR/screenshot-compare.json" +export CN1SS_PREVIEW_DIR="$SCREENSHOT_PREVIEW_DIR" +ri_log "STAGE:COMPARE -> Evaluating screenshots against stored references" +if ! cn1ss_java_run "$PROCESS_SCREENSHOTS_CLASS" \ + --reference-dir "$SCREENSHOT_REF_DIR" \ + --emit-base64 \ + --preview-dir "$SCREENSHOT_PREVIEW_DIR" \ + "${COMPARE_ARGS[@]}" > "$COMPARE_JSON"; then + ri_log "FATAL: Screenshot comparison helper failed" + exit 13 +fi + +SUMMARY_FILE="$SCREENSHOT_TMP_DIR/screenshot-summary.txt" +COMMENT_FILE="$SCREENSHOT_TMP_DIR/screenshot-comment.md" + +ri_log "STAGE:COMMENT_BUILD -> Rendering summary and PR comment markdown" +if ! cn1ss_java_run "$RENDER_SCREENSHOT_REPORT_CLASS" \ + --compare-json "$COMPARE_JSON" \ + --comment-out "$COMMENT_FILE" \ + --summary-out "$SUMMARY_FILE"; then + ri_log "FATAL: Failed to render screenshot summary/comment" + exit 14 +fi + +if [ -s "$SUMMARY_FILE" ]; then + ri_log " -> Wrote summary entries to $SUMMARY_FILE ($(wc -l < "$SUMMARY_FILE" 2>/dev/null || echo 0) line(s))" +else + ri_log " -> No summary entries generated (all screenshots matched stored baselines)" +fi + +if [ -s "$COMMENT_FILE" ]; then + ri_log " -> Prepared PR comment payload at $COMMENT_FILE (bytes=$(wc -c < "$COMMENT_FILE" 2>/dev/null || echo 0))" +else + ri_log " -> No PR comment content produced" +fi + +if [ -s "$SUMMARY_FILE" ]; then + while IFS='|' read -r status test message copy_flag path preview_note; do + [ -n "${test:-}" ] || continue + ri_log "Test '${test}': ${message}" + if [ "$copy_flag" = "1" ] && [ -n "${path:-}" ] && [ -f "$path" ]; then + cp -f "$path" "$ARTIFACTS_DIR/${test}.png" 2>/dev/null || true + ri_log " -> Stored PNG artifact copy at $ARTIFACTS_DIR/${test}.png" + fi + if [ "$status" = "equal" ] && [ -n "${path:-}" ]; then + rm -f "$path" 2>/dev/null || true + fi + if [ -n "${preview_note:-}" ]; then + ri_log " Preview note: ${preview_note}" + fi + done < "$SUMMARY_FILE" +fi + +cp -f "$COMPARE_JSON" "$ARTIFACTS_DIR/screenshot-compare.json" 2>/dev/null || true +if [ -s "$COMMENT_FILE" ]; then + cp -f "$COMMENT_FILE" "$ARTIFACTS_DIR/screenshot-comment.md" 2>/dev/null || true +fi + +ri_log "STAGE:COMMENT_POST -> Submitting PR feedback" +comment_rc=0 +if ! cn1ss_post_pr_comment "$COMMENT_FILE" "$SCREENSHOT_PREVIEW_DIR"; then + comment_rc=$? fi -PNG_FILES=() -while IFS= read -r png; do - [ -n "$png" ] || continue - PNG_FILES+=("$png") -done < <(find "$SCREENSHOT_RAW_DIR" -type f -name '*.png' -print | sort) +exit $comment_rc