From ec5aa645454689dc5acceb7926b72a0b3477d677 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:09:24 +0000 Subject: [PATCH 1/9] Release workflow: build, smoke test and publish the extension jar; README install for users Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- .github/workflows/release.yml | 109 ++++++++++++++++++++++++++++++++++ README.md | 23 +++++-- build.sh | 10 +++- docs/user-guide.md | 10 ++-- tests/smoke_jar.groovy | 37 ++++++++++++ tests/smoke_jar.sh | 19 ++++++ 6 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 tests/smoke_jar.groovy create mode 100755 tests/smoke_jar.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b0806c0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,109 @@ +name: release +# One-click install: when a release is published, this builds labconstrictor-qupath-.jar with build.sh (Java 25, against the pinned QuPath), +# runs a smoke test on that exact jar in a real QuPath on a virtual screen (the menu entry exists, the dialog opens), and attaches the jar, +# LabConstrictorTools-all.groovy and SHA256SUMS to the release; the QuPath version and the minimum Tools version are appended to the notes. +# Manually (workflow_dispatch, input "tag") it is a dry run: build, smoke test, files kept as workflow artifacts, nothing is published. +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Tag name to build for (dry run: nothing is published)" + required: true + default: "v0.0.0-dryrun" +permissions: + contents: read +env: + # QuPath is pinned by version and checksum (same pin as qupath-gui.yml); the download is cached per QUPATH_VERSION. + QUPATH_VERSION: "0.7.0" + QUPATH_SHA256: 165e27a0731d58ba039e9d0d34a54acf896bb92e81922370df295cb85418ce53 + TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }} +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Check the tag name + run: | + # the tag becomes a file name and the jar's version: letters, digits, dot, dash, underscore only + [[ "$TAG" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] || { echo "::error::unsafe or empty tag name"; exit 1; } + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 (QuPath 0.7 is built with Java 25) + with: { distribution: temurin, java-version: "25" } + - name: Install xvfb and the libraries QuPath's JavaFX needs + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends xvfb xauth libgtk-3-0t64 libxtst6 libxrender1 libxi6 libxrandr2 libgl1 libasound2t64 fonts-dejavu-core + - name: Cache QuPath + id: qupath-cache + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: qupath-dist + key: qupath-${{ env.QUPATH_VERSION }}-${{ env.QUPATH_SHA256 }} + - name: Download and check QuPath + if: steps.qupath-cache.outputs.cache-hit != 'true' + run: | + curl -fsSL --retry 5 --retry-delay 10 -o qupath.tar.xz "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/qupath/qupath/releases/download/v${QUPATH_VERSION}/QuPath-v${QUPATH_VERSION}-Linux.tar.xz" + echo "$QUPATH_SHA256 qupath.tar.xz" | sha256sum -c - + mkdir qupath-dist + tar -xJf qupath.tar.xz -C qupath-dist + rm qupath.tar.xz + - name: Build the jar and the single-file script + run: | + test -d "$PWD/qupath-dist/QuPath/lib/app" + QUPATH="$PWD/qupath-dist/QuPath" LC_QUPATH_VERSION="$TAG" ./build.sh + test -f "target/labconstrictor-qupath-$TAG.jar" + test -s target/LabConstrictorTools-all.groovy + - name: Check the version inside the jar + run: | + unzip -p "target/labconstrictor-qupath-$TAG.jar" META-INF/MANIFEST.MF | tr -d '\r' | grep -Fx "Implementation-Version: $TAG" + - name: Smoke test the jar (QuPath on a virtual screen) + run: QUPATH="$PWD/qupath-dist/QuPath" tests/smoke_jar.sh "target/labconstrictor-qupath-$TAG.jar" + - name: Collect the release files and the notes + run: | + MIN_TOOLS=$(sed -n 's/.*MIN_TOOLS_VERSION = "\([^"]*\)".*/\1/p' src/main/resources/org/cellmigrationlab/labconstrictor/qupath/LcCompat.groovy) + [ -n "$MIN_TOOLS" ] || { echo "::error::MIN_TOOLS_VERSION not found in LcCompat.groovy"; exit 1; } + mkdir release-files + cp "target/labconstrictor-qupath-$TAG.jar" target/LabConstrictorTools-all.groovy release-files/ + (cd release-files && sha256sum "labconstrictor-qupath-$TAG.jar" LabConstrictorTools-all.groovy > SHA256SUMS && cat SHA256SUMS) + { + echo "" + echo "### Install" + echo "Download \`labconstrictor-qupath-$TAG.jar\`, drag it onto the QuPath window (or copy it into the extensions folder shown in QuPath's Preferences) and restart QuPath. Then use **Extensions > LabConstrictor tools...**. \`LabConstrictorTools-all.groovy\` is the same script as one file for QuPath's Script Editor. \`SHA256SUMS\` lists the checksums." + echo + echo "- Built against **QuPath $QUPATH_VERSION** (Java 25). Tested: QuPath $QUPATH_VERSION on Linux; not tested on Windows or macOS." + echo "- Needs **labconstrictor-tools $MIN_TOOLS or newer** in the LabConstrictor app's environment." + } > release-files/release-notes-addition.md + cat release-files/release-notes-addition.md + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: release-files + path: release-files + if-no-files-found: error + retention-days: 14 + publish: + # Only for a published release; a manual run is a dry run and stops after the build job. + if: github.event_name == 'release' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: release-files + path: release-files + - name: Attach the files and append the notes + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + cd release-files + gh release upload "$TAG" "labconstrictor-qupath-$TAG.jar" LabConstrictorTools-all.groovy SHA256SUMS --clobber + gh release view "$TAG" --json body --jq .body > body.md + if ! grep -q 'labconstrictor-qupath-build' body.md; then + { cat body.md; echo; echo; cat release-notes-addition.md; } > notes.md + gh release edit "$TAG" --notes-file notes.md + fi diff --git a/README.md b/README.md index 2babc11..72252f1 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,20 @@ QuPath 0.7. The form is generated from the tool's declared schema ([LabConstrict so QuPath never imports the app's packages and needs no Python of its own. Same idea as the Napari and Fiji front-ends. ## Install -1. Install the LabConstrictor app(s) (the installer registers them; check with `labconstrictor-tools list`). -2. Drop `labconstrictor-qupath-0.1.0.jar` on the QuPath window (or into the QuPath extensions folder) and restart QuPath. -3. **Extensions > LabConstrictor tools...** +1. Install a LabConstrictor app (NucleiSky, CellTracksColab, ...): its installer registers the app and its tools (check with `labconstrictor-tools list`). +2. Download `labconstrictor-qupath-.jar` from the [Releases page](https://github.com/CellMigrationLab/LabConstrictor-QuPath/releases), drag it onto the QuPath window (or copy it into the extensions folder shown in QuPath's Preferences) and restart QuPath. +3. Open **Extensions > LabConstrictor tools...** -Without the jar: the script is several files now (`src/main/resources/org/cellmigrationlab/labconstrictor/qupath/`, joined in the order of `parts.txt`; the first is the main script). `./build.sh` also writes `target/LabConstrictorTools-all.groovy`, all of them in one file: open that one in QuPath's Script Editor and run it. The main script alone no longer runs. +For scripters, a second way that needs no jar: download `LabConstrictorTools-all.groovy` from the same release (the whole script in one file), open it in QuPath's Script Editor and run it. +Every release lists the QuPath version it was built against and the minimum Tools version in its notes; `SHA256SUMS` holds the checksums of the files. + +**QuPath versions.** Built against and tested with **QuPath 0.7.0 on Linux** (headless, on a virtual screen, in CI). Not tested on Windows or macOS, and not tested with other QuPath versions; QuPath 0.7 is built with Java 25 and the jar needs it, so older QuPath versions are not supported. + +### Troubleshooting +* No entry **Extensions > LabConstrictor tools...**: the jar needs QuPath 0.7 (Help > About shows the version); restart QuPath after adding the jar and check that it is in the extensions folder. +* "No LabConstrictor app is registered on this machine": install a LabConstrictor app first, then press **Rescan apps**. +* An app is listed as skipped, or a run is refused with "... labconstrictor-tools X is older than the 0.3.0a1 this host needs: update it": update the app (its Tools must be 0.3.0a1 or newer). +* The log is `~/.labconstrictor/logs/labconstrictor.log` (or `$LC_HOME/logs/`); the status line and **Details** of the window show what failed. Needs **labconstrictor-tools 0.3.0a1 or newer** in the app's environment (the run record and the clean-up use its helpers and its `prune` command). A run of an app with an older one is refused with `this app's labconstrictor-tools 0.2.0rc1 is older than the 0.3.0a1 this host needs: update it`. @@ -108,7 +117,11 @@ Each of these is deliberate and is written once to QuPath's log (logger `labcons * Label images are shown as a preview, not converted to QuPath annotations/detections, and tables are not added to the measurements. * No headless/script API, no Windows/macOS testing, no menu entry per tool. -## Build and test +## Build from source, and test +Users get the jar from the Releases page (see Install). Developers: `QUPATH=/path/to/QuPath JAVA_HOME= ./build.sh` writes `target/labconstrictor-qupath-0.1.0.jar` (set `LC_QUPATH_VERSION` for another version) and `target/LabConstrictorTools-all.groovy`. + +**Releases.** `.github/workflows/release.yml` runs when a release is published: it builds `labconstrictor-qupath-.jar` (the tag is the version in the jar's manifest), starts that jar in QuPath 0.7.0 on a virtual screen (`tests/smoke_jar.sh`: the menu entry exists and the dialog opens), and only then attaches the jar, `LabConstrictorTools-all.groovy` and `SHA256SUMS` to the release and appends the QuPath and Tools versions to its notes. Run it by hand (Actions > release > Run workflow, with a tag name) for a dry run: same build and smoke test, files kept as workflow artifacts, no release. + QUPATH=/path/to/QuPath JAVA_HOME= ./build.sh # QuPath 0.7 is built with Java 25 QUPATH=/path/to/QuPath LC_HOME= tests/run_gui_test.sh # needs xvfb; 21 checks diff --git a/build.sh b/build.sh index 9ad148d..67ef650 100755 --- a/build.sh +++ b/build.sh @@ -1,13 +1,17 @@ #!/bin/bash -# Builds target/labconstrictor-qupath-0.1.0.jar against a QuPath installation: QUPATH=/path/to/QuPath ./build.sh +# Builds target/labconstrictor-qupath-.jar against a QuPath installation: QUPATH=/path/to/QuPath ./build.sh +# The version is 0.1.0 unless LC_QUPATH_VERSION is set (the release workflow sets it to the release tag); it is the file name and the +# Implementation-Version of the jar's manifest. set -euo pipefail cd "$(dirname "$0")" QUPATH="${QUPATH:?set QUPATH to the QuPath folder (the one containing lib/app)}" +VERSION="${LC_QUPATH_VERSION:-0.1.0}" rm -rf target/classes && mkdir -p target/classes "${JAVA_HOME:+$JAVA_HOME/bin/}javac" -cp "$QUPATH/lib/app/*" -d target/classes $(find src/main/java -name '*.java') cp -r src/main/resources/* target/classes/ -"${JAVA_HOME:+$JAVA_HOME/bin/}jar" cf target/labconstrictor-qupath-0.1.0.jar -C target/classes . -echo "built target/labconstrictor-qupath-0.1.0.jar" +printf 'Implementation-Title: LabConstrictor-QuPath\nImplementation-Version: %s\n' "$VERSION" > target/manifest.txt +"${JAVA_HOME:+$JAVA_HOME/bin/}jar" cfm "target/labconstrictor-qupath-$VERSION.jar" target/manifest.txt -C target/classes . +echo "built target/labconstrictor-qupath-$VERSION.jar" # one file with the whole script, for QuPath's Script Editor (the jar joins the files listed in parts.txt itself) RES=src/main/resources/org/cellmigrationlab/labconstrictor/qupath { while IFS= read -r part || [ -n "$part" ]; do [ -z "$part" ] || { cat "$RES/$part"; echo; }; done < "$RES/parts.txt"; } > target/LabConstrictorTools-all.groovy diff --git a/docs/user-guide.md b/docs/user-guide.md index 5293640..1e83354 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -5,20 +5,20 @@ The extension runs the tools of every installed LabConstrictor app from QuPath, > [!WARNING] > **Under heavy construction.** This plugin is untested and unlikely to be stable. Expect bugs, missing features and changes without notice. Do not rely on it for work you cannot redo. > -> For QuPath 0.7. There is no ready-made file yet; the jar is built from its repository. +> For QuPath 0.7 (tested with 0.7.0 on Linux; not tested on Windows or macOS). The jar is attached to each release; until the first release is published, build it from the repository. ## What you need - [QuPath](https://qupath.github.io) 0.7. - At least one LabConstrictor app installed on your computer. The app's installer registers it, which is how the extension finds it. -- To build the jar: JDK 25 (QuPath 0.7 is built with Java 25), as described in the repository's README. +- Only to build the jar yourself: JDK 25 (QuPath 0.7 is built with Java 25), as described in the repository's README. ## Install -1. Get the [LabConstrictor-QuPath](https://github.com/CellMigrationLab/LabConstrictor-QuPath) repository and build `labconstrictor-qupath-0.1.0.jar` as its README describes. -2. Drop the jar onto the QuPath window, or put it in the QuPath extensions folder, and restart QuPath. +1. Download `labconstrictor-qupath-.jar` from the [Releases page](https://github.com/CellMigrationLab/LabConstrictor-QuPath/releases). +2. Drop the jar onto the QuPath window, or put it in the QuPath extensions folder (shown in QuPath's Preferences), and restart QuPath. -Without the jar: open `LabConstrictorTools.groovy` (under `src/main/resources/org/cellmigrationlab/labconstrictor/qupath/`) in QuPath's Script Editor and run it. +Without the jar: download `LabConstrictorTools-all.groovy` from the same release, open it in QuPath's Script Editor and run it. ## Open it diff --git a/tests/smoke_jar.groovy b/tests/smoke_jar.groovy new file mode 100644 index 0000000..247b879 --- /dev/null +++ b/tests/smoke_jar.groovy @@ -0,0 +1,37 @@ +// Smoke test of a built extension jar (tests/smoke_jar.sh). QuPath runs this file once started, with the jar on its class path: +// the menu entry Extensions > "LabConstrictor tools..." must exist and pressing it must open the "LabConstrictor tools" window +// (the joined script is read from the jar). Exit code 0 only when both hold. +import javafx.application.Platform +import javafx.scene.control.Menu +import javafx.stage.Stage +import javafx.stage.Window +import java.util.concurrent.CountDownLatch + +def fx = { Closure c -> + def latch = new CountDownLatch(1); def box = [null, null] + Platform.runLater { try { box[0] = c() } catch (Throwable t) { box[1] = t } finally { latch.countDown() } } + latch.await() + if (box[1] != null) throw box[1] + return box[0] +} +Thread.start("lc-smoke") { + def ok = false + try { + def item = fx { qupath.getMenu("Extensions", false)?.items?.find { it.text == "LabConstrictor tools..." } } + println((item != null ? "PASS " : "FAIL ") + "menu entry Extensions > LabConstrictor tools...") + if (item != null) { + fx { item.fire() } + def end = System.currentTimeMillis() + 90_000 + def dialog = null + while (dialog == null && System.currentTimeMillis() < end) { + Thread.sleep(500) + dialog = fx { Window.getWindows().find { it instanceof Stage && it.showing && it.title == "LabConstrictor tools" } } + } + println((dialog != null ? "PASS " : "FAIL ") + "dialog 'LabConstrictor tools' opens") + ok = dialog != null + } + } catch (Throwable t) { t.printStackTrace() } + println "SMOKE " + (ok ? "OK" : "FAILED") + Thread.sleep(300) + System.exit(ok ? 0 : 1) +} diff --git a/tests/smoke_jar.sh b/tests/smoke_jar.sh new file mode 100755 index 0000000..4cc1fb6 --- /dev/null +++ b/tests/smoke_jar.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Starts QuPath on a virtual screen (Xvfb) with a built extension jar and runs tests/smoke_jar.groovy: the menu entry exists and the dialog opens. +# QUPATH=/path/to/QuPath tests/smoke_jar.sh target/labconstrictor-qupath-.jar +# The QuPath folder is copied first (the jar is put on its class path like tests/run_gui_test.sh does), so the original stays untouched. +# Needs xvfb and QuPath's libraries (see .github/workflows/release.yml). Uses an empty LC_HOME: no app is registered, the dialog says so. +set -euo pipefail +JAR="$(cd "$(dirname "${1:?path of the jar to test}")" && pwd)/$(basename "$1")" +QUPATH="${QUPATH:?set QUPATH}" +HERE="$(cd "$(dirname "$0")" && pwd)" +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT +cp -a "$QUPATH" "$WORK/QuPath" +cp "$JAR" "$WORK/QuPath/lib/app/" +CFG="$WORK/QuPath/lib/app/QuPath.cfg" +grep -q '^app.mainclass=qupath.QuPath$' "$CFG" || { echo "FAIL QuPath.cfg has no app.mainclass line to extend" >&2; exit 1; } +sed -i "s|^app.mainclass=qupath.QuPath\$|&\napp.classpath=\$APPDIR/$(basename "$JAR")|" "$CFG" +mkdir -p "$WORK/home" +export LC_HOME="$WORK/home" HOME="$WORK/home" QT_QPA_PLATFORM= +timeout --kill-after=20 "${SMOKE_TIMEOUT:-300}" xvfb-run -a -s "-screen 0 1600x1000x24" "$WORK/QuPath/bin/QuPath" -q -Dlc.qupath.test.script="$HERE/smoke_jar.groovy" | tee "$WORK/out.txt" +grep -q '^SMOKE OK' "$WORK/out.txt" From a1fcad3b772730345d8159b66ce56a5f97be0de9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:09:38 +0000 Subject: [PATCH 2/9] release.yml: dry run also on pull requests that change the release machinery Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0806c0..04ca081 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,10 @@ name: release # One-click install: when a release is published, this builds labconstrictor-qupath-.jar with build.sh (Java 25, against the pinned QuPath), # runs a smoke test on that exact jar in a real QuPath on a virtual screen (the menu entry exists, the dialog opens), and attaches the jar, # LabConstrictorTools-all.groovy and SHA256SUMS to the release; the QuPath version and the minimum Tools version are appended to the notes. -# Manually (workflow_dispatch, input "tag") it is a dry run: build, smoke test, files kept as workflow artifacts, nothing is published. +# Manually (workflow_dispatch, input "tag") or on a pull request that changes this machinery it is a dry run: build, smoke test, files kept as workflow artifacts, nothing is published. on: + pull_request: # a dry run when the release machinery itself changes (never publishes) + paths: [".github/workflows/release.yml", "build.sh", "tests/smoke_jar.*"] release: types: [published] workflow_dispatch: @@ -18,7 +20,7 @@ env: # QuPath is pinned by version and checksum (same pin as qupath-gui.yml); the download is cached per QUPATH_VERSION. QUPATH_VERSION: "0.7.0" QUPATH_SHA256: 165e27a0731d58ba039e9d0d34a54acf896bb92e81922370df295cb85418ce53 - TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }} + TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag || 'v0.0.0-dryrun' }} jobs: build: runs-on: ubuntu-latest From c9547de47096c1d6e6ade3aeba1f2a9d8ff2c7fc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:10:33 +0000 Subject: [PATCH 3/9] smoke test: bounded run with its own session, output shown Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- tests/smoke_jar.groovy | 1 + tests/smoke_jar.sh | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/smoke_jar.groovy b/tests/smoke_jar.groovy index 247b879..b9cd34d 100644 --- a/tests/smoke_jar.groovy +++ b/tests/smoke_jar.groovy @@ -14,6 +14,7 @@ def fx = { Closure c -> if (box[1] != null) throw box[1] return box[0] } +println "SMOKE start" Thread.start("lc-smoke") { def ok = false try { diff --git a/tests/smoke_jar.sh b/tests/smoke_jar.sh index 4cc1fb6..4b025ba 100755 --- a/tests/smoke_jar.sh +++ b/tests/smoke_jar.sh @@ -15,5 +15,12 @@ grep -q '^app.mainclass=qupath.QuPath$' "$CFG" || { echo "FAIL QuPath.cfg has no sed -i "s|^app.mainclass=qupath.QuPath\$|&\napp.classpath=\$APPDIR/$(basename "$JAR")|" "$CFG" mkdir -p "$WORK/home" export LC_HOME="$WORK/home" HOME="$WORK/home" QT_QPA_PLATFORM= -timeout --kill-after=20 "${SMOKE_TIMEOUT:-300}" xvfb-run -a -s "-screen 0 1600x1000x24" "$WORK/QuPath/bin/QuPath" -q -Dlc.qupath.test.script="$HERE/smoke_jar.groovy" | tee "$WORK/out.txt" +# QuPath runs in its own session (setsid) so that a hang can be ended with all its child processes; its output goes to a file that is shown at the end. +LIMIT="${SMOKE_TIMEOUT:-240}" +setsid xvfb-run -a -s "-screen 0 1600x1000x24" "$WORK/QuPath/bin/QuPath" -q -Dlc.qupath.test.script="$HERE/smoke_jar.groovy" > "$WORK/out.txt" 2>&1 & +PID=$! +for ((i = 0; i < LIMIT; i++)); do kill -0 "$PID" 2>/dev/null || break; sleep 1; done +if kill -0 "$PID" 2>/dev/null; then echo "FAIL QuPath still running after ${LIMIT}s, ending it"; kill -TERM -- "-$PID" 2>/dev/null || true; sleep 5; kill -KILL -- "-$PID" 2>/dev/null || true; fi +wait "$PID" 2>/dev/null || true +tail -n 80 "$WORK/out.txt" grep -q '^SMOKE OK' "$WORK/out.txt" From 0d72423b3533496cf8693c33b535aba483dc4aa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:13:47 +0000 Subject: [PATCH 4/9] temp: discover QuPath assets Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- .github/workflows/release.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04ca081..2646439 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,3 +109,13 @@ jobs: { cat body.md; echo; echo; cat release-notes-addition.md; } > notes.md gh release edit "$TAG" --notes-file notes.md fi + discover: + runs-on: ubuntu-latest + steps: + - env: + GH_TOKEN: ${{ github.token }} + run: | + gh api repos/qupath/qupath/releases/tags/v0.7.0 --jq '.assets[] | [.name, .size, .digest] | @tsv' + for n in $(gh api repos/qupath/qupath/releases/tags/v0.7.0 --jq '.assets[].name' | grep -iE 'windows|mac'); do + curl -fsSL --retry 5 -o "$n" "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/qupath/qupath/releases/download/v0.7.0/$n" && sha256sum "$n" && case "$n" in *.zip) unzip -l "$n" | head -15;; esac; rm -f "$n" + done From 10a6593a3b96b991c8ce1c8223fafa485ade9d08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:15:54 +0000 Subject: [PATCH 5/9] OS smoke on Windows and macOS (best effort), replaces the asset discovery job Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- .github/workflows/release.yml | 77 +++++++++++++++++++++++++++++++---- tests/ci_setup_os.sh | 19 +++++++++ tests/smoke_jar.groovy | 9 +++- tests/smoke_os.sh | 57 ++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 9 deletions(-) create mode 100755 tests/ci_setup_os.sh create mode 100755 tests/smoke_os.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2646439..08c9208 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,13 +109,74 @@ jobs: { cat body.md; echo; echo; cat release-notes-addition.md; } > notes.md gh release edit "$TAG" --notes-file notes.md fi - discover: - runs-on: ubuntu-latest + os-smoke: + # Windows and macOS (arm64): the same jar in the pinned QuPath build of that platform, on the runner's desktop. Best effort: these jobs inform + # (the table in the job summary says pass, fail with the reason, or "no gui"), they never block a release; publish waits for the Linux smoke only. + needs: build + runs-on: ${{ matrix.os }} + continue-on-error: true + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + asset: QuPath-v0.7.0-Windows.zip + sha256: 5839743281d11eca1ac55c87ddcf92c89a30cba7ba2060f162036b0514b1e30c + - os: macos-latest # Apple silicon (arm64) + asset: QuPath-v0.7.0-Mac-arm64.pkg + sha256: 955c4cdaa239610feccd4d315a09df5bb13835407766009a56585f9504b9b2e1 steps: - - env: - GH_TOKEN: ${{ github.token }} + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: { python-version: "3.12" } + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: release-files + path: release-files + - name: Cache the QuPath download + id: dl-cache + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: qupath-download + key: qupath-dl-${{ matrix.asset }}-${{ matrix.sha256 }} + - name: Download and check QuPath + if: steps.dl-cache.outputs.cache-hit != 'true' + shell: bash + run: | + mkdir -p qupath-download + curl -fsSL --retry 5 --retry-delay 10 -o "qupath-download/${{ matrix.asset }}" "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/qupath/qupath/releases/download/v${QUPATH_VERSION}/${{ matrix.asset }}" + echo "${{ matrix.sha256 }} qupath-download/${{ matrix.asset }}" > sha.txt + if command -v sha256sum >/dev/null; then sha256sum -c sha.txt; else shasum -a 256 -c sha.txt; fi + - name: Unpack QuPath (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: Expand-Archive -Path "qupath-download/${{ matrix.asset }}" -DestinationPath qupath-dist + - name: Install QuPath (macOS) + if: runner.os == 'macOS' + run: sudo installer -pkg "qupath-download/${{ matrix.asset }}" -target / + - name: Register the small apps (scratch LC_HOME, this OS's Python) + shell: bash + run: PYTHON=python tests/ci_setup_os.sh "$RUNNER_TEMP/lc" main + - name: Smoke test the jar and the OS-specific code paths + shell: bash run: | - gh api repos/qupath/qupath/releases/tags/v0.7.0 --jq '.assets[] | [.name, .size, .digest] | @tsv' - for n in $(gh api repos/qupath/qupath/releases/tags/v0.7.0 --jq '.assets[].name' | grep -iE 'windows|mac'); do - curl -fsSL --retry 5 -o "$n" "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/qupath/qupath/releases/download/v0.7.0/$n" && sha256sum "$n" && case "$n" in *.zip) unzip -l "$n" | head -15;; esac; rm -f "$n" - done + if [ "$RUNNER_OS" = Windows ]; then + APPDIR="$PWD/qupath-dist/app" + LAUNCHER="$(find qupath-dist -maxdepth 1 -name '*console*.exe' | head -1)" + [ -n "$LAUNCHER" ] || LAUNCHER="$(find qupath-dist -maxdepth 1 -name '*.exe' | head -1)" + else + APP="$(ls -d /Applications/QuPath*.app | head -1)" + APPDIR="$APP/Contents/app" + LAUNCHER="$(find "$APP/Contents/MacOS" -maxdepth 1 -type f -name 'QuPath*' | head -1)" + fi + case "$LAUNCHER" in /*) ;; *) LAUNCHER="$PWD/$LAUNCHER";; esac + echo "QuPath app folder: $APPDIR"; echo "launcher: $LAUNCHER"; ls "$APPDIR" | head -5 + LOGS="$PWD/os-logs" tests/smoke_os.sh "$APPDIR" "$LAUNCHER" "$(ls "$PWD"/release-files/labconstrictor-qupath-*.jar)" "$RUNNER_TEMP/lc/home" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: ${{ !cancelled() }} + with: + name: os-smoke-logs-${{ matrix.os }} + path: os-logs + if-no-files-found: ignore + retention-days: 14 diff --git a/tests/ci_setup_os.sh b/tests/ci_setup_os.sh new file mode 100755 index 0000000..f41cbb9 --- /dev/null +++ b/tests/ci_setup_os.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Registers the small apps of the OS smoke (tests/smoke_os.sh) in a scratch LC_HOME on Linux, macOS or Windows (Git Bash): +# tests/ci_setup_os.sh [tools git ref, default main] PYTHON= (default: python) +# interactions the example app of labconstrictor-tools runs tests/runs_app (delayed, failing and number-reporting tools) +# Like tests/ci_setup_apps.sh, with the interpreter path of the OS: /Scripts/python.exe on Windows, /bin/python elsewhere. +set -euo pipefail +native() { if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else echo "$1"; fi; } +WORK="$(mkdir -p "${1:?work dir}" && cd "$1" && pwd)"; REF="${2:-main}" +REPO="$(cd "$(dirname "$0")/.." && pwd)" +WORKN="$(native "$WORK")"; REPON="$(native "$REPO")" +export LC_HOME="$WORKN/home"; mkdir -p "$WORK/home" +"${PYTHON:-python}" -m venv --copies "$WORK/tools_env" +if [ -x "$WORK/tools_env/Scripts/python.exe" ]; then PY="$WORK/tools_env/Scripts/python.exe"; else PY="$WORK/tools_env/bin/python"; fi +echo "interpreter of the app environment: $PY" +"$PY" -m pip install -q --upgrade pip +"$PY" -m pip install -q "git+https://github.com/CellMigrationLab/LabConstrictor-Tools@$REF" numpy pandas tifffile scipy +"$PY" -m labconstrictor_tools register --name interactions --prefix "$WORKN/tools_env" --module labconstrictor_tools.examples.interactions +"$PY" -m labconstrictor_tools register --name runs --prefix "$WORKN/tools_env" --module runs_lc_tools --pythonpath "$REPON/tests/runs_app" +"$PY" -m labconstrictor_tools list diff --git a/tests/smoke_jar.groovy b/tests/smoke_jar.groovy index b9cd34d..a8dc5bf 100644 --- a/tests/smoke_jar.groovy +++ b/tests/smoke_jar.groovy @@ -1,6 +1,6 @@ // Smoke test of a built extension jar (tests/smoke_jar.sh). QuPath runs this file once started, with the jar on its class path: // the menu entry Extensions > "LabConstrictor tools..." must exist and pressing it must open the "LabConstrictor tools" window -// (the joined script is read from the jar). Exit code 0 only when both hold. +// (the joined script is read from the jar). With the environment variable LC_SMOKE_APP the dialog must also list that app. Exit code 0 only when all hold. import javafx.application.Platform import javafx.scene.control.Menu import javafx.stage.Stage @@ -30,6 +30,13 @@ Thread.start("lc-smoke") { } println((dialog != null ? "PASS " : "FAIL ") + "dialog 'LabConstrictor tools' opens") ok = dialog != null + // optional (the OS smokes register one app): the app chooser of the dialog must list it + def app = System.getenv("LC_SMOKE_APP") + if (ok && app) { + def listed = fx { dialog.scene.root.lookupAll(".combo-box").any { it.items.any { i -> i.toString() == app } } } + println((listed ? "PASS " : "FAIL ") + "the dialog lists the registered app " + app) + ok = listed + } } } catch (Throwable t) { t.printStackTrace() } println "SMOKE " + (ok ? "OK" : "FAILED") diff --git a/tests/smoke_os.sh b/tests/smoke_os.sh new file mode 100755 index 0000000..17372c4 --- /dev/null +++ b/tests/smoke_os.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# OS smoke of a built extension jar on Windows, macOS or Linux with a real display (a CI runner's desktop session; no Xvfb): +# tests/smoke_os.sh +# holds the launcher's .cfg files and jars (Windows: /app, macOS: .app/Contents/app). The jar is put on +# its class path (the same way tests/run_gui_test.sh does), QuPath is started and runs, one after the other: +# 1. tests/smoke_jar.groovy: the menu entry exists, the dialog opens and lists the registered app "interactions"; +# 2. the test bodies that reach OS-specific code: the Copy-as-command quoting of every case of tests/quote_vectors.json, the trust checks +# (interpreter inside the prefix, the Windows note), the results folders and the run-record clean-up (file locking while pruning). +# Prints one line per step: "RESULT : pass | fail (reason) | no gui (QuPath never ran the script)". Exit 1 when a step is not "pass". +set -uo pipefail +APPDIR="${1:?QuPath app folder}"; LAUNCHER="${2:?launcher}"; JAR="${3:?jar}"; SHARED="${4:?LC_HOME}" +HERE="$(cd "$(dirname "$0")" && pwd)"; REPO="$(dirname "$HERE")" +native() { if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else echo "$1"; fi; } +LOGS="${LOGS:-$REPO/os-logs}"; mkdir -p "$LOGS" +RES="$REPO/src/main/resources/org/cellmigrationlab/labconstrictor/qupath" +cp "$JAR" "$APPDIR/"; JARNAME="$(basename "$JAR")" +for cfg in "$APPDIR"/*.cfg; do + line="$(grep -m1 '^app.classpath=' "$cfg" || true)"; [ -n "$line" ] || continue + sep='/'; case "$line" in *'\'*) sep='\';; esac + awk -v new="app.classpath=\$APPDIR$sep$JARNAME" '{ print } !d && /^app.classpath=/ { print new; d = 1 }' "$cfg" > "$cfg.new" && mv "$cfg.new" "$cfg" + echo "jar added to the class path of $(basename "$cfg")" +done +rc=0; summary="$LOGS/summary.md"; echo "| step | result |" > "$summary"; echo "|---|---|" >> "$summary" +# run_qupath [extra -D...]: QuPath with a time limit; the output goes to $LOGS/.log +run_qupath() { + local name="$1" script="$2" home="$3" log="$LOGS/$1.log"; shift 3 + LC_HOME="$home" "$LAUNCHER" -q "-Dlc.qupath.test.script=$(native "$script")" "-Dlc.test.out=$(native "$LOGS/shots_$name")" "-Dlc.test.fixtures=$(native "$REPO/tests/fixtures")" "$@" > "$log" 2>&1 & + local pid=$! i + for ((i = 0; i < ${STEP_TIMEOUT:-300}; i++)); do kill -0 "$pid" 2>/dev/null || break; sleep 1; done + if kill -0 "$pid" 2>/dev/null; then echo "TIMEOUT after ${STEP_TIMEOUT:-300}s" >> "$log"; kill -9 "$pid" 2>/dev/null; command -v taskkill >/dev/null 2>&1 && taskkill //F //T //PID "$pid" >/dev/null 2>&1; fi + wait "$pid" 2>/dev/null; echo $? > "$LOGS/$name.code" +} +verdict() { # + local name="$1" log="$LOGS/$1.log" r + if ! grep -qE '^(SMOKE start|PASS |FAIL |RESULTS |SUMMARY )' "$log"; then r="no gui (QuPath never ran the script; see $name.log)" + elif grep -q '^FAIL ' "$log"; then r="fail ($(grep -c '^FAIL ' "$log") failed check(s): $(grep -m1 '^FAIL ' "$log" | cut -c1-160))" + elif ! grep -qE "$2" "$log"; then r="fail (no final line: crash or timeout; code $(cat "$LOGS/$name.code"))" + elif [ "$(grep -c '^PASS ' "$log")" -eq 0 ]; then r="fail (no check passed)" + else r="pass ($(grep -c '^PASS ' "$log") checks)"; fi + echo "RESULT $name: $r"; echo "| $name | $r |" >> "$summary" + case "$r" in pass*) ;; *) rc=1; tail -n 40 "$log";; esac +} +# 1. the jar itself: menu entry and dialog (the apps of $SHARED are registered) +LC_SMOKE_APP=interactions run_qupath smoke_jar "$HERE/smoke_jar.groovy" "$SHARED" +verdict smoke_jar '^SMOKE OK' +# 2. OS-specific code paths: each body is appended to the joined script (what tests/run_gui_test.sh does) +declare -A scratch=([gui_test_parity_trust_body.groovy]=lhome_trust [gui_test_parity_lifecycle_body.groovy]=lhome_life [gui_test_retention_body.groovy]=lhome_retention) +for body in gui_test_parity_quote_body.groovy gui_test_parity_trust_body.groovy gui_test_parity_lifecycle_body.groovy gui_test_retention_body.groovy; do + name="${body%.groovy}"; joined="$LOGS/$name.joined.groovy" + { while IFS= read -r part || [ -n "$part" ]; do part="${part%$'\r'}"; [ -z "$part" ] || { cat "$RES/$part"; echo; }; done < "$RES/parts.txt"; cat "$HERE/$body"; } > "$joined" + home="$SHARED" + if [ -n "${scratch[$body]:-}" ]; then home="$LOGS/scratch/${scratch[$body]}"; mkdir -p "$home"; fi + LC_SHARED_HOME="$(native "$SHARED")" run_qupath "$name" "$joined" "$(native "$home")" -Dlc.noshow=true + verdict "$name" '^(RESULTS|SUMMARY) ' +done +[ -z "${GITHUB_STEP_SUMMARY:-}" ] || cat "$summary" >> "$GITHUB_STEP_SUMMARY" +cat "$summary"; exit $rc From 5d2024d5790077fc02875cbb28a8d2b7599443e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:46:05 +0000 Subject: [PATCH 6/9] OS smoke: fix class path line (awk ate the backslash on Windows), shorter timeouts, skip after no gui Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- .github/workflows/release.yml | 1 + tests/smoke_os.sh | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08c9208..f48ef0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,6 +112,7 @@ jobs: os-smoke: # Windows and macOS (arm64): the same jar in the pinned QuPath build of that platform, on the runner's desktop. Best effort: these jobs inform # (the table in the job summary says pass, fail with the reason, or "no gui"), they never block a release; publish waits for the Linux smoke only. + name: os-smoke (${{ matrix.os }}) needs: build runs-on: ${{ matrix.os }} continue-on-error: true diff --git a/tests/smoke_os.sh b/tests/smoke_os.sh index 17372c4..3f8ddbd 100755 --- a/tests/smoke_os.sh +++ b/tests/smoke_os.sh @@ -17,7 +17,7 @@ cp "$JAR" "$APPDIR/"; JARNAME="$(basename "$JAR")" for cfg in "$APPDIR"/*.cfg; do line="$(grep -m1 '^app.classpath=' "$cfg" || true)"; [ -n "$line" ] || continue sep='/'; case "$line" in *'\'*) sep='\';; esac - awk -v new="app.classpath=\$APPDIR$sep$JARNAME" '{ print } !d && /^app.classpath=/ { print new; d = 1 }' "$cfg" > "$cfg.new" && mv "$cfg.new" "$cfg" + NEWLINE="app.classpath=\$APPDIR$sep$JARNAME" awk '{ print } !d && /^app.classpath=/ { print ENVIRON["NEWLINE"] ($0 ~ /\r$/ ? "\r" : ""); d = 1 }' "$cfg" > "$cfg.new" && mv "$cfg.new" "$cfg" echo "jar added to the class path of $(basename "$cfg")" done rc=0; summary="$LOGS/summary.md"; echo "| step | result |" > "$summary"; echo "|---|---|" >> "$summary" @@ -26,8 +26,8 @@ run_qupath() { local name="$1" script="$2" home="$3" log="$LOGS/$1.log"; shift 3 LC_HOME="$home" "$LAUNCHER" -q "-Dlc.qupath.test.script=$(native "$script")" "-Dlc.test.out=$(native "$LOGS/shots_$name")" "-Dlc.test.fixtures=$(native "$REPO/tests/fixtures")" "$@" > "$log" 2>&1 & local pid=$! i - for ((i = 0; i < ${STEP_TIMEOUT:-300}; i++)); do kill -0 "$pid" 2>/dev/null || break; sleep 1; done - if kill -0 "$pid" 2>/dev/null; then echo "TIMEOUT after ${STEP_TIMEOUT:-300}s" >> "$log"; kill -9 "$pid" 2>/dev/null; command -v taskkill >/dev/null 2>&1 && taskkill //F //T //PID "$pid" >/dev/null 2>&1; fi + for ((i = 0; i < ${STEP_TIMEOUT:-180}; i++)); do kill -0 "$pid" 2>/dev/null || break; sleep 1; done + if kill -0 "$pid" 2>/dev/null; then echo "TIMEOUT after ${STEP_TIMEOUT:-180}s" >> "$log"; kill -9 "$pid" 2>/dev/null; command -v taskkill >/dev/null 2>&1 && taskkill //F //T //PID "$pid" >/dev/null 2>&1; fi wait "$pid" 2>/dev/null; echo $? > "$LOGS/$name.code" } verdict() { # @@ -43,6 +43,7 @@ verdict() { # # 1. the jar itself: menu entry and dialog (the apps of $SHARED are registered) LC_SMOKE_APP=interactions run_qupath smoke_jar "$HERE/smoke_jar.groovy" "$SHARED" verdict smoke_jar '^SMOKE OK' +if grep -q 'no gui' "$summary"; then echo "QuPath did not run the script: the other steps are skipped"; echo "| (other steps) | skipped: QuPath did not run the script |" >> "$summary"; [ -z "${GITHUB_STEP_SUMMARY:-}" ] || cat "$summary" >> "$GITHUB_STEP_SUMMARY"; exit 1; fi # 2. OS-specific code paths: each body is appended to the joined script (what tests/run_gui_test.sh does) declare -A scratch=([gui_test_parity_trust_body.groovy]=lhome_trust [gui_test_parity_lifecycle_body.groovy]=lhome_life [gui_test_retention_body.groovy]=lhome_retention) for body in gui_test_parity_quote_body.groovy gui_test_parity_trust_body.groovy gui_test_parity_lifecycle_body.groovy gui_test_retention_body.groovy; do From dec971b4f66b02f29934e85d7ce3eae48504a2b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:59:16 +0000 Subject: [PATCH 7/9] OS smoke: make the installed macOS app writable, scikit-image for the Python apps Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- .github/workflows/release.yml | 4 +++- tests/ci_setup_os.sh | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f48ef0f..b134c6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,7 +155,9 @@ jobs: run: Expand-Archive -Path "qupath-download/${{ matrix.asset }}" -DestinationPath qupath-dist - name: Install QuPath (macOS) if: runner.os == 'macOS' - run: sudo installer -pkg "qupath-download/${{ matrix.asset }}" -target / + run: | + sudo installer -pkg "qupath-download/${{ matrix.asset }}" -target / + sudo chown -R "$(id -un)" /Applications/QuPath*.app # the smoke puts the jar on the app's class path - name: Register the small apps (scratch LC_HOME, this OS's Python) shell: bash run: PYTHON=python tests/ci_setup_os.sh "$RUNNER_TEMP/lc" main diff --git a/tests/ci_setup_os.sh b/tests/ci_setup_os.sh index f41cbb9..e2fb4fe 100755 --- a/tests/ci_setup_os.sh +++ b/tests/ci_setup_os.sh @@ -13,7 +13,7 @@ export LC_HOME="$WORKN/home"; mkdir -p "$WORK/home" if [ -x "$WORK/tools_env/Scripts/python.exe" ]; then PY="$WORK/tools_env/Scripts/python.exe"; else PY="$WORK/tools_env/bin/python"; fi echo "interpreter of the app environment: $PY" "$PY" -m pip install -q --upgrade pip -"$PY" -m pip install -q "git+https://github.com/CellMigrationLab/LabConstrictor-Tools@$REF" numpy pandas tifffile scipy +"$PY" -m pip install -q "git+https://github.com/CellMigrationLab/LabConstrictor-Tools@$REF" numpy pandas tifffile scipy scikit-image "$PY" -m labconstrictor_tools register --name interactions --prefix "$WORKN/tools_env" --module labconstrictor_tools.examples.interactions "$PY" -m labconstrictor_tools register --name runs --prefix "$WORKN/tools_env" --module runs_lc_tools --pythonpath "$REPON/tests/runs_app" "$PY" -m labconstrictor_tools list From e0fc7765afded49f6039fa3e3426fac1a7ebd5ad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 20:03:45 +0000 Subject: [PATCH 8/9] OS smoke: no associative array (macOS bash 3.2) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- tests/smoke_os.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/smoke_os.sh b/tests/smoke_os.sh index 3f8ddbd..31c4c48 100755 --- a/tests/smoke_os.sh +++ b/tests/smoke_os.sh @@ -45,12 +45,14 @@ LC_SMOKE_APP=interactions run_qupath smoke_jar "$HERE/smoke_jar.groovy" "$SHARED verdict smoke_jar '^SMOKE OK' if grep -q 'no gui' "$summary"; then echo "QuPath did not run the script: the other steps are skipped"; echo "| (other steps) | skipped: QuPath did not run the script |" >> "$summary"; [ -z "${GITHUB_STEP_SUMMARY:-}" ] || cat "$summary" >> "$GITHUB_STEP_SUMMARY"; exit 1; fi # 2. OS-specific code paths: each body is appended to the joined script (what tests/run_gui_test.sh does) -declare -A scratch=([gui_test_parity_trust_body.groovy]=lhome_trust [gui_test_parity_lifecycle_body.groovy]=lhome_life [gui_test_retention_body.groovy]=lhome_retention) +# bodies that write their own registry get a scratch LC_HOME with the name they assert (no associative array: macOS has bash 3.2) +scratch_for() { case "$1" in gui_test_parity_trust_body.groovy) echo lhome_trust;; gui_test_parity_lifecycle_body.groovy) echo lhome_life;; gui_test_retention_body.groovy) echo lhome_retention;; esac; } for body in gui_test_parity_quote_body.groovy gui_test_parity_trust_body.groovy gui_test_parity_lifecycle_body.groovy gui_test_retention_body.groovy; do name="${body%.groovy}"; joined="$LOGS/$name.joined.groovy" { while IFS= read -r part || [ -n "$part" ]; do part="${part%$'\r'}"; [ -z "$part" ] || { cat "$RES/$part"; echo; }; done < "$RES/parts.txt"; cat "$HERE/$body"; } > "$joined" home="$SHARED" - if [ -n "${scratch[$body]:-}" ]; then home="$LOGS/scratch/${scratch[$body]}"; mkdir -p "$home"; fi + scr="$(scratch_for "$body")" + if [ -n "$scr" ]; then home="$LOGS/scratch/$scr"; mkdir -p "$home"; fi LC_SHARED_HOME="$(native "$SHARED")" run_qupath "$name" "$joined" "$(native "$home")" -Dlc.noshow=true verdict "$name" '^(RESULTS|SUMMARY) ' done From e33dbe6497270290215d912e3ae583f89a54d526 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 20:11:04 +0000 Subject: [PATCH 9/9] README: QuPath versions and systems, OS smoke Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72252f1..8a43e41 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ so QuPath never imports the app's packages and needs no Python of its own. Same For scripters, a second way that needs no jar: download `LabConstrictorTools-all.groovy` from the same release (the whole script in one file), open it in QuPath's Script Editor and run it. Every release lists the QuPath version it was built against and the minimum Tools version in its notes; `SHA256SUMS` holds the checksums of the files. -**QuPath versions.** Built against and tested with **QuPath 0.7.0 on Linux** (headless, on a virtual screen, in CI). Not tested on Windows or macOS, and not tested with other QuPath versions; QuPath 0.7 is built with Java 25 and the jar needs it, so older QuPath versions are not supported. +**QuPath versions and systems.** Built against QuPath **0.7.0**; QuPath 0.7 is built with Java 25 and the jar needs it, so older QuPath versions are not supported, and other 0.7.x versions are not tested. +* **Linux**: tested in CI (QuPath 0.7.0 on a virtual screen: the whole GUI test suite on every pull request, and the smoke test of the exact release jar before a release is published; a broken jar is not published). +* **macOS (Apple silicon) and Windows**: tried by the release workflow with the same jar in the QuPath 0.7.0 build of that system (menu entry, dialog, a registered app in the list, quoting, trust checks, results folders, clean-up). These runs inform but never block a release; see the job summary of the "release" workflow for the latest result. Last dry run: macOS all steps passed; Windows: the jar loads, the dialog opens and lists the app, the results-folder test passes; three of the test bodies fail there because they use POSIX tools or POSIX file permissions (the Windows trust note: the owner/mode checks of the registry do not exist on Windows, so a group-writable entry is accepted; the clean-up test needs `chmod`), so the clean-up while a results folder is in use is **not** verified on Windows. Not tested by a person on either system. ### Troubleshooting * No entry **Extensions > LabConstrictor tools...**: the jar needs QuPath 0.7 (Help > About shows the version); restart QuPath after adding the jar and check that it is in the extensions folder. @@ -120,7 +122,7 @@ Each of these is deliberate and is written once to QuPath's log (logger `labcons ## Build from source, and test Users get the jar from the Releases page (see Install). Developers: `QUPATH=/path/to/QuPath JAVA_HOME= ./build.sh` writes `target/labconstrictor-qupath-0.1.0.jar` (set `LC_QUPATH_VERSION` for another version) and `target/LabConstrictorTools-all.groovy`. -**Releases.** `.github/workflows/release.yml` runs when a release is published: it builds `labconstrictor-qupath-.jar` (the tag is the version in the jar's manifest), starts that jar in QuPath 0.7.0 on a virtual screen (`tests/smoke_jar.sh`: the menu entry exists and the dialog opens), and only then attaches the jar, `LabConstrictorTools-all.groovy` and `SHA256SUMS` to the release and appends the QuPath and Tools versions to its notes. Run it by hand (Actions > release > Run workflow, with a tag name) for a dry run: same build and smoke test, files kept as workflow artifacts, no release. +**Releases.** `.github/workflows/release.yml` runs when a release is published: it builds `labconstrictor-qupath-.jar` (the tag is the version in the jar's manifest), starts that jar in QuPath 0.7.0 on a virtual screen (`tests/smoke_jar.sh`: the menu entry exists and the dialog opens), and only then attaches the jar, `LabConstrictorTools-all.groovy` and `SHA256SUMS` to the release and appends the QuPath and Tools versions to its notes. Run it by hand (Actions > release > Run workflow, with a tag name) for a dry run: same build and smoke test, files kept as workflow artifacts, no release. The same jar is then tried on Windows and macOS (`os-smoke` jobs, `tests/smoke_os.sh`, apps registered by `tests/ci_setup_os.sh`): best effort, they never block the release (`publish` waits for the Linux job only). QUPATH=/path/to/QuPath JAVA_HOME= ./build.sh # QuPath 0.7 is built with Java 25 QUPATH=/path/to/QuPath LC_HOME= tests/run_gui_test.sh # needs xvfb; 21 checks