diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b134c6c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,185 @@ +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") 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: + 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 || 'v0.0.0-dryrun' }} +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 + 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 + 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: + - 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 / + 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 + - name: Smoke test the jar and the OS-specific code paths + shell: bash + run: | + 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/README.md b/README.md index 2babc11..8a43e41 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,22 @@ 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 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. +* "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 +119,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. 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 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/ci_setup_os.sh b/tests/ci_setup_os.sh new file mode 100755 index 0000000..e2fb4fe --- /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 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 diff --git a/tests/smoke_jar.groovy b/tests/smoke_jar.groovy new file mode 100644 index 0000000..a8dc5bf --- /dev/null +++ b/tests/smoke_jar.groovy @@ -0,0 +1,45 @@ +// 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). 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 +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] +} +println "SMOKE start" +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 + // 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") + 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..4b025ba --- /dev/null +++ b/tests/smoke_jar.sh @@ -0,0 +1,26 @@ +#!/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= +# 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" diff --git a/tests/smoke_os.sh b/tests/smoke_os.sh new file mode 100755 index 0000000..31c4c48 --- /dev/null +++ b/tests/smoke_os.sh @@ -0,0 +1,60 @@ +#!/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 + 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" +# 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:-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() { # + 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' +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) +# 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" + 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 +[ -z "${GITHUB_STEP_SUMMARY:-}" ] || cat "$summary" >> "$GITHUB_STEP_SUMMARY" +cat "$summary"; exit $rc