From 9d3c6f8065ea76319fd1980ccfe2933ab4b57640 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 11 May 2026 19:27:19 +0800 Subject: [PATCH 1/2] test: add UI smoke tests for axis, touchy, gmoccapy, qtdragon Adds a minimal harness under tests/ui-smoke/ that launches each GUI against its sim config under xvfb-run and verifies it reaches the 'task ready' NML state without crashing. Auto-discovered by scripts/runtests via per-GUI test.sh + checkresult + skip files. Layout: _lib/launch.sh - spawns linuxcnc -r under xvfb, runs driver, handles clean shutdown (group-SIGTERM with 60s wait, escalate to SIGKILL + shm cleanup) _lib/drive.py - polls linuxcnc.stat() until task ready, prints UI_SMOKE_OK / UI_SMOKE_FAIL _lib/checkresult.sh - grep for UI_SMOKE_OK / absence of FAIL _lib/skip-if-missing.sh - skip when xvfb-run absent (dev env) _lib/cleanup-runtime.sh - pre/post belt-and-braces daemon + shm cleanup; SHM key list mirrors scripts/runtests:157 (full 6-key set) _lib/run-gui.sh - dispatcher taking a relpath under configs/sim/, exec'd by per-GUI test.sh axis|touchy|gmoccapy|qtdragon/test.sh - one-line wrappers Force software OpenGL via LIBGL_ALWAYS_SOFTWARE + Qt RHI/QSG/QtQuick software backends; CI runners have no GPU and Qt GL paths segfault on headless display. Skip vs fail policy (BsAtHome / hdiethelm review): only xvfb-run absence skips; missing Python/typelib deps fail loudly so review catches them. Required deps are gated under !nocheck in debian/control.top.in (separate commit). --- tests/ui-smoke/.gitignore | 7 ++ tests/ui-smoke/README | 26 ++++++ tests/ui-smoke/_lib/checkresult.sh | 32 ++++++++ tests/ui-smoke/_lib/cleanup-runtime.sh | 30 +++++++ tests/ui-smoke/_lib/drive.py | 62 +++++++++++++++ tests/ui-smoke/_lib/launch.sh | 105 +++++++++++++++++++++++++ tests/ui-smoke/_lib/run-gui.sh | 15 ++++ tests/ui-smoke/_lib/skip-if-missing.sh | 17 ++++ tests/ui-smoke/axis/checkresult | 2 + tests/ui-smoke/axis/skip | 2 + tests/ui-smoke/axis/test.sh | 2 + tests/ui-smoke/gmoccapy/checkresult | 2 + tests/ui-smoke/gmoccapy/skip | 2 + tests/ui-smoke/gmoccapy/test.sh | 2 + tests/ui-smoke/qtdragon/checkresult | 2 + tests/ui-smoke/qtdragon/skip | 2 + tests/ui-smoke/qtdragon/test.sh | 2 + tests/ui-smoke/touchy/checkresult | 2 + tests/ui-smoke/touchy/skip | 2 + tests/ui-smoke/touchy/test.sh | 2 + 20 files changed, 318 insertions(+) create mode 100644 tests/ui-smoke/.gitignore create mode 100644 tests/ui-smoke/README create mode 100755 tests/ui-smoke/_lib/checkresult.sh create mode 100755 tests/ui-smoke/_lib/cleanup-runtime.sh create mode 100755 tests/ui-smoke/_lib/drive.py create mode 100755 tests/ui-smoke/_lib/launch.sh create mode 100755 tests/ui-smoke/_lib/run-gui.sh create mode 100755 tests/ui-smoke/_lib/skip-if-missing.sh create mode 100755 tests/ui-smoke/axis/checkresult create mode 100755 tests/ui-smoke/axis/skip create mode 100755 tests/ui-smoke/axis/test.sh create mode 100755 tests/ui-smoke/gmoccapy/checkresult create mode 100755 tests/ui-smoke/gmoccapy/skip create mode 100755 tests/ui-smoke/gmoccapy/test.sh create mode 100755 tests/ui-smoke/qtdragon/checkresult create mode 100755 tests/ui-smoke/qtdragon/skip create mode 100755 tests/ui-smoke/qtdragon/test.sh create mode 100755 tests/ui-smoke/touchy/checkresult create mode 100755 tests/ui-smoke/touchy/skip create mode 100755 tests/ui-smoke/touchy/test.sh diff --git a/tests/ui-smoke/.gitignore b/tests/ui-smoke/.gitignore new file mode 100644 index 00000000000..05fea704e9d --- /dev/null +++ b/tests/ui-smoke/.gitignore @@ -0,0 +1,7 @@ +# Runtime artifacts left by launch.sh; per Bertho's clean-tree rule, +# generated build/test outputs must be gitignored or committed. +linuxcnc.out +linuxcnc.err +linuxcnc.pid +ui-smoke.out +ui-smoke.err diff --git a/tests/ui-smoke/README b/tests/ui-smoke/README new file mode 100644 index 00000000000..eb9ea8ef0ff --- /dev/null +++ b/tests/ui-smoke/README @@ -0,0 +1,26 @@ +UI smoke tests +~~~~~~~~~~~~~~ + +These tests launch each GUI (axis, touchy, gmoccapy, qtdragon) under +xvfb-run against an existing sim config and verify the GUI starts and +the NML task is reachable. Phase 1 ("does it start"); functional +checks (load G-code, jog, MDI) belong in tests/ui-functional/. + +Each test directory contains: + test.sh launches the GUI under xvfb-run, runs drive.py + checkresult examines the captured output for crash markers + skip skips this test only when xvfb-run is not on the host + +Shared helpers live in _lib/: + drive.py common NML driver, prints UI_SMOKE_OK on success + launch.sh xvfb-run wrapper, signal escalation for shutdown + cleanup-runtime.sh belt-and-suspenders: kill stray daemons, ipcrm + shared memory, drop /tmp/linuxcnc.lock + checkresult.sh shared pass/fail predicate + skip-if-missing.sh shared skip predicate + +Skip vs fail policy: the only condition we skip on is xvfb-run absence +(rare local dev env). Python and gi typelib deps the GUIs need are +declared in debian/control under !nocheck so apt-get build-dep +installs them on CI; if they are missing the test should fail loudly +rather than silently skip, so missing deps surface during review. diff --git a/tests/ui-smoke/_lib/checkresult.sh b/tests/ui-smoke/_lib/checkresult.sh new file mode 100755 index 00000000000..b8760481ab6 --- /dev/null +++ b/tests/ui-smoke/_lib/checkresult.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Shared result check for UI smoke tests. +# +# Pass if the driver printed UI_SMOKE_OK and did not print UI_SMOKE_FAIL. +# The driver only emits UI_SMOKE_OK after a successful NML round-trip +# (linuxcnc task ready and stat.poll() still alive after settle), so +# this is sufficient evidence that the GUI booted. We do not grep for +# generic crash markers like "Segmentation fault" or "Traceback": +# linuxcnc's own scripts/linuxcnc Cleanup may emit shutdown-side +# segfaults (Qt/GTK teardown races) that are out of scope for a +# startup smoke test, and the driver-signal approach catches the +# startup-time failures we actually care about. +set -u + +if [ $# -lt 1 ]; then + echo "FAIL: checkresult requires the result-log path as argument" >&2 + exit 1 +fi + +LOG="$1" + +if grep -q '^UI_SMOKE_FAIL' "$LOG"; then + echo "FAIL: driver reported UI_SMOKE_FAIL" >&2 + exit 1 +fi + +if ! grep -q '^UI_SMOKE_OK$' "$LOG"; then + echo "FAIL: driver did not report UI_SMOKE_OK" >&2 + exit 1 +fi + +exit 0 diff --git a/tests/ui-smoke/_lib/cleanup-runtime.sh b/tests/ui-smoke/_lib/cleanup-runtime.sh new file mode 100755 index 00000000000..179d6bb8707 --- /dev/null +++ b/tests/ui-smoke/_lib/cleanup-runtime.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Reset linuxcnc-related runtime state so the next ui-smoke test starts +# from a clean environment. Used as both a pre-launch belt-and-braces +# cleanup and as a post-shutdown last-resort if scripts/linuxcnc's own +# SIGTERM trap could not reap everything in time. +# +# SHM_KEYS mirrors SHMEM_KEY in scripts/runtests:157. If a ui-smoke +# crash leaks any of these, the next runtests invocation aborts in +# test_shmem(); we must clean the full set. + +set -u + +DAEMONS=(linuxcncsvr milltask halui rtapi_app) +SHM_KEYS=(0x00000064 0x48414c32 0x48484c34 0x90280a48 0x130cf406 0x434c522b) + +for proc in "${DAEMONS[@]}"; do + pkill -KILL -x "$proc" 2>/dev/null || true +done + +rm -f /tmp/linuxcnc.lock +halrun -U 2>/dev/null || true + +for key in "${SHM_KEYS[@]}"; do + shmid=$(LC_ALL=C ipcs -m | awk -v k="$key" 'tolower($1)==k {print $2}') + if [ -n "$shmid" ]; then + ipcrm -m "$shmid" 2>/dev/null || true + fi +done + +exit 0 diff --git a/tests/ui-smoke/_lib/drive.py b/tests/ui-smoke/_lib/drive.py new file mode 100755 index 00000000000..1ee90a42234 --- /dev/null +++ b/tests/ui-smoke/_lib/drive.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# Minimal UI smoke driver: confirm linuxcnc task came up and the GUI +# did not crash. The smoke layer answers Bertho's "does it start" +# question only; functional behaviour (home, run a file, verify +# position) belongs in tests/ui-functional/ (Phase 2). + +import linuxcnc +import sys +import time + +CONNECT_TIMEOUT_S = 60.0 +SETTLE_S = 3.0 + + +def connect_and_wait_ready(timeout): + """Wait until linuxcnc.stat().poll() returns without error and + reports a non-negative echo_serial_number. The NML status buffer + can be 'invalid err=3' for the first ~30s while linuxcncsvr is + still initialising; recreate the stat object on every iteration so + a stale invalid buffer does not stick after linuxcncsvr is ready.""" + deadline = time.monotonic() + timeout + last_err = None + while time.monotonic() < deadline: + try: + stat = linuxcnc.stat() + stat.poll() + if stat.echo_serial_number >= 0: + return linuxcnc.command(), stat + except linuxcnc.error as e: + last_err = e + time.sleep(0.5) + sys.stderr.write( + f"UI_SMOKE_FAIL: task not ready within {timeout}s " + f"(last NML error: {last_err})\n") + return None, None + + +def main(): + cmd, stat = connect_and_wait_ready(CONNECT_TIMEOUT_S) + if cmd is None: + return 1 + + # Give the GUI process enough time to finish constructing itself + # (load .ui files, compile resources.py if needed, etc.) and + # settle. If the GUI was going to crash on startup it has crashed + # by now. + time.sleep(SETTLE_S) + + # Re-check task is still alive; a GUI crash may have torn linuxcnc + # down via Cleanup. + try: + stat.poll() + except linuxcnc.error as e: + sys.stderr.write(f"UI_SMOKE_FAIL: task disappeared after GUI startup: {e}\n") + return 1 + + print("UI_SMOKE_OK") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/ui-smoke/_lib/launch.sh b/tests/ui-smoke/_lib/launch.sh new file mode 100755 index 00000000000..fc3cdc127e2 --- /dev/null +++ b/tests/ui-smoke/_lib/launch.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Shared launcher for UI smoke tests. +# Usage: launch.sh +# +# Spawns linuxcnc -r under xvfb-run, then runs the common driver +# script against it via NML. Captures stdout/stderr to per-test files. +# +# Skip vs fail (BsAtHome / hdiethelm review, PR #3999): xvfb-run absence +# is handled by the per-test skip files (skip-if-missing.sh), which +# runtests gates on before invoking test.sh. CI is expected to have all +# required deps; if any python module the GUI needs is missing the test +# should fail loudly rather than silently skip. Per-GUI deps are +# declared in debian/control under !nocheck, so apt-get build-dep +# installs them on CI. + +set -u + +CONFIG_INI="$1" +TEST_DIR="${TEST_DIR:-$(cd "$(dirname "$0")" && pwd)}" +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +cd "$TEST_DIR" || exit 1 +rm -f ui-smoke.out ui-smoke.err linuxcnc.pid + +# Pre-launch cleanup: a previous ui-smoke test in the same job may +# have left a daemon listening on the NML TCP port or HAL shared +# memory still attached. Run the shared cleanup once before we start. +bash "$LIB_DIR/cleanup-runtime.sh" + +# Launch linuxcnc inside xvfb-run. The outer timeout is a safety net +# so a wedged GUI cannot hang CI. +LINUXCNC_TIMEOUT=240 +DRIVER_TIMEOUT=90 + +# Force software OpenGL (Mesa llvmpipe). CI runners have no GPU and +# Qt/GL widgets segfault under hardware GL with no display. The Qt- +# specific knobs cover qtdragon's QtQuick + RHI paths. +export LIBGL_ALWAYS_SOFTWARE=1 +export GALLIUM_DRIVER=llvmpipe +export QT_QUICK_BACKEND=software +export QSG_RHI_BACKEND=software +export QT_OPENGL=software + +# Export the per-invocation values so the inner bash -c receives them +# as proper env vars (avoids embedding paths into the inner script +# via quoting, which breaks on apostrophes / spaces). +export CONFIG_INI LIB_DIR DRIVER_TIMEOUT + +# Single quotes around the inner script are intentional: CONFIG_INI, +# LIB_DIR and DRIVER_TIMEOUT are expanded by the inner bash (which sees +# them via the exported env), not by the outer shell. +# shellcheck disable=SC2016 +xvfb-run -a --server-args="-screen 0 1024x768x24" \ + timeout "$LINUXCNC_TIMEOUT" \ + bash -c ' + # Run linuxcnc in its own process group so we can signal the + # whole group cleanly (linuxcnc forks task, motion, GUI, halrun). + # setsid makes the child a session leader, so its PID equals + # the PGID and we can group-signal via "kill -- -$PID". + setsid linuxcnc -r "$CONFIG_INI" >linuxcnc.out 2>linuxcnc.err & + LINUXCNC_PID=$! + echo "$LINUXCNC_PID" >linuxcnc.pid + + # The driver polls NML readiness itself (BsAtHome review: + # avoid real-clock waits where status polling will do). + timeout "$DRIVER_TIMEOUT" python3 "$LIB_DIR/drive.py" >ui-smoke.out 2>ui-smoke.err + DRIVE_RC=$? + + # Clean shutdown: GUI-specific quit first (lets linuxcnc end + # its own SIGTERM trap run Cleanup which unloads halrun and + # reaps shared memory). axis-remote works only for axis but is + # harmless otherwise. Then group-SIGTERM so the trap runs + # in-process. Wait up to 60s for Cleanup to finish before + # falling back to SIGKILL + cleanup-runtime.sh. + if command -v axis-remote >/dev/null 2>&1; then + axis-remote --quit 2>/dev/null || true + fi + + kill -TERM -- -"$LINUXCNC_PID" 2>/dev/null || true + for _ in $(seq 60); do + kill -0 "$LINUXCNC_PID" 2>/dev/null || break + sleep 1 + done + if kill -0 "$LINUXCNC_PID" 2>/dev/null; then + echo "WARN: linuxcnc did not exit on SIGTERM, escalating to KILL" >&2 + kill -KILL -- -"$LINUXCNC_PID" 2>/dev/null || true + sleep 2 + bash "$LIB_DIR/cleanup-runtime.sh" + fi + + exit "$DRIVE_RC" + ' +RC=$? + +# Surface logs so checkresult and CI artifact upload can see them. +echo "=== linuxcnc.out ===" +[ -f linuxcnc.out ] && cat linuxcnc.out +echo "=== linuxcnc.err ===" +[ -f linuxcnc.err ] && cat linuxcnc.err +echo "=== ui-smoke.out ===" +[ -f ui-smoke.out ] && cat ui-smoke.out +echo "=== ui-smoke.err ===" +[ -f ui-smoke.err ] && cat ui-smoke.err + +exit "$RC" diff --git a/tests/ui-smoke/_lib/run-gui.sh b/tests/ui-smoke/_lib/run-gui.sh new file mode 100755 index 00000000000..01840944bc0 --- /dev/null +++ b/tests/ui-smoke/_lib/run-gui.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Dispatcher invoked from each per-GUI test.sh. Resolves an INI path +# under configs/sim/ and execs launch.sh in the caller's test dir. +# Usage: run-gui.sh +# e.g. run-gui.sh axis/axis.ini +# run-gui.sh qtdragon/qtdragon_xyz/qtdragon_metric.ini + +set -u + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEST_DIR="${TEST_DIR:-$(cd "$(dirname "$0")" && pwd)}" +CONFIGS_DIR="$(cd "$LIB_DIR/../../../configs/sim" && pwd)" + +export TEST_DIR +exec "$LIB_DIR/launch.sh" "$CONFIGS_DIR/$1" diff --git a/tests/ui-smoke/_lib/skip-if-missing.sh b/tests/ui-smoke/_lib/skip-if-missing.sh new file mode 100755 index 00000000000..77a0155efcc --- /dev/null +++ b/tests/ui-smoke/_lib/skip-if-missing.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Shared "skip" predicate for ui-smoke tests. +# runtests semantics: a `skip` script that returns non-zero causes the +# test to be skipped. Per-test skip files invoke this. +# +# We only skip on xvfb-run absence (rare local dev env). Python / +# typelib deps are declared in debian/control under !nocheck so CI +# always has them; missing deps should fail the test loudly rather +# than silently skip (BsAtHome / hdiethelm review, PR #3999). +set -u + +if ! command -v xvfb-run >/dev/null 2>&1; then + echo "skip: xvfb-run not installed" >&2 + exit 1 +fi + +exit 0 diff --git a/tests/ui-smoke/axis/checkresult b/tests/ui-smoke/axis/checkresult new file mode 100755 index 00000000000..bbe14f5f99f --- /dev/null +++ b/tests/ui-smoke/axis/checkresult @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/checkresult.sh" "$@" diff --git a/tests/ui-smoke/axis/skip b/tests/ui-smoke/axis/skip new file mode 100755 index 00000000000..c1c260edf05 --- /dev/null +++ b/tests/ui-smoke/axis/skip @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/skip-if-missing.sh" diff --git a/tests/ui-smoke/axis/test.sh b/tests/ui-smoke/axis/test.sh new file mode 100755 index 00000000000..efa45dd9590 --- /dev/null +++ b/tests/ui-smoke/axis/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/run-gui.sh" axis/axis.ini diff --git a/tests/ui-smoke/gmoccapy/checkresult b/tests/ui-smoke/gmoccapy/checkresult new file mode 100755 index 00000000000..bbe14f5f99f --- /dev/null +++ b/tests/ui-smoke/gmoccapy/checkresult @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/checkresult.sh" "$@" diff --git a/tests/ui-smoke/gmoccapy/skip b/tests/ui-smoke/gmoccapy/skip new file mode 100755 index 00000000000..c1c260edf05 --- /dev/null +++ b/tests/ui-smoke/gmoccapy/skip @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/skip-if-missing.sh" diff --git a/tests/ui-smoke/gmoccapy/test.sh b/tests/ui-smoke/gmoccapy/test.sh new file mode 100755 index 00000000000..29adc2b9397 --- /dev/null +++ b/tests/ui-smoke/gmoccapy/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/run-gui.sh" gmoccapy/gmoccapy.ini diff --git a/tests/ui-smoke/qtdragon/checkresult b/tests/ui-smoke/qtdragon/checkresult new file mode 100755 index 00000000000..bbe14f5f99f --- /dev/null +++ b/tests/ui-smoke/qtdragon/checkresult @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/checkresult.sh" "$@" diff --git a/tests/ui-smoke/qtdragon/skip b/tests/ui-smoke/qtdragon/skip new file mode 100755 index 00000000000..c1c260edf05 --- /dev/null +++ b/tests/ui-smoke/qtdragon/skip @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/skip-if-missing.sh" diff --git a/tests/ui-smoke/qtdragon/test.sh b/tests/ui-smoke/qtdragon/test.sh new file mode 100755 index 00000000000..7df11989920 --- /dev/null +++ b/tests/ui-smoke/qtdragon/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/run-gui.sh" qtdragon/qtdragon_xyz/qtdragon_metric.ini diff --git a/tests/ui-smoke/touchy/checkresult b/tests/ui-smoke/touchy/checkresult new file mode 100755 index 00000000000..bbe14f5f99f --- /dev/null +++ b/tests/ui-smoke/touchy/checkresult @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/checkresult.sh" "$@" diff --git a/tests/ui-smoke/touchy/skip b/tests/ui-smoke/touchy/skip new file mode 100755 index 00000000000..c1c260edf05 --- /dev/null +++ b/tests/ui-smoke/touchy/skip @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/skip-if-missing.sh" diff --git a/tests/ui-smoke/touchy/test.sh b/tests/ui-smoke/touchy/test.sh new file mode 100755 index 00000000000..4b9c904d700 --- /dev/null +++ b/tests/ui-smoke/touchy/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec "$(dirname "$0")/../_lib/run-gui.sh" touchy/touchy.ini From df9dd5f5e7f29a08d3e3c81b603e01662f76a60c Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 11 May 2026 19:27:19 +0800 Subject: [PATCH 2/2] debian: add GUI runtime python deps under !nocheck Adds the Python, Qt, GTK and typelib runtime deps needed for the ui-smoke harness under tests/ui-smoke/ to actually exercise each GUI's import path on CI. All gated with so users building with DEB_BUILD_OPTIONS=nocheck aren't penalised with the extra packages. Includes pyqt5 (+ qsci/qtsvg/qtopengl/qtwebengine/qtpy/dev-tools), python3-dbus.mainloop.pyqt5, python3-cairo, python3-gi(+cairo), gir1.2-gtk-3.0, gir1.2-gtksource-4, python3-numpy, python3-configobj, xvfb and x11-xserver-utils. --- debian/control.top.in | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/debian/control.top.in b/debian/control.top.in index 1016d940fc9..8a11b559f73 100644 --- a/debian/control.top.in +++ b/debian/control.top.in @@ -48,6 +48,23 @@ Build-Depends: tk@TCLTK_VERSION@-dev, xvfb , x11-xserver-utils , + python3-opengl , + python3-pyqt5 , + python3-pyqt5.qsci , + python3-pyqt5.qtsvg , + python3-pyqt5.qtopengl , + python3-pyqt5.qtwebengine , + pyqt5-dev-tools , + python3-dbus , + python3-dbus.mainloop.pyqt5 , + python3-qtpy , + python3-cairo , + python3-gi , + python3-gi-cairo , + gir1.2-gtk-3.0 , + gir1.2-gtksource-4 , + python3-numpy , + python3-configobj , libfmt-dev, yapps2 Build-Depends-Indep: