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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion .github/workflows/scripts-android.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: Test Android build scripts

'on':
on:
pull_request:
paths:
- '.github/workflows/scripts-android.yml'
Expand Down Expand Up @@ -90,12 +90,87 @@ jobs:
sudo udevadm trigger --name-match=kvm
- name: Run Android instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
env:
CN1SS_SKIP_COMMENT: "1"
RUN_CONNECTED_TESTS: "1"
with:
api-level: 31
arch: x86_64
target: google_apis
script: |
./scripts/run-android-instrumentation-tests.sh "${{ steps.build-android-app.outputs.gradle_project_dir }}"
- name: Collect Android coverage artifacts
if: always()
run: ./scripts/android/collect-android-coverage-artifacts.sh
- name: Upload Android coverage artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: android-coverage-artifacts
path: android-quality-artifacts
if-no-files-found: ignore
- name: Publish Android coverage preview
if: ${{ always() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
id: publish-android-coverage
env:
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: ./scripts/android/publish-android-coverage-preview.sh
- name: Generate Android test report comment
if: always()
env:
COVERAGE_HTML_URL: ${{ steps.publish-android-coverage.outputs.coverage_url }}
ANDROID_PREVIEW_BASE_URL: ${{ steps.publish-android-coverage.outputs.preview_base }}
run: python3 ./scripts/android/generate-android-report-comment.py
- name: Upload Android report comment
if: always()
uses: actions/upload-artifact@v4
with:
name: android-report-comment
path: android-comment.md
- name: Publish Android test report comment
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const marker = '<!-- CN1SS_ANDROID_COMMENT -->';
const commentPath = 'android-comment.md';
if (!fs.existsSync(commentPath)) {
core.warning('android-comment.md was not generated.');
return;
}
const body = fs.readFileSync(commentPath, 'utf8');
if (!body.includes(marker)) {
core.warning('Comment marker missing from android-comment.md.');
return;
}
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find(comment => comment.body && comment.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
- name: Upload emulator screenshot
if: always() # still collect it if tests fail
uses: actions/upload-artifact@v4
Expand Down
23 changes: 23 additions & 0 deletions scripts/android/collect-android-coverage-artifacts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail

ARTIFACT_ROOT="android-quality-artifacts/coverage"
SOURCE_ROOT="artifacts/android-coverage"

mkdir -p "${ARTIFACT_ROOT}"

if [ -d "${SOURCE_ROOT}/site/jacoco" ]; then
mkdir -p "${ARTIFACT_ROOT}/html"
cp -R "${SOURCE_ROOT}/site/jacoco/." "${ARTIFACT_ROOT}/html/"
fi

if [ -d "artifacts/android-previews" ]; then
mkdir -p "${ARTIFACT_ROOT}/previews"
cp -R "artifacts/android-previews/." "${ARTIFACT_ROOT}/previews/"
fi

for file in coverage.ec coverage.json jacoco-report.log; do
if [ -f "${SOURCE_ROOT}/${file}" ]; then
cp "${SOURCE_ROOT}/${file}" "${ARTIFACT_ROOT}/"
fi
done
74 changes: 74 additions & 0 deletions scripts/android/generate-android-report-comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import json
import os
import re
from pathlib import Path

marker = "<!-- CN1SS_ANDROID_COMMENT -->"
comment_lines = [marker, "### Android screenshot tests", ""]

screenshot_path = Path("artifacts/screenshot-comment.md")

# This is set by publish-android-coverage-preview.sh via workflow outputs
preview_base = os.environ.get("ANDROID_PREVIEW_BASE_URL", "").strip()
if preview_base.endswith("/"):
preview_base = preview_base.rstrip("/")

# ---- 1) Screenshot section ----
if screenshot_path.is_file():
screenshot_text = screenshot_path.read_text().strip()
# Strip the inner screenshot marker if present
screenshot_text = screenshot_text.replace("<!-- CN1SS_SCREENSHOT_COMMENT -->", "").strip()
if not screenshot_text:
screenshot_text = "✅ Native Android screenshot tests passed."
else:
screenshot_text = "✅ Native Android screenshot tests passed."

# If we have a preview_base, replace (attachment:foo.jpg) with a raw.githubusercontent.com URL
if screenshot_text and preview_base:
pattern = re.compile(r"\(attachment:([^)]+)\)")

def replace_attachment(match: re.Match) -> str:
name = match.group(1).strip()
# Result: (https://raw.githubusercontent.com/.../android-runs/.../previews/name)
return f"({preview_base}/{name})"

screenshot_text = pattern.sub(replace_attachment, screenshot_text)

comment_lines.append(screenshot_text)
comment_lines.append("")
comment_lines.append("### Android coverage")
comment_lines.append("")

# ---- 2) Coverage section ----
coverage_section = "Coverage report was not generated."
coverage_path = Path("artifacts/android-coverage/coverage.json")

if coverage_path.is_file():
data = json.loads(coverage_path.read_text())
if data.get("available"):
lines = data.get("lines", {})
covered = lines.get("covered", 0)
total = lines.get("total", 0)
percent = data.get("percent", 0.0)
detail = f"{covered}/{total} lines" if total else "0/0 lines"

coverage_html = os.environ.get("COVERAGE_HTML_URL", "").strip()
if coverage_html:
coverage_section = (
f"- 📊 **Line coverage:** {percent:.2f}% ({detail}) "
f"([HTML report]({coverage_html}))"
)
else:
coverage_section = f"- 📊 **Line coverage:** {percent:.2f}% ({detail})"
else:
note = data.get("note")
if note:
coverage_section = f"Coverage report is unavailable ({note})."
else:
coverage_section = "Coverage report is unavailable."

comment_lines.append(coverage_section)
comment_lines.append("")

Path("android-comment.md").write_text("\n".join(comment_lines) + "\n")
22 changes: 22 additions & 0 deletions scripts/android/lib/PatchGradleFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ private static boolean patchAppBuildGradle(Path path, int compileSdk, int target
content = r.content();
changed |= r.changed();

r = ensureCoverageEnabled(content);
content = r.content();
changed |= r.changed();

if (changed) {
Files.writeString(path, ensureTrailingNewline(content), StandardCharsets.UTF_8);
}
Expand Down Expand Up @@ -247,6 +251,24 @@ private static Result ensureTestDependencies(String content) {
return new Result(content + block, true);
}

private static Result ensureCoverageEnabled(String content) {
if (content.contains("testCoverageEnabled")) {
return new Result(content, false);
}
StringBuilder builder = new StringBuilder(content);
if (!content.endsWith("\n")) {
builder.append('\n');
}
builder.append("android {\n")
.append(" buildTypes {\n")
.append(" debug {\n")
.append(" testCoverageEnabled true\n")
.append(" }\n")
.append(" }\n")
.append("}\n");
return new Result(builder.toString(), true);
}

private static String ensureTrailingNewline(String content) {
return content.endsWith("\n") ? content : content + "\n";
}
Expand Down
56 changes: 56 additions & 0 deletions scripts/android/publish-android-coverage-preview.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail

HTML_DIR="artifacts/android-coverage/site/jacoco"
PREVIEW_DIR="artifacts/android-previews"

if [ ! -d "${HTML_DIR}" ] && { [ ! -d "${PREVIEW_DIR}" ] || ! find "${PREVIEW_DIR}" -type f \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) -print -quit >/dev/null; }; then
echo "No coverage HTML report or screenshot previews generated; skipping preview publication."
exit 0
fi

tmp_dir=$(mktemp -d)
run_dir="android-runs/${RUN_ID:-manual}-${RUN_ATTEMPT:-0}"
dest_dir="${tmp_dir}/${run_dir}"
mkdir -p "${dest_dir}"

if [ -d "${HTML_DIR}" ]; then
mkdir -p "${dest_dir}/coverage"
cp -R "${HTML_DIR}/." "${dest_dir}/coverage/"
fi

if [ -d "${PREVIEW_DIR}" ]; then
if find "${PREVIEW_DIR}" -type f \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) -print -quit >/dev/null; then
mkdir -p "${dest_dir}/previews"
cp -R "${PREVIEW_DIR}/." "${dest_dir}/previews/"
fi
fi

cat <<'README' > "${tmp_dir}/README.md"
# Android quality previews

This branch is automatically managed by the Android CI workflow and may be force-pushed.
README

git -C "${tmp_dir}" init -b previews >/dev/null
git -C "${tmp_dir}" config user.name "github-actions[bot]"
git -C "${tmp_dir}" config user.email "github-actions[bot]@users.noreply.github.com"
git -C "${tmp_dir}" add .
git -C "${tmp_dir}" commit -m "Publish Android quality previews for run ${RUN_ID} (attempt ${RUN_ATTEMPT})" >/dev/null

remote_url="${SERVER_URL}/${REPOSITORY}.git"
token_remote_url="${remote_url/https:\/\//https://x-access-token:${GITHUB_TOKEN}@}"
git -C "${tmp_dir}" push --force "${token_remote_url}" previews:quality-report-previews >/dev/null

commit_sha=$(git -C "${tmp_dir}" rev-parse HEAD)
raw_base="https://raw.githubusercontent.com/${REPOSITORY}/${commit_sha}/${run_dir}"
preview_base="https://htmlpreview.github.io/?${raw_base}"

if [ -d "${dest_dir}/coverage" ]; then
echo "coverage_commit=${commit_sha}" >> "$GITHUB_OUTPUT"
echo "coverage_url=${preview_base}/coverage/index.html" >> "$GITHUB_OUTPUT"
fi

if [ -d "${dest_dir}/previews" ]; then
echo "preview_base=${raw_base}/previews" >> "$GITHUB_OUTPUT"
fi
26 changes: 20 additions & 6 deletions scripts/android/tests/Cn1ssChunkTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,30 @@ private static void runExtract(String[] args) throws IOException {
if (path == null) {
throw new IllegalArgumentException("Path is required for extract");
}
if (path == null) {
throw new IllegalArgumentException("Path is required for extract");
}
String targetTest = test != null ? test : DEFAULT_TEST_NAME;
List<Chunk> chunks = new ArrayList<>();
for (Chunk chunk : iterateChunks(path, Optional.ofNullable(targetTest), Optional.ofNullable(channel))) {
chunks.add(chunk);

// Collect chunks by index so that if the same test/channel was emitted multiple
// times (e.g. test re-runs), only the last payload for a given index is kept.
java.util.Map<Integer, String> byIndex = new java.util.HashMap<>();
for (Chunk chunk : iterateChunks(path,
Optional.ofNullable(targetTest),
Optional.ofNullable(channel))) {
// For duplicate indices, later entries overwrite earlier ones.
byIndex.put(chunk.index, chunk.payload);
}
Collections.sort(chunks);

// Rebuild the base64 payload in ascending index order
List<Integer> indices = new ArrayList<>(byIndex.keySet());
Collections.sort(indices);

StringBuilder payload = new StringBuilder();
for (Chunk chunk : chunks) {
payload.append(chunk.payload);
for (int idx : indices) {
payload.append(byIndex.get(idx));
}

if (decode) {
byte[] data;
try {
Expand Down
20 changes: 14 additions & 6 deletions scripts/android/tests/HelloCodenameOneInstrumentedTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.AfterClass;

import java.io.ByteArrayOutputStream;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.codename1.io.Log;

@RunWith(AndroidJUnit4.class)
public class HelloCodenameOneInstrumentedTest {

private static final int CHUNK_SIZE = 2000;
private static final int CHUNK_SIZE = 768;
private static final String PREVIEW_CHANNEL = "PREVIEW";
private static final int[] PREVIEW_JPEG_QUALITIES =
new int[] {60, 50, 40, 35, 30, 25, 20, 18, 16, 14, 12, 10, 8, 6, 5, 4, 3, 2, 1};
Expand Down Expand Up @@ -190,7 +190,7 @@ private static ScreenshotCapture captureScreenshot(ActivityScenario<Activity> sc
}
} catch (Throwable t) {
println("CN1SS:ERR:test=" + testName + " " + t);
Log.e(t);
com.codename1.io.Log.e(t);
} finally {
latch.countDown();
}
Expand Down Expand Up @@ -242,18 +242,20 @@ private static void emitScreenshotChannel(byte[] bytes, String testName, String
for (int pos = 0; pos < b64.length(); pos += CHUNK_SIZE) {
int end = Math.min(pos + CHUNK_SIZE, b64.length());
String chunk = b64.substring(pos, end);
System.out.println(
String line =
prefix
+ ":"
+ safeName
+ ":"
+ String.format(Locale.US, "%06d", pos)
+ ":"
+ chunk);
+ chunk;
System.out.println(line);
count++;
}
println("CN1SS:INFO:test=" + safeName + " chunks=" + count + " total_b64_len=" + b64.length());
System.out.println(prefix + ":END:" + safeName);
String endLine = prefix + ":END:" + safeName;
System.out.println(endLine);
System.out.flush();
}

Expand Down Expand Up @@ -379,4 +381,10 @@ public void testBrowserComponentScreenshot() throws Exception {

emitScreenshot(capture, BROWSER_TEST);
}

@AfterClass
public static void suiteFinished() {
println("CN1SS:SUITE:FINISHED");
System.out.flush();
}
}
Loading
Loading