Skip to content

Commit 3c26601

Browse files
committed
Refine Android coverage workflow scripts
Revert "Use gradle stdout for CN1 screenshots" This reverts commit f29c6407f5243cd26188d9ec404fbe2a6f415b85. Use gradle stdout for CN1 screenshots Stop logcat capture before screenshot decoding Restore Android instrumentation test stdout semantics Reduce CN1SS chunk size and mirror logs Force plain console for Android Gradle run Fix Android screenshot logging and coverage class discovery Limit Android coverage scope and improve screenshot capture logging Fix Android preview publishing and comment rendering Filter instrumented classes from Android coverage and prefer preview attachments Fix Android screenshot previews and JaCoCo CLI deps Handle JaCoCo CLI jars without runnable manifests Improve Android coverage extraction reliability Improve Android coverage harvesting and reporting Inline Android screenshot previews and generate coverage HTML Relax screenshot decoding failure handling
1 parent 25f962b commit 3c26601

11 files changed

+669
-79
lines changed

.github/workflows/scripts-android.yml

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: Test Android build scripts
33

4-
'on':
4+
on:
55
pull_request:
66
paths:
77
- '.github/workflows/scripts-android.yml'
@@ -90,12 +90,86 @@ jobs:
9090
sudo udevadm trigger --name-match=kvm
9191
- name: Run Android instrumentation tests
9292
uses: reactivecircus/android-emulator-runner@v2
93+
env:
94+
CN1SS_SKIP_COMMENT: "1"
9395
with:
9496
api-level: 31
9597
arch: x86_64
9698
target: google_apis
9799
script: |
98100
./scripts/run-android-instrumentation-tests.sh "${{ steps.build-android-app.outputs.gradle_project_dir }}"
101+
- name: Collect Android coverage artifacts
102+
if: always()
103+
run: ./scripts/android/collect-android-coverage-artifacts.sh
104+
- name: Upload Android coverage artifacts
105+
if: always()
106+
uses: actions/upload-artifact@v4
107+
with:
108+
name: android-coverage-artifacts
109+
path: android-quality-artifacts
110+
if-no-files-found: ignore
111+
- name: Publish Android coverage preview
112+
if: ${{ always() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
113+
id: publish-android-coverage
114+
env:
115+
SERVER_URL: ${{ github.server_url }}
116+
REPOSITORY: ${{ github.repository }}
117+
RUN_ID: ${{ github.run_id }}
118+
RUN_ATTEMPT: ${{ github.run_attempt }}
119+
run: ./scripts/android/publish-android-coverage-preview.sh
120+
- name: Generate Android test report comment
121+
if: always()
122+
env:
123+
COVERAGE_HTML_URL: ${{ steps.publish-android-coverage.outputs.coverage_url }}
124+
ANDROID_PREVIEW_BASE_URL: ${{ steps.publish-android-coverage.outputs.preview_base }}
125+
run: python3 ./scripts/android/generate-android-report-comment.py
126+
- name: Upload Android report comment
127+
if: always()
128+
uses: actions/upload-artifact@v4
129+
with:
130+
name: android-report-comment
131+
path: android-comment.md
132+
- name: Publish Android test report comment
133+
if: ${{ github.event_name == 'pull_request' }}
134+
uses: actions/github-script@v7
135+
with:
136+
script: |
137+
const fs = require('fs');
138+
const marker = '<!-- CN1SS_ANDROID_COMMENT -->';
139+
const commentPath = 'android-comment.md';
140+
if (!fs.existsSync(commentPath)) {
141+
core.warning('android-comment.md was not generated.');
142+
return;
143+
}
144+
const body = fs.readFileSync(commentPath, 'utf8');
145+
if (!body.includes(marker)) {
146+
core.warning('Comment marker missing from android-comment.md.');
147+
return;
148+
}
149+
const { owner, repo } = context.repo;
150+
const issue_number = context.issue.number;
151+
const { data: comments } = await github.rest.issues.listComments({
152+
owner,
153+
repo,
154+
issue_number,
155+
per_page: 100,
156+
});
157+
const existing = comments.find(comment => comment.body && comment.body.includes(marker));
158+
if (existing) {
159+
await github.rest.issues.updateComment({
160+
owner,
161+
repo,
162+
comment_id: existing.id,
163+
body,
164+
});
165+
} else {
166+
await github.rest.issues.createComment({
167+
owner,
168+
repo,
169+
issue_number,
170+
body,
171+
});
172+
}
99173
- name: Upload emulator screenshot
100174
if: always() # still collect it if tests fail
101175
uses: actions/upload-artifact@v4
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ARTIFACT_ROOT="android-quality-artifacts/coverage"
5+
SOURCE_ROOT="artifacts/android-coverage"
6+
7+
mkdir -p "${ARTIFACT_ROOT}"
8+
9+
if [ -d "${SOURCE_ROOT}/site/jacoco" ]; then
10+
mkdir -p "${ARTIFACT_ROOT}/html"
11+
cp -R "${SOURCE_ROOT}/site/jacoco/." "${ARTIFACT_ROOT}/html/"
12+
fi
13+
14+
if [ -d "artifacts/android-previews" ]; then
15+
mkdir -p "${ARTIFACT_ROOT}/previews"
16+
cp -R "artifacts/android-previews/." "${ARTIFACT_ROOT}/previews/"
17+
fi
18+
19+
for file in coverage.ec coverage.json jacoco-report.log; do
20+
if [ -f "${SOURCE_ROOT}/${file}" ]; then
21+
cp "${SOURCE_ROOT}/${file}" "${ARTIFACT_ROOT}/"
22+
fi
23+
done
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import os
4+
import re
5+
from pathlib import Path
6+
7+
marker = "<!-- CN1SS_ANDROID_COMMENT -->"
8+
comment_lines = [marker, "### Android screenshot tests", ""]
9+
10+
screenshot_path = Path("artifacts/screenshot-comment.md")
11+
compare_path = Path("artifacts/screenshot-compare.json")
12+
preview_base = os.environ.get("ANDROID_PREVIEW_BASE_URL", "").strip()
13+
if preview_base.endswith("/"):
14+
preview_base = preview_base.rstrip("/")
15+
16+
attachment_payloads = {}
17+
if compare_path.is_file():
18+
try:
19+
compare_data = json.loads(compare_path.read_text())
20+
for item in compare_data.get("results", []):
21+
if not isinstance(item, dict):
22+
continue
23+
preview = item.get("preview") or {}
24+
preview_name = preview.get("name") or preview.get("path")
25+
if isinstance(preview_name, str) and "/" in preview_name:
26+
preview_name = preview_name.split("/")[-1]
27+
base64_data = item.get("base64")
28+
mime = item.get("base64_mime", "image/png")
29+
if isinstance(base64_data, str) and base64_data:
30+
if preview_name:
31+
attachment_payloads.setdefault(preview_name, (mime, base64_data))
32+
except json.JSONDecodeError:
33+
attachment_payloads = {}
34+
if screenshot_path.is_file():
35+
screenshot_text = screenshot_path.read_text().strip()
36+
screenshot_text = screenshot_text.replace("<!-- CN1SS_SCREENSHOT_COMMENT -->", "").strip()
37+
if not screenshot_text:
38+
screenshot_text = "✅ Native Android screenshot tests passed."
39+
else:
40+
screenshot_text = "✅ Native Android screenshot tests passed."
41+
42+
if screenshot_text:
43+
pattern = re.compile(r"\(attachment:([^)]+)\)")
44+
45+
def replace_attachment(match: re.Match) -> str:
46+
name = match.group(1)
47+
if preview_base:
48+
return f"({preview_base}/{name})"
49+
payload = attachment_payloads.get(name)
50+
if payload:
51+
mime, data = payload
52+
return f"(data:{mime};base64,{data})"
53+
return match.group(0)
54+
55+
screenshot_text = pattern.sub(replace_attachment, screenshot_text)
56+
57+
comment_lines.append(screenshot_text)
58+
comment_lines.append("")
59+
comment_lines.append("### Android coverage")
60+
comment_lines.append("")
61+
62+
coverage_section = "Coverage report was not generated."
63+
coverage_path = Path("artifacts/android-coverage/coverage.json")
64+
if coverage_path.is_file():
65+
data = json.loads(coverage_path.read_text())
66+
if data.get("available"):
67+
lines = data.get("lines", {})
68+
covered = lines.get("covered", 0)
69+
total = lines.get("total", 0)
70+
percent = lines.get("percent", 0.0)
71+
detail = f"{covered}/{total} lines" if total else "0/0 lines"
72+
coverage_html = os.environ.get("COVERAGE_HTML_URL", "")
73+
if coverage_html:
74+
coverage_section = f"- 📊 **Line coverage:** {percent:.2f}% ({detail}) ([HTML report]({coverage_html}))"
75+
else:
76+
coverage_section = f"- 📊 **Line coverage:** {percent:.2f}% ({detail})"
77+
else:
78+
note = data.get("note")
79+
if note:
80+
coverage_section = f"Coverage report is unavailable ({note})."
81+
else:
82+
coverage_section = "Coverage report is unavailable."
83+
84+
comment_lines.append(coverage_section)
85+
comment_lines.append("")
86+
87+
Path("android-comment.md").write_text("\n".join(comment_lines) + "\n")

scripts/android/lib/PatchGradleFiles.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ private static boolean patchAppBuildGradle(Path path, int compileSdk, int target
106106
content = r.content();
107107
changed |= r.changed();
108108

109+
r = ensureCoverageEnabled(content);
110+
content = r.content();
111+
changed |= r.changed();
112+
109113
if (changed) {
110114
Files.writeString(path, ensureTrailingNewline(content), StandardCharsets.UTF_8);
111115
}
@@ -247,6 +251,24 @@ private static Result ensureTestDependencies(String content) {
247251
return new Result(content + block, true);
248252
}
249253

254+
private static Result ensureCoverageEnabled(String content) {
255+
if (content.contains("testCoverageEnabled")) {
256+
return new Result(content, false);
257+
}
258+
StringBuilder builder = new StringBuilder(content);
259+
if (!content.endsWith("\n")) {
260+
builder.append('\n');
261+
}
262+
builder.append("android {\n")
263+
.append(" buildTypes {\n")
264+
.append(" debug {\n")
265+
.append(" testCoverageEnabled true\n")
266+
.append(" }\n")
267+
.append(" }\n")
268+
.append("}\n");
269+
return new Result(builder.toString(), true);
270+
}
271+
250272
private static String ensureTrailingNewline(String content) {
251273
return content.endsWith("\n") ? content : content + "\n";
252274
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
HTML_DIR="artifacts/android-coverage/site/jacoco"
5+
PREVIEW_DIR="artifacts/android-previews"
6+
7+
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
8+
echo "No coverage HTML report or screenshot previews generated; skipping preview publication."
9+
exit 0
10+
fi
11+
12+
tmp_dir=$(mktemp -d)
13+
run_dir="android-runs/${RUN_ID:-manual}-${RUN_ATTEMPT:-0}"
14+
dest_dir="${tmp_dir}/${run_dir}"
15+
mkdir -p "${dest_dir}"
16+
17+
if [ -d "${HTML_DIR}" ]; then
18+
mkdir -p "${dest_dir}/coverage"
19+
cp -R "${HTML_DIR}/." "${dest_dir}/coverage/"
20+
fi
21+
22+
if [ -d "${PREVIEW_DIR}" ]; then
23+
if find "${PREVIEW_DIR}" -type f \( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \) -print -quit >/dev/null; then
24+
mkdir -p "${dest_dir}/previews"
25+
cp -R "${PREVIEW_DIR}/." "${dest_dir}/previews/"
26+
fi
27+
fi
28+
29+
cat <<'README' > "${tmp_dir}/README.md"
30+
# Android quality previews
31+
32+
This branch is automatically managed by the Android CI workflow and may be force-pushed.
33+
README
34+
35+
git -C "${tmp_dir}" init -b previews >/dev/null
36+
git -C "${tmp_dir}" config user.name "github-actions[bot]"
37+
git -C "${tmp_dir}" config user.email "github-actions[bot]@users.noreply.github.com"
38+
git -C "${tmp_dir}" add .
39+
git -C "${tmp_dir}" commit -m "Publish Android quality previews for run ${RUN_ID} (attempt ${RUN_ATTEMPT})" >/dev/null
40+
41+
remote_url="${SERVER_URL}/${REPOSITORY}.git"
42+
token_remote_url="${remote_url/https:\/\//https://x-access-token:${GITHUB_TOKEN}@}"
43+
git -C "${tmp_dir}" push --force "${token_remote_url}" previews:quality-report-previews >/dev/null
44+
45+
commit_sha=$(git -C "${tmp_dir}" rev-parse HEAD)
46+
raw_base="https://raw.githubusercontent.com/${REPOSITORY}/${commit_sha}/${run_dir}"
47+
preview_base="https://htmlpreview.github.io/?${raw_base}"
48+
49+
if [ -d "${dest_dir}/coverage" ]; then
50+
echo "coverage_commit=${commit_sha}" >> "$GITHUB_OUTPUT"
51+
echo "coverage_url=${preview_base}/coverage/index.html" >> "$GITHUB_OUTPUT"
52+
fi
53+
54+
if [ -d "${dest_dir}/previews" ]; then
55+
echo "preview_base=${raw_base}/previews" >> "$GITHUB_OUTPUT"
56+
fi

scripts/android/tests/HelloCodenameOneInstrumentedTest.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
import org.junit.Assume;
2626
import org.junit.Test;
2727
import org.junit.runner.RunWith;
28+
import org.junit.AfterClass;
2829

2930
import java.io.ByteArrayOutputStream;
3031
import java.util.Locale;
3132
import java.util.concurrent.CountDownLatch;
3233
import java.util.concurrent.TimeUnit;
33-
import com.codename1.io.Log;
3434

3535
@RunWith(AndroidJUnit4.class)
3636
public class HelloCodenameOneInstrumentedTest {
@@ -45,6 +45,7 @@ public class HelloCodenameOneInstrumentedTest {
4545

4646
private static void println(String s) {
4747
System.out.println(s);
48+
android.util.Log.i("CN1SS", s);
4849
}
4950

5051
private static void settle(long millis) {
@@ -190,7 +191,7 @@ private static ScreenshotCapture captureScreenshot(ActivityScenario<Activity> sc
190191
}
191192
} catch (Throwable t) {
192193
println("CN1SS:ERR:test=" + testName + " " + t);
193-
Log.e(t);
194+
com.codename1.io.Log.e(t);
194195
} finally {
195196
latch.countDown();
196197
}
@@ -242,18 +243,22 @@ private static void emitScreenshotChannel(byte[] bytes, String testName, String
242243
for (int pos = 0; pos < b64.length(); pos += CHUNK_SIZE) {
243244
int end = Math.min(pos + CHUNK_SIZE, b64.length());
244245
String chunk = b64.substring(pos, end);
245-
System.out.println(
246+
String line =
246247
prefix
247248
+ ":"
248249
+ safeName
249250
+ ":"
250251
+ String.format(Locale.US, "%06d", pos)
251252
+ ":"
252-
+ chunk);
253+
+ chunk;
254+
System.out.println(line);
255+
android.util.Log.i("CN1SS", line);
253256
count++;
254257
}
255258
println("CN1SS:INFO:test=" + safeName + " chunks=" + count + " total_b64_len=" + b64.length());
256-
System.out.println(prefix + ":END:" + safeName);
259+
String endLine = prefix + ":END:" + safeName;
260+
System.out.println(endLine);
261+
android.util.Log.i("CN1SS", endLine);
257262
System.out.flush();
258263
}
259264

@@ -379,4 +384,10 @@ public void testBrowserComponentScreenshot() throws Exception {
379384

380385
emitScreenshot(capture, BROWSER_TEST);
381386
}
387+
388+
@AfterClass
389+
public static void suiteFinished() {
390+
println("CN1SS:SUITE:FINISHED");
391+
System.out.flush();
392+
}
382393
}

scripts/android/tests/ProcessScreenshots.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,22 @@ private static CommentPayload loadPreviewOrBuild(String testName, Path actualPat
113113

114114
private static CommentPayload loadExternalPreviewPayload(String testName, Path previewDir) throws IOException {
115115
String slug = slugify(testName);
116-
Path jpg = previewDir.resolve(slug + ".jpg");
117-
Path jpeg = previewDir.resolve(slug + ".jpeg");
118-
Path png = previewDir.resolve(slug + ".png");
116+
List<String> baseNames = new ArrayList<>();
117+
if (slug != null && !slug.isEmpty()) {
118+
baseNames.add(slug);
119+
}
120+
if (testName != null && !testName.isEmpty() && baseNames.stream().noneMatch(s -> s.equals(testName))) {
121+
baseNames.add(testName);
122+
}
119123
List<Path> candidates = new ArrayList<>();
120-
if (Files.exists(jpg)) candidates.add(jpg);
121-
if (Files.exists(jpeg)) candidates.add(jpeg);
122-
if (Files.exists(png)) candidates.add(png);
124+
for (String base : baseNames) {
125+
Path jpg = previewDir.resolve(base + ".jpg");
126+
Path jpeg = previewDir.resolve(base + ".jpeg");
127+
Path png = previewDir.resolve(base + ".png");
128+
if (Files.exists(jpg)) candidates.add(jpg);
129+
if (Files.exists(jpeg)) candidates.add(jpeg);
130+
if (Files.exists(png)) candidates.add(png);
131+
}
123132
if (candidates.isEmpty()) {
124133
return null;
125134
}

0 commit comments

Comments
 (0)