diff --git a/scripts/android/tests/Cn1ssChunkTools.java b/scripts/common/java/Cn1ssChunkTools.java similarity index 100% rename from scripts/android/tests/Cn1ssChunkTools.java rename to scripts/common/java/Cn1ssChunkTools.java diff --git a/scripts/android/tests/PostPrComment.java b/scripts/common/java/PostPrComment.java similarity index 100% rename from scripts/android/tests/PostPrComment.java rename to scripts/common/java/PostPrComment.java diff --git a/scripts/android/tests/ProcessScreenshots.java b/scripts/common/java/ProcessScreenshots.java similarity index 93% rename from scripts/android/tests/ProcessScreenshots.java rename to scripts/common/java/ProcessScreenshots.java index fc91793131..a0b941910e 100644 --- a/scripts/android/tests/ProcessScreenshots.java +++ b/scripts/common/java/ProcessScreenshots.java @@ -28,6 +28,8 @@ public class ProcessScreenshots { private static final byte[] PNG_SIGNATURE = new byte[]{ (byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }; + private static final int MAX_RETRIES = 5; + private static final long RETRY_DELAY_MS = 2000; public static void main(String[] args) throws Exception { Arguments arguments = Arguments.parse(args); @@ -71,8 +73,8 @@ static Map buildResults( } } else { try { - PNGImage actual = loadPng(actualPath); - PNGImage expected = loadPng(expectedPath); + PNGImage actual = loadPngWithRetry(actualPath); + PNGImage expected = loadPngWithRetry(expectedPath); Map outcome = compareImages(expected, actual); if (Boolean.TRUE.equals(outcome.get("equal"))) { record.put("status", "equal"); @@ -107,7 +109,7 @@ private static CommentPayload loadPreviewOrBuild(String testName, Path actualPat return external; } } - PNGImage image = cached != null ? cached : loadPng(actualPath); + PNGImage image = cached != null ? cached : loadPngWithRetry(actualPath); return buildCommentPayload(image, MAX_COMMENT_BASE64); } @@ -331,6 +333,58 @@ private static Map compareImages(PNGImage expected, PNGImage act return result; } + private static PNGImage loadPngWithRetry(Path path) throws IOException { + int attempt = 0; + long lastSize = -1; + while (true) { + try { + // Stabilize check: if file size is changing, wait + if (Files.exists(path)) { + long size = Files.size(path); + if (size != lastSize) { + lastSize = size; + if (attempt > 0) { + // If size changed, we should wait and retry + Thread.sleep(RETRY_DELAY_MS); + attempt++; + if (attempt >= MAX_RETRIES) { + break; // fall through to try loading anyway, will likely fail + } + continue; + } + } + } + + return loadPng(path); + } catch (IOException e) { + // Only retry on truncated chunk or premature end of file + if (e.getMessage() != null && + (e.getMessage().contains("PNG chunk truncated") || + e.getMessage().contains("Premature end of file") || + e.getMessage().contains("Missing IHDR"))) { + + attempt++; + if (attempt >= MAX_RETRIES) { + throw e; + } + try { + System.err.println("Retrying load of " + path + " (attempt " + (attempt + 1) + "/" + MAX_RETRIES + "): " + e.getMessage()); + Thread.sleep(RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting to retry load of " + path, ie); + } + } else { + throw e; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while loading " + path, e); + } + } + return loadPng(path); // Final attempt + } + private static PNGImage loadPng(Path path) throws IOException { byte[] data = Files.readAllBytes(path); for (int i = 0; i < PNG_SIGNATURE.length; i++) { @@ -984,4 +1038,3 @@ private void skipWhitespace() { } } } - diff --git a/scripts/android/tests/RenderScreenshotReport.java b/scripts/common/java/RenderScreenshotReport.java similarity index 100% rename from scripts/android/tests/RenderScreenshotReport.java rename to scripts/common/java/RenderScreenshotReport.java diff --git a/scripts/lib/cn1ss.sh b/scripts/lib/cn1ss.sh index f1e9e99881..b55f78b56f 100644 --- a/scripts/lib/cn1ss.sh +++ b/scripts/lib/cn1ss.sh @@ -28,11 +28,18 @@ cn1ss_setup() { CN1SS_SOURCE_PATH="$2" local cache_override="${3:-}" tmp_root + if [ -z "$CN1SS_SOURCE_PATH" ]; then + # Default to common/java if not provided or empty + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + CN1SS_SOURCE_PATH="$script_dir/common/java" + fi + 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 + if [ ! -d "$CN1SS_SOURCE_PATH" ]; then cn1ss_log "CN1SS setup failed: source directory missing ($CN1SS_SOURCE_PATH)" return 1 fi @@ -305,3 +312,94 @@ cn1ss_post_pr_comment() { fi return $rc } + +# Shared function to generate report, compare screenshots, and post PR comment +cn1ss_process_and_report() { + local platform_title="$1" + local compare_json_out="$2" + local summary_out="$3" + local comment_out="$4" + local ref_dir="$5" + local preview_dir="$6" + local artifacts_dir="$7" + # Optional: array of actual entries in format "testName=path" + shift 7 + local actual_entries=("$@") + + local rc=0 + + # Run ProcessScreenshots + local -a compare_args=("--reference-dir" "$ref_dir" "--emit-base64" "--preview-dir" "$preview_dir") + for entry in "${actual_entries[@]}"; do + compare_args+=("--actual" "$entry") + done + + cn1ss_log "STAGE:COMPARE -> Evaluating screenshots against stored references" + if ! cn1ss_java_run "$CN1SS_PROCESS_CLASS" "${compare_args[@]}" > "$compare_json_out"; then + cn1ss_log "FATAL: Screenshot comparison helper failed" + return 13 + fi + + # Run RenderScreenshotReport + cn1ss_log "STAGE:COMMENT_BUILD -> Rendering summary and PR comment markdown" + local -a render_args=( + --title "$platform_title" + --compare-json "$compare_json_out" + --comment-out "$comment_out" + --summary-out "$summary_out" + ) + if [ -n "${CN1SS_COVERAGE_SUMMARY:-}" ]; then + render_args+=(--coverage-summary "$CN1SS_COVERAGE_SUMMARY") + fi + if [ -n "${CN1SS_COVERAGE_HTML_URL:-}" ]; then + render_args+=(--coverage-html-url "$CN1SS_COVERAGE_HTML_URL") + fi + + if ! cn1ss_java_run "$CN1SS_RENDER_CLASS" "${render_args[@]}"; then + cn1ss_log "FATAL: Failed to render screenshot summary/comment" + return 14 + fi + + if [ -s "$summary_out" ]; then + cn1ss_log " -> Wrote summary entries to $summary_out ($(wc -l < "$summary_out" 2>/dev/null || echo 0) line(s))" + else + cn1ss_log " -> No summary entries generated (all screenshots matched stored baselines)" + fi + + if [ -s "$comment_out" ]; then + cn1ss_log " -> Prepared PR comment payload at $comment_out (bytes=$(wc -c < "$comment_out" 2>/dev/null || echo 0))" + else + cn1ss_log " -> No PR comment content produced" + fi + + # Process summary entries (copy artifacts, clean up) + if [ -s "$summary_out" ]; then + while IFS='|' read -r status test message copy_flag path preview_note; do + [ -n "${test:-}" ] || continue + cn1ss_log "Test '${test}': ${message}" + if [ "$copy_flag" = "1" ] && [ -n "${path:-}" ] && [ -f "$path" ]; then + cp -f "$path" "$artifacts_dir/${test}.png" 2>/dev/null || true + cn1ss_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 + cn1ss_log " Preview note: ${preview_note}" + fi + done < "$summary_out" + fi + + cp -f "$compare_json_out" "$artifacts_dir/screenshot-compare.json" 2>/dev/null || true + if [ -s "$comment_out" ]; then + cp -f "$comment_out" "$artifacts_dir/screenshot-comment.md" 2>/dev/null || true + fi + + cn1ss_log "STAGE:COMMENT_POST -> Submitting PR feedback" + local comment_rc=0 + if ! cn1ss_post_pr_comment "$comment_out" "$preview_dir"; then + comment_rc=$? + fi + + return $comment_rc +} diff --git a/scripts/run-android-instrumentation-tests.sh b/scripts/run-android-instrumentation-tests.sh index c6a278163d..18811f0c42 100755 --- a/scripts/run-android-instrumentation-tests.sh +++ b/scripts/run-android-instrumentation-tests.sh @@ -9,9 +9,7 @@ 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_MAIN_CLASS="Cn1ssChunkTools" -PROCESS_SCREENSHOTS_CLASS="ProcessScreenshots" -RENDER_SCREENSHOT_REPORT_CLASS="RenderScreenshotReport" +# (Defaults for class names are provided by cn1ss.sh) # ---- Args & environment ---------------------------------------------------- @@ -25,13 +23,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" -CN1SS_HELPER_SOURCE_DIR="$SCRIPT_DIR/android/tests" +CN1SS_HELPER_SOURCE_DIR="$SCRIPT_DIR/common/java" +source "$SCRIPT_DIR/lib/cn1ss.sh" + 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%/}" @@ -220,86 +218,37 @@ done # ---- Compare against stored references ------------------------------------ -COMPARE_ARGS=() +COMPARE_ENTRIES=() for test in "${TEST_NAMES[@]}"; do dest="${TEST_OUTPUTS[$test]:-}" [ -n "$dest" ] || continue - COMPARE_ARGS+=("--actual" "${test}=${dest}") + COMPARE_ENTRIES+=("${test}=${dest}") 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" -if ! cn1ss_java_run "$PROCESS_SCREENSHOTS_CLASS" \ - --reference-dir "$SCREENSHOT_REF_DIR" \ - --emit-base64 \ - --preview-dir "$SCREENSHOT_PREVIEW_DIR" \ - "${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" -render_args=( - --compare-json "$COMPARE_JSON" - --comment-out "$COMMENT_FILE" - --summary-out "$SUMMARY_FILE" - --coverage-summary "$COVERAGE_SUMMARY" -) -if [ -n "${ANDROID_COVERAGE_HTML_URL:-}" ]; then - render_args+=(--coverage-html-url "${ANDROID_COVERAGE_HTML_URL}") -fi -if ! cn1ss_java_run "$RENDER_SCREENSHOT_REPORT_CLASS" "${render_args[@]}"; 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))" -else - ra_log " -> No summary entries generated (all screenshots matched stored baselines)" -fi - -if [ -s "$COMMENT_FILE" ]; then - ra_log " -> Prepared PR comment payload at $COMMENT_FILE (bytes=$(wc -c < "$COMMENT_FILE" 2>/dev/null || echo 0))" -else - ra_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 - ra_log "Test '${test}': ${message}" - if [ "$copy_flag" = "1" ] && [ -n "${path:-}" ] && [ -f "$path" ]; then - cp -f "$path" "$ARTIFACTS_DIR/${test}.png" 2>/dev/null || true - ra_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 - ra_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 - -ra_log "STAGE:COMMENT_POST -> Submitting PR feedback" -comment_rc=0 +export CN1SS_PREVIEW_DIR="$SCREENSHOT_PREVIEW_DIR" export CN1SS_COMMENT_MARKER="" export CN1SS_COMMENT_LOG_PREFIX="[run-android-device-tests]" export CN1SS_PREVIEW_SUBDIR="android" -if ! cn1ss_post_pr_comment "$COMMENT_FILE" "$SCREENSHOT_PREVIEW_DIR"; then - comment_rc=$? +export CN1SS_COVERAGE_SUMMARY="$COVERAGE_SUMMARY" +if [ -n "${ANDROID_COVERAGE_HTML_URL:-}" ]; then + export CN1SS_COVERAGE_HTML_URL="${ANDROID_COVERAGE_HTML_URL}" fi +cn1ss_process_and_report \ + "Android screenshot updates" \ + "$COMPARE_JSON" \ + "$SUMMARY_FILE" \ + "$COMMENT_FILE" \ + "$SCREENSHOT_REF_DIR" \ + "$SCREENSHOT_PREVIEW_DIR" \ + "$ARTIFACTS_DIR" \ + "${COMPARE_ENTRIES[@]}" +comment_rc=$? + # Copy useful artifacts for GH Actions cp -f "$TEST_LOG" "$ARTIFACTS_DIR/device-runner-logcat.txt" 2>/dev/null || true [ -n "${TEST_EXEC_LOG:-}" ] && cp -f "$TEST_EXEC_LOG" "$ARTIFACTS_DIR/test-results.log" 2>/dev/null || true diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index d49dae0a17..5eb5f3e959 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -34,16 +34,13 @@ 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" +CN1SS_HELPER_SOURCE_DIR="$SCRIPT_DIR/common/java" +source "$SCRIPT_DIR/lib/cn1ss.sh" + 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%/}" @@ -598,84 +595,36 @@ lookup_test_output() { return 1 } -COMPARE_ARGS=() +COMPARE_ENTRIES=() for test in "${TEST_NAMES[@]}"; do if dest="$(lookup_test_output "$test")"; then [ -n "$dest" ] || continue - COMPARE_ARGS+=("--actual" "${test}=${dest}") + COMPARE_ENTRIES+=("${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" \ - --title "iOS screenshot updates" \ - --success-message "✅ Native iOS screenshot tests passed." \ - --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 +export CN1SS_PREVIEW_DIR="$SCREENSHOT_PREVIEW_DIR" +export CN1SS_COMMENT_MARKER="" +export CN1SS_COMMENT_LOG_PREFIX="[run-ios-device-tests]" +export CN1SS_PREVIEW_SUBDIR="ios" -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 +cn1ss_process_and_report \ + "iOS screenshot updates" \ + "$COMPARE_JSON" \ + "$SUMMARY_FILE" \ + "$COMMENT_FILE" \ + "$SCREENSHOT_REF_DIR" \ + "$SCREENSHOT_PREVIEW_DIR" \ + "$ARTIFACTS_DIR" \ + "${COMPARE_ENTRIES[@]}" +comment_rc=$? -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 cp -f "$BUILD_LOG" "$ARTIFACTS_DIR/xcodebuild-build.log" 2>/dev/null || true cp -f "$TEST_LOG" "$ARTIFACTS_DIR/device-runner.log" 2>/dev/null || true -ri_log "STAGE:COMMENT_POST -> Submitting PR feedback" -comment_rc=0 -export CN1SS_COMMENT_MARKER="" -export CN1SS_COMMENT_LOG_PREFIX="[run-ios-device-tests]" -export CN1SS_PREVIEW_SUBDIR="ios" -if ! cn1ss_post_pr_comment "$COMMENT_FILE" "$SCREENSHOT_PREVIEW_DIR"; then - comment_rc=$? -fi - exit $comment_rc