OXYGENHUB

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