From 619b6ed73f6ef3edea9a0164575e72cbc4a1facb Mon Sep 17 00:00:00 2001 From: Anton Sundqvist Date: Wed, 30 Sep 2026 08:46:52 +0300 Subject: [PATCH 1/6] Add LUnit support on linux --- .github/labview-ci/README.md | 2 +- .github/labview-ci/catalog.json | 10 +- .github/labview-ci/config.example.yml | 13 + .../notes/linux-lunit-unit-tests.md | 7 + .../labview-ci/providers/gitlab/files.json | 4 +- .../gitlab/templates/unit-tests-linux.yml | 76 ++++ .github/labview/build-unittest-report.py | 34 +- .github/labview/run-unit-tests.ps1 | 5 + .github/labview/run-unit-tests.sh | 347 ++++++++++++++++++ .github/labview/vipm/ci-tooling.packages.json | 4 +- .github/pages/documentation.html | 36 +- .github/pages/unit-tests.html | 2 + .../workflows/unit-tests-linux-container.yml | 257 +++++++++++++ actions/dashboard/dashboard.py | 9 +- actions/dashboard/documentation.html | 36 +- actions/dashboard/unit-tests.html | 2 + 16 files changed, 815 insertions(+), 29 deletions(-) create mode 100644 .github/labview-ci/notes/linux-lunit-unit-tests.md create mode 100644 .github/labview-ci/providers/gitlab/templates/unit-tests-linux.yml create mode 100644 .github/labview/run-unit-tests.sh create mode 100644 .github/workflows/unit-tests-linux-container.yml diff --git a/.github/labview-ci/README.md b/.github/labview-ci/README.md index c7f73726..05108d4f 100644 --- a/.github/labview-ci/README.md +++ b/.github/labview-ci/README.md @@ -12,7 +12,7 @@ installed into any LabVIEW repository — from the command line or from the | **VIDiff** | Side-by-side visual diff reports per changed VI; PR comments | Windows / Linux | | **VI Snapshots** | Browseable gallery of every VI's block diagram (the VI Browser) | Windows | | **Shared image** | Builds the LabVIEW CI container image in the provider's project registry | Windows / Linux | -| **Unit Tests** | *Planned* — placeholder showing how new capabilities slot in | Windows / Linux | +| **Unit Tests** | Runs LUnit tests headlessly and publishes a JUnit-based report (`CI / Unit Tests`, `CI / Unit Tests (Linux)`) | Windows / Linux | Everything is driven by [`catalog.json`](catalog.json) — a single capability registry that **both** the configurator UI and the installer read. Adding a new diff --git a/.github/labview-ci/catalog.json b/.github/labview-ci/catalog.json index 8c98bc27..9d1a58aa 100644 --- a/.github/labview-ci/catalog.json +++ b/.github/labview-ci/catalog.json @@ -4736,7 +4736,8 @@ "status": "stable", "recommended": false, "supportsOs": [ - "windows" + "windows", + "linux" ], "requires": [], "recommends": [ @@ -4752,9 +4753,12 @@ ".github/workflows/unit-tests-windows-container.yml", ".github/labview/run-unit-tests.ps1" ], - "linux": [] + "linux": [ + ".github/workflows/unit-tests-linux-container.yml", + ".github/labview/run-unit-tests.sh" + ] }, - "notes": "Runs the configured LabVIEW unit-test frameworks headlessly in the Windows worker, normalises their JUnit output into one report, and links each failing test to the VI under test in the VI Browser. Supports Caraya, LUnit, JKI VI Tester, and the NI Unit Test Framework. Windows-only because the frameworks ship as VIPM packages; choose which tools run and where their tests live on the Unit Testing page." + "notes": "Runs the configured LabVIEW unit-test frameworks headlessly in the worker, normalises their JUnit output into one report, and links each failing test to the VI under test in the VI Browser. Supports Caraya, LUnit, JKI VI Tester, and the NI Unit Test Framework on Windows. LUnit also runs on Linux (status CI / Unit Tests (Linux)). Choose which tools run and where their tests live on the Unit Testing page." }, { "id": "antidoc", diff --git a/.github/labview-ci/config.example.yml b/.github/labview-ci/config.example.yml index 53555cb3..b82897e6 100644 --- a/.github/labview-ci/config.example.yml +++ b/.github/labview-ci/config.example.yml @@ -85,6 +85,19 @@ activities: - masscompile - vi-analyzer - vidiff + # - unit-tests + +# Unit tests (read when the unit-tests activity is installed). The Unit Testing +# page writes this block. LUnit runs on Windows and Linux. +# unitTests: +# tools: +# lunit: +# enabled: true +# locations: +# - "tests/" # a directory, a glob, or a file extension; empty = whole repo +# # Optional override, same tokens on both platforms ({cli} {lv} {dir} {out} {ver}). +# # Keep -Headless last: on Linux, LabVIEWCLI ignores any argument after it. +# # command: '"{cli}" -LogToConsole TRUE -OperationName LUnit -Path "{dir}" -ReportPath "{out}" -LabVIEWPath "{lv}" -Headless' # ---------------------------------------------------------------------------- # GitHub Actions variables the GitHub workflows honor at runtime (set with diff --git a/.github/labview-ci/notes/linux-lunit-unit-tests.md b/.github/labview-ci/notes/linux-lunit-unit-tests.md new file mode 100644 index 00000000..b6be5a2a --- /dev/null +++ b/.github/labview-ci/notes/linux-lunit-unit-tests.md @@ -0,0 +1,7 @@ +Unit Tests now run LUnit in the Linux worker as well. Installing Unit Tests with +Linux adds unit-tests-linux-container.yml and run-unit-tests.sh, which publish a +Linux report under unit-tests//linux/ with the commit status +CI / Unit Tests (Linux); the dashboard shows the Windows / Linux split and the +report's platform toggle switches between them. Keep -Headless as the last +argument of any unitTests command: override, since Linux LabVIEWCLI ignores +anything after it. diff --git a/.github/labview-ci/providers/gitlab/files.json b/.github/labview-ci/providers/gitlab/files.json index 75fbd871..e313f603 100644 --- a/.github/labview-ci/providers/gitlab/files.json +++ b/.github/labview-ci/providers/gitlab/files.json @@ -16,6 +16,7 @@ "templates/snapshots-2-windows.yml", "templates/snapshots-windows.yml", "templates/unit-tests-windows.yml", + "templates/unit-tests-linux.yml", "templates/vi-analyzer-linux.yml", "templates/vi-analyzer-windows.yml", "templates/antidoc-windows.yml", @@ -44,7 +45,8 @@ "linux": "templates/vi-analyzer-linux.yml" }, "unit-tests": { - "windows": "templates/unit-tests-windows.yml" + "windows": "templates/unit-tests-windows.yml", + "linux": "templates/unit-tests-linux.yml" }, "antidoc": { "windows": "templates/antidoc-windows.yml" diff --git a/.github/labview-ci/providers/gitlab/templates/unit-tests-linux.yml b/.github/labview-ci/providers/gitlab/templates/unit-tests-linux.yml new file mode 100644 index 00000000..e435d753 --- /dev/null +++ b/.github/labview-ci/providers/gitlab/templates/unit-tests-linux.yml @@ -0,0 +1,76 @@ +lvci:unit-tests-linux: + extends: .lvci:linux-docker + stage: verify + needs: + - job: lvci:worker-linux + optional: true + artifacts: false + rules: + - if: '$CI_PIPELINE_SOURCE == "web"' + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: manual + allow_failure: true + - changes: + - "**/*.vi" + - "**/*.ctl" + - "**/*.lvproj" + - "**/*.lvlib" + - "**/*.lvclass" + - "**/*.lvtest" + script: + - | + set +e + report_dir="ci-out/unit-tests" + results_dir="$report_dir/results" + mkdir -p "$results_dir" + if [ -n "${LVCI_LINUX_IMAGE:-}" ]; then + image="$LVCI_LINUX_IMAGE" + elif [ "${LVCI_USE_CUSTOM_IMAGE:-false}" = "true" ]; then + image="${CI_REGISTRY_IMAGE}/labview:linux-latest" + else + image="nationalinstruments/labview:latest-linux" + fi + if [[ "$image" == "${CI_REGISTRY_IMAGE}"/* ]]; then + echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password-stdin + fi + docker pull "$image" + pull_exit=$? + run_exit=1 + if [ "$pull_exit" -eq 0 ]; then + # One fresh container per run; run-unit-tests.sh always exits 0 and + # pass/fail comes from the JUnit files. + docker run --rm \ + -e "GITHUB_REPOSITORY=$CI_PROJECT_PATH" \ + -e "GITHUB_SHA=$CI_COMMIT_SHA" \ + -v "$CI_PROJECT_DIR:/workspace" \ + "$image" \ + bash /workspace/.github/labview/run-unit-tests.sh /workspace "/workspace/$results_dir" + run_exit=$? + else + printf 'Could not pull LabVIEW image %s\n' "$image" > "$results_dir/runner-error.txt" + fi + python3 .github/labview/build-unittest-report.py \ + --results "$results_dir" \ + --out "$report_dir" \ + --workspace "$CI_PROJECT_DIR" \ + --sha "$CI_COMMIT_SHA" \ + --platform linux \ + --repo "$CI_PROJECT_PATH" \ + --pages-url "$CI_PAGES_URL" \ + --labview-version "${LVCI_LABVIEW_VERSION:-}" \ + --commit-msg "$(git log -1 --pretty=%s "$CI_COMMIT_SHA")" \ + --author "$(git log -1 --pretty=%an "$CI_COMMIT_SHA")" \ + --date "$(git log -1 --pretty=%cI "$CI_COMMIT_SHA")" || true + python3 .gitlab/labview-ci/scripts/stage-report.py \ + --source "$report_dir" \ + --destination "unit-tests/$CI_COMMIT_SHA/linux" \ + --fallback "Unit tests (Linux) did not produce a report. See the GitLab job log." + exit "$run_exit" + artifacts: + when: always + expire_in: 30 days + reports: + junit: ci-out/unit-tests/results/*.xml + paths: + - ci-out/unit-tests/ + - ci-out/gitlab-pages/unit-tests/$CI_COMMIT_SHA/linux/ diff --git a/.github/labview/build-unittest-report.py b/.github/labview/build-unittest-report.py index f8c9fd7f..68524b5b 100644 --- a/.github/labview/build-unittest-report.py +++ b/.github/labview/build-unittest-report.py @@ -306,9 +306,10 @@ def build_data(args) -> dict: platforms = [{"id": "windows", "url": "../results.json"}, {"id": "linux", "url": None}] snap_depth = "../../../" - # Optional marker written by run-unit-tests.ps1 when a configured tool could - # not run because the selected container lacks the required tooling. Drives - # the shared "missing container tooling" banner (see tooling_banner_html). + # Optional marker written by run-unit-tests.ps1/.sh when a configured tool could + # not run because the selected container lacks the required tooling (drives the + # shared "missing container tooling" banner, see tooling_banner_html) or has no + # runner on this platform yet (see unsupported_note_html). tooling = {} res_dir = Path(args.results) if args.results else None if res_dir and (res_dir / "_tooling.json").exists(): @@ -391,6 +392,28 @@ def tooling_banner_html(missing: list, configure_url: str) -> str: ) +def unsupported_note_html(missing: list) -> str: + """Render an informational note for configured tools that have no runner on + this platform yet (kind `unsupported-on-linux`, written by run-unit-tests.sh), + so the report explains why those tools have no results here.""" + if isinstance(missing, dict): + missing = [missing] + unsup = [x for x in (missing or []) if isinstance(x, dict) and x.get("kind") == "unsupported-on-linux"] + if not unsup: + return "" + names = ", ".join(_esc(x.get("name") or x.get("tool") or "") for x in unsup if (x.get("name") or x.get("tool"))) + return ( + '
' + '' + '
Not run on Linux.' + f'{names} {"is" if len(unsup) == 1 else "are"} enabled but not supported in the Linux container yet. ' + f'{"Its" if len(unsup) == 1 else "Their"} results come from the Windows unit-test run.
' + ) + + def render(data: dict) -> str: blob = json.dumps(data, ensure_ascii=False) blob = blob.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026") @@ -409,7 +432,8 @@ def render(data: dict) -> str: dash = m.get("dash_url") or "" repo = m.get("repo") or "" cfg_url = (dash or "") + "configure.html" + ("?repo=" + quote(repo, safe="") if repo else "") - banner = tooling_banner_html((data.get("tooling") or {}).get("missing") or [], cfg_url) + missing = (data.get("tooling") or {}).get("missing") or [] + banner = tooling_banner_html(missing, cfg_url) + unsupported_note_html(missing) out = _TEMPLATE.replace("__UT_DATA_JSON__", blob) out = out.replace("__UT_HEADER_CFG__", json.dumps(hdr_cfg, ensure_ascii=False)) @@ -516,6 +540,8 @@ def main() -> None: .lvci-needtool-cta{flex:0 0 auto;align-self:center;font-size:.85em;font-weight:600;color:#fff;background:#bb8009;border-radius:7px;padding:8px 13px;text-decoration:none;white-space:nowrap} .lvci-needtool-cta:hover{background:#a17609} @media(max-width:560px){.lvci-needtool{flex-wrap:wrap}.lvci-needtool-cta{align-self:stretch;text-align:center}} +.lvci-needtool.info{background:rgba(56,139,253,.1);border-color:rgba(56,139,253,.4);border-left-color:#388bfd} +.lvci-needtool.info svg{color:#388bfd} /* snapshot drawer */ #backdrop{position:fixed;inset:0;background:rgba(1,4,9,.5);opacity:0;pointer-events:none;transition:opacity .15s;z-index:40} #backdrop.show{opacity:1;pointer-events:auto} diff --git a/.github/labview/run-unit-tests.ps1 b/.github/labview/run-unit-tests.ps1 index dac7416b..df4ee5ef 100644 --- a/.github/labview/run-unit-tests.ps1 +++ b/.github/labview/run-unit-tests.ps1 @@ -19,6 +19,11 @@ template that can be overridden from the config (`command:` key) so the precise invocation can be corrected on a real worker without editing this script. + LINUX: run-unit-tests.sh is the Linux counterpart (LUnit only for now). The two + scripts share no code but share a contract: the config shape, the command + tokens, the -.xml output names and the _tooling.json findings. A + framework added here should be added there the same way (see its header). + HEADLESS: LabVIEW must run -Headless in LabVIEW 2026+ Windows containers (same constraint run-vi-analyzer.ps1 documents) or VI Server fails with -350000. g-cli launches LabVIEW the same way, so we pass the LabVIEW path/version through. diff --git a/.github/labview/run-unit-tests.sh b/.github/labview/run-unit-tests.sh new file mode 100644 index 00000000..61ee1221 --- /dev/null +++ b/.github/labview/run-unit-tests.sh @@ -0,0 +1,347 @@ +#!/usr/bin/env bash +# ============================================================================= +# run-unit-tests.sh - Runs the configured LabVIEW unit-test frameworks in a +# Linux container +# ============================================================================= +# Linux counterpart of run-unit-tests.ps1. The two scripts share no code (the +# repo keeps per-OS runners, like run-vi-analyzer.ps1/.sh), but they share a +# CONTRACT, so one config works on both platforms and build-unittest-report.py +# treats both the same way: +# +# * Config: config.unitTests.tools. with `enabled`, an optional `command` +# override and `locations` (see read_unit_test_tools below). +# * Locations: a directory is taken as-is; a glob / file extension is expanded +# and each match contributes its parent directory; empty = whole workspace. +# * Command tokens: {cli}=LabVIEWCLI, {lv}=LabVIEW executable, {dir}=a resolved +# test-root directory, {out}=JUnit output path, {ver}=LabVIEW year. +# * Output: /-.xml (JUnit) per test root, plus +# /_tooling.json = {"missing":[{tool,name,kind,detail}]} when a +# configured tool could not run. kind is `missing-tooling` (the container +# lacks the tool; the report shows the "set up the container" banner), +# `error`, or `unsupported-on-linux` (no Linux handler yet). +# * Always exits 0: pass/fail comes from the JUnit content. +# +# Only LUnit runs on Linux today. To add a framework, write a run_() handler +# with the same contract as run_lunit() and add its id to LINUX_TOOLS. +# +# LabVIEWCLI on Linux: -Headless must be the LAST argument (anything after it is +# swallowed), and the exit code is not a result (a failed operation exits 255), +# so only the JUnit file counts. +# +# Usage (inside container, workspace mounted at /workspace): +# bash /workspace/.github/labview/run-unit-tests.sh \ +# /workspace \ +# /workspace/ci-out/unit-tests/results \ +# [/workspace/.github/labview-ci.yml] +# ============================================================================= +set -uo pipefail +shopt -s globstar nullglob dotglob + +WORKSPACE_ROOT="${1:-/workspace}" +RESULTS_DIR="${2:-/workspace/ci-out/unit-tests/results}" +CONFIG_PATH="${3:-}" +[ -n "$CONFIG_PATH" ] || CONFIG_PATH="$WORKSPACE_ROOT/.github/labview-ci.yml" +WORKSPACE_ROOT="${WORKSPACE_ROOT%/}" + +mkdir -p "$RESULTS_DIR" + +# Tools that can run in the Linux worker. To add a framework, write a +# run_() handler with the same contract and add its id here. +LINUX_TOOLS=(lunit) + +# LabVIEWCLI is on PATH in the NI Linux container; labviewprofull year varies by tag. +LABVIEWCLI="${LABVIEWCLI:-$(command -v LabVIEWCLI 2>/dev/null || true)}" +LABVIEW_EXE="${LABVIEW_EXE:-$(find /usr/local/natinst -name 'labviewprofull' 2>/dev/null | head -1)}" +LABVIEW_VERSION="${LABVIEW_VERSION:-}" +if [ -z "$LABVIEW_VERSION" ] && [[ "$LABVIEW_EXE" =~ LabVIEW-([0-9]{4}) ]]; then + LABVIEW_VERSION="${BASH_REMATCH[1]}" +fi +LABVIEW_VERSION="${LABVIEW_VERSION:-2026}" + +echo "=== Unit Tests (Linux) ===" +echo " Workspace : $WORKSPACE_ROOT" +echo " Results : $RESULTS_DIR" +echo " LabVIEW : ${LABVIEW_EXE:-} (v$LABVIEW_VERSION)" +echo " LabVIEWCLI: ${LABVIEWCLI:-}" +echo " Config : $CONFIG_PATH" +echo "" + +# --- _tooling.json findings -------------------------------------------------- +TOOLING_ISSUES=() +has_tooling_issue() { + local entry + for entry in "${TOOLING_ISSUES[@]+"${TOOLING_ISSUES[@]}"}"; do + [[ "$entry" == "{\"tool\": \"$1\","* ]] && return 0 + done + return 1 +} +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + printf '%s' "$s" +} +add_tooling_issue() { + # $1 tool id, $2 display name, $3 kind, $4 detail + TOOLING_ISSUES+=("{\"tool\": \"$(json_escape "$1")\", \"name\": \"$(json_escape "$2")\", \"kind\": \"$(json_escape "$3")\", \"detail\": \"$(json_escape "$4")\"}") +} + +tool_display_name() { + case "$1" in + lunit) echo "LUnit" ;; + caraya) echo "Caraya" ;; + vi-tester) echo "JKI VI Tester" ;; + utf) echo "NI Unit Test Framework" ;; + *) echo "$1" ;; + esac +} + +# --- Minimal reader for config.unitTests.tools -------------------------------- +# Same fixed, shallow shape as Read-UnitTestTools in run-unit-tests.ps1: +# config: +# unitTests: +# tools: +# lunit: +# enabled: true +# command: "" +# locations: +# - "tests/" +# - "**/*.lvclass" +# Prints one line per ENABLED tool: US US ... (US=\x1f). +read_unit_test_tools() { + [ -f "$1" ] || return 0 + awk ' + function flush() { + if (cur != "" && enabled) print cur "\037" cmd locs + cur = ""; enabled = 0; cmd = ""; locs = ""; inloc = 0 + } + function unquote(s) { + sub(/[[:space:]]+$/, "", s) + if (s ~ /^".*"$/) { + # YAML double-quoted scalar: drop the quotes and undo \" and \\ escapes. + s = substr(s, 2, length(s) - 2) + gsub(/\\\\/, "\001", s); gsub(/\\"/, "\"", s); gsub(/\001/, "\\", s) + } + return s + } + { gsub(/\r$/, ""); gsub(/\t/, " ") } + /^[[:space:]]*unitTests:[[:space:]]*$/ { flush(); inut = 1; intools = 0; next } + !inut { next } + /^ tools:[[:space:]]*$/ { intools = 1; next } + intools && /^ [A-Za-z0-9_.-]+:[[:space:]]*$/ { + flush() + cur = $0; sub(/^ /, "", cur); sub(/:[[:space:]]*$/, "", cur); cur = tolower(cur) + next + } + cur != "" && /^ enabled:[[:space:]]*(true|false)[[:space:]]*$/ { enabled = ($0 ~ /true/); inloc = 0; next } + cur != "" && /^ command:[[:space:]]*[^[:space:]]/ { + c = $0; sub(/^ command:[[:space:]]*/, "", c); cmd = unquote(c); inloc = 0; next + } + cur != "" && /^ locations:[[:space:]]*\[[[:space:]]*\][[:space:]]*$/ { locs = ""; inloc = 0; next } + cur != "" && /^ locations:[[:space:]]*$/ { inloc = 1; next } + cur != "" && inloc && /^ -[[:space:]]*/ { + l = $0; sub(/^ -[[:space:]]*/, "", l); l = unquote(l) + if (l != "") locs = locs "\037" l + next + } + /^[^[:space:]]/ { flush(); inut = 0; intools = 0 } + END { flush() } + ' "$1" +} + +# --- Resolve locations (dir OR glob/extension) to test-root directories ------- +# Mirrors Resolve-TestRoots: a directory is taken as-is; a glob / extension is +# expanded (recursively when it has no path separator) and each match contributes +# its parent directory. Prints absolute, de-duplicated paths. +resolve_test_roots() { + local loc full match + { + for loc in "$@"; do + [ -n "$loc" ] || continue + loc="${loc//\\//}" + loc="${loc#./}" + full="$WORKSPACE_ROOT/${loc%/}" + if [ -d "$full" ]; then + (cd "$full" && pwd) + continue + fi + local pattern="$full" + [[ "$loc" == */* ]] || pattern="$WORKSPACE_ROOT/**/$loc" + for match in $pattern; do + if [ -f "$match" ]; then + (cd "$(dirname "$match")" && pwd) + fi + done + done + } | sort -u +} + +# --- Run a LabVIEWCLI command and echo the session log on failure ------------- +# $1 tool id, $2 command. Sets CLI_OUTPUT to the console output + session log. +CLI_OUTPUT="" +run_cli_command() { + local id="$1" cmd="$2" out_file + out_file="$(mktemp)" + echo " [$id] $cmd" + bash -c "$cmd" 2>&1 | tee "$out_file" + echo " [$id] exit=${PIPESTATUS[0]} (not a result; the JUnit file is)" + CLI_OUTPUT="$(cat "$out_file")" + rm -f "$out_file" +} + +print_session_log() { + # $1 tool id. The console error is generic; the real detail lives in the CLI's + # own session log. Appends the log to CLI_OUTPUT so it can be classified too. + local id="$1" log_path="" + local re=$'[Ss]tarted logging in file:[ \t]*([^\n]*\\.log)' + [[ "$CLI_OUTPUT" =~ $re ]] && log_path="${BASH_REMATCH[1]}" + if [ -z "$log_path" ]; then + echo " [$id] (no CLI session-log path found in output)" + return 0 + fi + echo " [$id] --- LabVIEW CLI session log ($log_path) ---" + if [ -f "$log_path" ]; then + sed "s/^/ [$id-log] /" "$log_path" + CLI_OUTPUT="$CLI_OUTPUT"$'\n'"$(cat "$log_path")" + else + echo " [$id] (session log not found on disk)" + fi + echo " [$id] --- end LabVIEW CLI session log ---" +} + +expand_tokens() { + # $1 template, $2 {dir}, $3 {out} + local s="$1" + s="${s//\{cli\}/$LABVIEWCLI}" + s="${s//\{lv\}/$LABVIEW_EXE}" + s="${s//\{dir\}/$2}" + s="${s//\{out\}/$3}" + s="${s//\{ver\}/$LABVIEW_VERSION}" + printf '%s' "$s" +} + +# --- LUnit (Astemes) ---------------------------------------------------------- +# The LUnit CLI package (astemes_lib_lunit_cli) registers the +# "LUnit" LabVIEWCLI operation, which discovers Test Case classes under -Path and +# writes a JUnit report when -ReportPath ends in .xml. The default command is +# token-for-token the Windows one, so a `command:` override works on both. +LUNIT_DEFAULT_CMD='"{cli}" -LogToConsole TRUE -OperationName LUnit -Path "{dir}" -ReportPath "{out}" -LabVIEWPath "{lv}" -Headless' + +# Contract: run_ ... +run_lunit() { + local id="$1" index="$2" override="$3" + shift 3 + local tmpl="${override:-$LUNIT_DEFAULT_CMD}" + if [ -n "$override" ] && [[ ! "$override" =~ -Headless[[:space:]]*$ ]]; then + echo " WARNING: [$id] the command: override does not end with -Headless. On Linux, LabVIEWCLI swallows any argument after -Headless; keep it last." >&2 + fi + if [ -z "$LABVIEWCLI" ] || [ -z "$LABVIEW_EXE" ]; then + echo " WARNING: [$id] LabVIEWCLI or labviewprofull not found; cannot run LUnit." >&2 + has_tooling_issue "$id" || add_tooling_issue "$id" "LUnit" "missing-tooling" \ + "LabVIEWCLI or the LabVIEW executable was not found in this container." + return 0 + fi + local i=0 dir out cmd + for dir in "$@"; do + out="$RESULTS_DIR/$id-$(( index * 100 + i )).xml" + echo " [$id] path: $dir" + cmd="$(expand_tokens "$tmpl" "$dir" "$out")" + run_cli_command "$id" "$cmd" + if [ -f "$out" ]; then + echo " [$id] wrote $out" + else + echo " WARNING: [$id] produced no JUnit at $out (check the LUnit output above; override with the tool's command: key)." >&2 + print_session_log "$id" + if ! has_tooling_issue "$id"; then + if printf '%s' "$CLI_OUTPUT" | grep -qiE '350053|missing or bad files|required modules or toolkits'; then + add_tooling_issue "$id" "LUnit" "missing-tooling" \ + "The LUnit CLI toolkit (astemes_lib_lunit_cli) is not installed in this container, so the LabVIEW CLI LUnit operation could not load (error -350053)." + elif printf '%s' "$CLI_OUTPUT" | grep -q '350006'; then + add_tooling_issue "$id" "LUnit" "missing-tooling" \ + "The LabVIEW CLI LUnit operation was not found (error -350006). The LUnit CLI toolkit (astemes_lib_lunit_cli) is not installed in this container." + else + add_tooling_issue "$id" "LUnit" "error" "The LUnit operation produced no JUnit output." + fi + fi + fi + i=$(( i + 1 )) + done +} + +# --- Main --------------------------------------------------------------------- +is_linux_tool() { + local t + for t in "${LINUX_TOOLS[@]}"; do [ "$t" = "$1" ] && return 0; done + return 1 +} + +TOOL_LINES=() +while IFS= read -r line; do + [ -n "$line" ] && TOOL_LINES+=("$line") +done < <(read_unit_test_tools "$CONFIG_PATH") + +if [ "${#TOOL_LINES[@]}" -eq 0 ]; then + echo "WARNING: No config.unitTests.tools configured in $CONFIG_PATH - nothing to run." >&2 + echo "Wrote 0 JUnit file(s) to $RESULTS_DIR." + exit 0 +fi + +tool_ids=() +for line in "${TOOL_LINES[@]}"; do tool_ids+=("${line%%$'\x1f'*}"); done +echo "Configured tools: ${tool_ids[*]}" +echo "" + +idx=0 +for line in "${TOOL_LINES[@]}"; do + IFS=$'\x1f' read -r -a fields <<< "$line" + tool="${fields[0]}" + command_override="${fields[1]:-}" + locations=("${fields[@]:2}") + name="$(tool_display_name "$tool")" + echo "--- tool: $tool ($name) ---" + + if ! is_linux_tool "$tool"; then + echo " $name ($tool) is not supported on Linux yet - skipping. It runs in the Windows unit-test workflow." + add_tooling_issue "$tool" "$name" "unsupported-on-linux" \ + "$name does not run in the Linux container yet; its results come from the Windows unit-test run." + idx=$(( idx + 1 )); echo "" + continue + fi + + roots=() + if [ "${#locations[@]}" -gt 0 ]; then + while IFS= read -r r; do [ -n "$r" ] && roots+=("$r"); done < <(resolve_test_roots "${locations[@]}") + else + # Empty locations means "the whole project". + roots=("$WORKSPACE_ROOT") + fi + if [ "${#roots[@]}" -eq 0 ]; then + echo " WARNING: no test locations resolved for '$tool' (locations: ${locations[*]}) - skipping." >&2 + idx=$(( idx + 1 )); echo "" + continue + fi + + "run_${tool//-/_}" "$tool" "$idx" "$command_override" "${roots[@]}" + idx=$(( idx + 1 )); echo "" +done + +xml_count=0 +for f in "$RESULTS_DIR"/*.xml; do [ -f "$f" ] && xml_count=$(( xml_count + 1 )); done +echo "=== Unit Tests finished: wrote $xml_count JUnit file(s) to $RESULTS_DIR ===" + +# Persist findings for the report builder (same shape as the Windows runner). +if [ "${#TOOLING_ISSUES[@]}" -gt 0 ]; then + { + printf '{"missing": [\n' + for n in "${!TOOLING_ISSUES[@]}"; do + sep=","; [ "$n" -eq $(( ${#TOOLING_ISSUES[@]} - 1 )) ] && sep="" + printf ' %s%s\n' "${TOOLING_ISSUES[$n]}" "$sep" + done + printf ']}\n' + } > "$RESULTS_DIR/_tooling.json" + echo "Recorded ${#TOOLING_ISSUES[@]} tooling finding(s) -> $RESULTS_DIR/_tooling.json" +fi + +# Always exit 0: pass/fail is derived from the JUnit content by +# build-unittest-report.py, exactly like the Windows runner. +exit 0 diff --git a/.github/labview/vipm/ci-tooling.packages.json b/.github/labview/vipm/ci-tooling.packages.json index 7bee8aa4..b810e232 100644 --- a/.github/labview/vipm/ci-tooling.packages.json +++ b/.github/labview/vipm/ci-tooling.packages.json @@ -62,8 +62,8 @@ "name": "astemes_lib_lunit_cli", "version": null, "include": true, - "summary": "LUnit CLI by Astemes - registers the native 'LabVIEWCLI -OperationName LUnit' operation that run-unit-tests.ps1 invokes headlessly to run LUnit Test Cases and emit a JUnit report (.xml -ReportPath).", - "note": "REQUIRED for headless LUnit, exactly as ni_lib_utf_junit_report is for UTF: without it the 'LUnit' CLI operation cannot load and the runner fails with -350053. Its VIPM install must run AS ADMINISTRATOR on Windows (it registers a LabVIEW CLI operation under Program Files); the worker image build runs VIPM as ContainerAdministrator, so this is satisfied. Published in the VIPM Community (JKI) index. Its declared dependency astemes_lib_lunit_cli_system ships INSIDE this .vip and is not published standalone, so the resolver skips that unresolvable transitive dependency with a note. Version null (latest)." + "summary": "LUnit CLI by Astemes - registers the native 'LabVIEWCLI -OperationName LUnit' operation that run-unit-tests.ps1 (Windows) and run-unit-tests.sh (Linux) invoke headlessly to run LUnit Test Cases and emit a JUnit report (.xml -ReportPath).", + "note": "REQUIRED for headless LUnit on BOTH Windows and Linux, exactly as ni_lib_utf_junit_report is for UTF: without it the 'LUnit' CLI operation cannot load and the runner fails with -350053. It installs to vi.lib/Astemes/LUnit CLI and a post-install action registers the operation with the LabVIEW CLI (on Linux under /usr/local/natinst/nilvcli/Operations/LUnitCLI); that action runs inside LabVIEW, so VIPM needs a running LabVIEW during the install, which install-vipc-linux.sh starts headless. The worker builds run as ContainerAdministrator / root, so the registration succeeds. Published in the VIPM Community (JKI) index. Version null (latest)." } ] }, diff --git a/.github/pages/documentation.html b/.github/pages/documentation.html index 43eb387d..3e97f06f 100644 --- a/.github/pages/documentation.html +++ b/.github/pages/documentation.html @@ -399,7 +399,7 @@

5. The activities & when to use each Mass CompileCompiles every VI/CTL and flags broken VIs and missing dependencies.Win Linux VI AnalyzerRuns NI's static-analysis test suite for correctness, performance, style and documentation.Win Linux VIDiffGenerates visual, side-by-side front-panel + block-diagram diffs of changed VIs between commits.Win Linux - Unit TestsRuns Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report.Win + Unit TestsRuns Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report. LUnit also runs on Linux.Win Linux AntidocGenerates project documentation from the VI hierarchy using Wovalab's Antidoc CLI.Win VI Snapshots / BrowserRenders every VI to a content-addressed HTML snapshot gallery (the classic VI Browser).Win VI Snapshots 2.0Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine.Win Linux @@ -1658,12 +1658,12 @@

16.4 Unit Tests — a fully worked example windows-2022. + Per-commit workflowunit-tests-windows-container.yml on windows-2022; unit-tests-linux-container.yml on ubuntu-latest. History backfillunit-tests-backfill-windows.yml + unit-tests-backfill.ps1 — one warm container across many revisions. - Output & statusReport deploys to unit-tests/<sha>/ on gh-pages; commit-status context is CI / Unit Tests. - PlatformWindows only — every framework depends on baked VIPM/UTF tooling (§15). + Output & statusReport deploys to unit-tests/<sha>/ on gh-pages with commit-status context CI / Unit Tests; the Linux run deploys to unit-tests/<sha>/linux/ with CI / Unit Tests (Linux). + PlatformWindows; LUnit also runs on Linux. Every framework depends on baked VIPM/UTF tooling (§15). @@ -1680,6 +1680,11 @@

The four frameworks & the exact headless command each runs

NI UTF.lvtest items inside a .lvprojnative LabVIEWCLI"{cli}" -LogToConsole TRUE -OperationName RunUnitTests -ProjectPath "{proj}" -JUnitReportPath "{out}" -LabVIEWPath "{lv}" -Headless +

On Linux, LUnit runs with the same default command and tokens ({lv} is + labviewprofull, {cli} is LabVIEWCLI), so one command: line + works on both platforms. Keep -Headless as the last argument of any override: on + Linux, LabVIEWCLI swallows whatever follows it, and the Linux driver warns when an override doesn’t + end with it.

Tokens the driver substitutes: {ver} = LabVIEW year, {dir} = a resolved test-root directory, {proj} = a resolved .lvproj, {out} = the JUnit output path, {lv} = LabVIEW.exe, {cli} = LabVIEWCLI.exe.

@@ -1758,6 +1763,22 @@

Invoking LabVIEW headless inside the container

  • Runs each command via cmd.exe /c, capturing exit code and output; on a failed UTF/LUnit run it echoes the LabVIEW CLI session log (the console error is generic) and records a tooling finding.
  • +

    The Linux run

    +

    unit-tests-linux-container.yml follows the same step order on ubuntu-latest, + with the Linux image selection used by the other Linux activities (config.container.actions.unit-tests + → config.container.use → the shared Linux worker :linux-latest; + base/none selects nationalinstruments/labview:latest-linux). It runs the + driver in a fresh container:

    +
    docker run --rm -v "$GITHUB_WORKSPACE:/workspace" "$LABVIEW_CONTAINER_IMAGE" \
    +  bash /workspace/.github/labview/run-unit-tests.sh /workspace /workspace/ci-out/unit-tests/results
    +
      +
    • LUnit. The driver dispatches each enabled tool to a run_<id>() handler + listed in LINUX_TOOLS; lunit is the only one.
    • +
    • Exit codes are not results. On Linux, LabVIEWCLI exits 255 whenever an operation + fails, so the driver only trusts the JUnit file and always exits 0, like the Windows driver.
    • +
    • History backfill has no Linux workflow yet; use the per-revision Run button.
    • +
    +

    The output contract

    The container emits raw JUnit XML; the host builder normalises it. Two files matter:

    +

    The Linux run

    +

    unit-tests-linux-container.yml follows the same step order on ubuntu-latest, + with the Linux image selection used by the other Linux activities (config.container.actions.unit-tests + → config.container.use → the shared Linux worker :linux-latest; + base/none selects nationalinstruments/labview:latest-linux). It runs the + driver in a fresh container:

    +
    docker run --rm -v "$GITHUB_WORKSPACE:/workspace" "$LABVIEW_CONTAINER_IMAGE" \
    +  bash /workspace/.github/labview/run-unit-tests.sh /workspace /workspace/ci-out/unit-tests/results
    +
      +
    • LUnit. The driver dispatches each enabled tool to a run_<id>() handler + listed in LINUX_TOOLS; lunit is the only one.
    • +
    • Exit codes are not results. On Linux, LabVIEWCLI exits 255 whenever an operation + fails, so the driver only trusts the JUnit file and always exits 0, like the Windows driver.
    • +
    • History backfill has no Linux workflow yet; use the per-revision Run button.
    • +
    +

    The output contract

    The container emits raw JUnit XML; the host builder normalises it. Two files matter: