CI on OxygenFlow: unit tests with JUnit and JaCoCo coverage, and a debug build, offline (closes #19) 8a657957d5a0
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key e6d3226a2568
- When
- · 2026-10-26 17:49 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
8a657957d5a0e692af9e4b1d355099363690961ad79f3e1ea8b0b1c18fbd862f- Depends on
- 0a2858c0dad3 57c0824300db 62429abe5869 9481bfa7c5d0 e0e4a1c48a77 f821ee741b9f
- Depended on by
- c1b5d82f16cd bd1d3ad70c70 b610bc6eeab7 4c0d52760d2f bfa2eff05d35 26530d9758ee 9af639e2ea06 03f2c0361594
- In bookmarks
- main
- In tags
- v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9
- Edits
- 7 atoms, 1 tree operation, 5 files
Browse the tree at this change
added scripts/ci.sh +31 −0
| 1 | + | #!/usr/bin/env bash |
|
| 2 | + | # The app's CI job (#19), in OxygenFlow's Android image with no network: |
|
| 3 | + | # unit tests with coverage and a debug build. Not the screenshot tests: |
|
| 4 | + | # their references change with the clock, so are not kept, and CI would |
|
| 5 | + | # have nothing to compare a screen with. Gradle runs every task even when |
|
| 6 | + | # one fails (--continue), so the reports are written either way: |
|
| 7 | + | # junit.xml every unit test class's JUnit XML in one <testsuites> |
|
| 8 | + | # coverage.xml JaCoCo XML of the unit tests, for the Coverage tab |
|
| 9 | + | set -u |
|
| 10 | + | cd "$(dirname "$0")/.." |
|
| 11 | + | status=0 |
|
| 12 | + | gradle --offline --no-daemon --continue \ |
|
| 13 | + | :app:createDebugUnitTestCoverageReport \ |
|
| 14 | + | :app:assembleDebug || status=$? |
|
| 15 | + | ||
| 16 | + | { |
|
| 17 | + | echo '<?xml version="1.0" encoding="UTF-8"?>' |
|
| 18 | + | echo '<testsuites>' |
|
| 19 | + | for f in app/build/test-results/testDebugUnitTest/TEST-*.xml; do |
|
| 20 | + | [ -f "$f" ] && sed '/^<?xml /d' "$f" |
|
| 21 | + | done |
|
| 22 | + | echo '</testsuites>' |
|
| 23 | + | } > junit.xml |
|
| 24 | + | report=app/build/reports/coverage/test/debug/report.xml |
|
| 25 | + | if [ -f "$report" ]; then |
|
| 26 | + | cp "$report" coverage.xml |
|
| 27 | + | else |
|
| 28 | + | echo "ci: no coverage report at $report" >&2 |
|
| 29 | + | [ "$status" -ne 0 ] || status=1 |
|
| 30 | + | fi |
|
| 31 | + | exit "$status" |
modified .flow/pipeline.kdl +49 −5
| 1 | + | // The OxygenHub Android app's pipeline (OxygenFlow, #19). Every push runs |
|
| 2 | + | // the unit tests with coverage and a debug build in OxygenFlow's Android |
|
| 3 | + | // image, with no network: the JDK, the Android SDK and NDK, Gradle, |
|
| 4 | + | // cargo-ndk, rust-core's crates and the app's Gradle dependencies are all |
|
| 5 | + | // in the image (oxygenflow/images/android). Rebuild and re-pin it when the |
|
| 6 | + | // app's SDK, Gradle or dependency versions change, or rust-core's |
|
| 7 | + | // Cargo.lock does. Locally, ./gradlew :app:createDebugUnitTestCoverageReport |
|
| 8 | + | // runs the same tests. |
|
| 1 | 9 | schema-version 1 |
|
| 2 | 10 | ||
| 3 | - | // No jobs yet (oxygenhub-android#1): the app needs OxygenFlow's Android |
|
| 4 | - | // image (oxygenflow#19: JDK, the Android SDK and NDK, cargo-ndk, Gradle's |
|
| 5 | - | // dependencies and rust-core's crates, all offline). Until it is built and |
|
| 6 | - | // pinned, the app is built and tested locally: |
|
| 7 | - | // ./gradlew :app:assembleDebug :app:testDebugUnitTest |
|
| 11 | + | sandbox "android" { |
|
| 12 | + | runtime "microvm" |
|
| 13 | + | rootfs "cas://blob/cceb10ea42f539bed31c4bbe16e4f49961533f80a2397ca4981afb483a066d0a" |
|
| 14 | + | memory-limit "8GiB" |
|
| 15 | + | cpu-limit 4 |
|
| 16 | + | network "isolated" |
|
| 17 | + | // Gradle reads dependencies from the image's read-only cache and |
|
| 18 | + | // writes its own state under /tmp. |
|
| 19 | + | env { |
|
| 20 | + | ANDROID_HOME "/opt/android-sdk" |
|
| 21 | + | JAVA_HOME "/usr/lib/jvm/java-17-openjdk-amd64" |
|
| 22 | + | GRADLE_RO_DEP_CACHE "/opt/gradle-home/caches" |
|
| 23 | + | GRADLE_USER_HOME "/tmp/gradle" |
|
| 24 | + | CARGO_INCREMENTAL "0" |
|
| 25 | + | } |
|
| 26 | + | } |
|
| 27 | + | ||
| 28 | + | job "test" { |
|
| 29 | + | sandbox "android" |
|
| 30 | + | timeout "60m" |
|
| 31 | + | // rust-core (with oxy-core and the runner) for two ABIs is most of the |
|
| 32 | + | // build: kept from the last passing run, so only what changed builds. |
|
| 33 | + | mount { |
|
| 34 | + | source "ephemeral://disk" |
|
| 35 | + | target "/src/rust-core/target" |
|
| 36 | + | size "20GiB" |
|
| 37 | + | cache "rust-core-target" |
|
| 38 | + | key "rust-core/Cargo.lock" |
|
| 39 | + | } |
|
| 40 | + | exec "bash" "scripts/ci.sh" |
|
| 41 | + | output { |
|
| 42 | + | artifact "junit" { |
|
| 43 | + | path "/src/junit.xml" |
|
| 44 | + | when "always" |
|
| 45 | + | } |
|
| 46 | + | artifact "coverage" { |
|
| 47 | + | path "/src/coverage.xml" |
|
| 48 | + | when "always" |
|
| 49 | + | } |
|
| 50 | + | } |
|
| 51 | + | } |
modified .oxyignore +4 −0
| … | 22 unchanged lines |
||
| 23 | 23 | # Compose screenshot references: the README copies live in docs/screenshots |
|
| 24 | 24 | # (scripts/screenshots.sh); these change with the clock, so are not kept. |
|
| 25 | 25 | /app/src/screenshotTestDebug/ |
|
| 26 | + | ||
| 27 | + | # The CI job's reports (scripts/ci.sh). |
|
| 28 | + | /junit.xml |
|
| 29 | + | /coverage.xml |
modified CHANGELOG.md +1 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **CI on OxygenFlow (#19):** every push builds the app and runs its unit tests in OxygenFlow's Android image, with no network, and keeps a JUnit report (`junit.xml`, the hub's Tests tab) and JaCoCo coverage (`coverage.xml`, the Coverage tab); `scripts/ci.sh` is the job. Debug builds record unit-test coverage (`./gradlew :app:createDebugUnitTestCoverageReport`). The screenshot tests stay local: their references change with the clock, so are not kept. |
|
| 5 | 6 | - **Account and organisation pages:** an owner's page shows who they are (avatar, name, when they joined; an organisation's description and members, each opening their own page), their open project boards and the repositories of theirs you can read, newest first. Opened from a repository's top bar, from a member, and from hub links to `/{owner}` or `/orgs/{name}`. From `GET /users/{name}`, else `GET /orgs/{name}`. |
|
| 6 | 7 | - **Off charge, the runner gives a minute's notice (#8, oxygenflow#76/#77):** taken off charge, the phone claims nothing more, finishes a job the hub expects to end within a minute and hands back any other to another runner, its checkpoint saved so the next attempt resumes where it stopped. Leaving Wi-Fi still hands back at once. Pins OxygenFlow's main (checkpoints, `leave_within`). |
|
| 7 | 8 | - **Diffs on a change (#21):** each file a change touched shows its diff in the app: context, removed and added lines with their old and new numbers, tinted, long lines wrapped, gaps between hunks marked. A small change (up to five files, 400 lines) opens every diff; a larger one starts folded and a file's row opens it. The icon on the row still opens the file at the change. From the hub's change API (hub #320); an older hub sends no lines, and the row says so. |
|
| … | 8 unchanged lines |
modified app/build.gradle.kts +5 −0
| … | 67 unchanged lines |
||
| 68 | 68 | } |
|
| 69 | 69 | ||
| 70 | 70 | buildTypes { |
|
| 71 | + | // Unit test coverage as JaCoCo XML for the hub's Coverage tab |
|
| 72 | + | // (#19): ./gradlew :app:createDebugUnitTestCoverageReport. |
|
| 73 | + | debug { |
|
| 74 | + | enableUnitTestCoverage = true |
|
| 75 | + | } |
|
| 71 | 76 | release { |
|
| 72 | 77 | optimization { |
|
| 73 | 78 | enable = false |
|
| … | 58 unchanged lines |