From 83c8282bbf210bd255105d3fd3e828bb3b433085 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 7 Aug 2026 20:18:19 +0000 Subject: [PATCH 1/2] Add virtual CLOCK_REALTIME shim for per-node Kind CI testing Introduce an LD_PRELOAD shim and wrapper that redirect phc2sys CLOCK_REALTIME operations to per-node stand-in mock PHCs, plus scripts/create-vrt-clocks.sh to create those PHCs on Kind workers. Update ARCHITECTURE.md and README.md with the CI-only VRT setup. --- ARCHITECTURE.md | 5 + README.md | 7 + scripts/create-vrt-clocks.sh | 106 ++++++++++++++ shim/.gitignore | 1 + shim/Makefile | 25 ++++ shim/README.md | 23 ++++ shim/phc2sys-wrapper | 24 ++++ shim/ptp_vrt_shim.c | 258 +++++++++++++++++++++++++++++++++++ shim/test-shim-smoke.sh | 56 ++++++++ shim/test_vrt_cli | Bin 0 -> 70704 bytes shim/test_vrt_cli.c | 41 ++++++ 11 files changed, 546 insertions(+) create mode 100755 scripts/create-vrt-clocks.sh create mode 100644 shim/.gitignore create mode 100644 shim/Makefile create mode 100644 shim/README.md create mode 100755 shim/phc2sys-wrapper create mode 100644 shim/ptp_vrt_shim.c create mode 100755 shim/test-shim-smoke.sh create mode 100755 shim/test_vrt_cli create mode 100644 shim/test_vrt_cli.c diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b8fa74d..2208282 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -258,7 +258,12 @@ netdevsim-dkms/ │ ├── psample.c # Packet sampling (optional) │ └── macsec.c # MACsec offload (optional) ├── scripts/ +│ ├── create-vrt-clocks.sh # Per-Kind-node stand-in mock PHCs (virtual RT) │ └── test-utm-ubuntu.sh # Local macOS testing via UTM +├── shim/ # CI LD_PRELOAD: phc2sys CLOCK_REALTIME → mock PHC +│ ├── ptp_vrt_shim.c +│ ├── phc2sys-wrapper +│ └── README.md ├── Makefile # Top-level: ordered build, tarball, RPM ├── dkms.conf # DKMS registration (4 modules) ├── install-udev-rule.sh # POST_INSTALL hook diff --git a/README.md b/README.md index 1e2bbc0..f7f7f17 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,13 @@ When using netdevsim inside containers or Kubernetes pods: - **Kubernetes pods:** The udev rule creates real `/dev/ptpN` device nodes (not symlinks) so they propagate into containers. +- **Virtual CLOCK_REALTIME (CI):** Kind workers share one host + `CLOCK_REALTIME`. Use `scripts/create-vrt-clocks.sh` plus the + `shim/` LD_PRELOAD (packaged in the linuxptp-daemon image) so each + node's `phc2sys -a -r` disciplines a dedicated stand-in mock PHC. + See [shim/README.md](shim/README.md). This is not a second kernel + realtime clock. + ## Local libvirt/KVM VMs (Linux x86_64) ### Prerequisites diff --git a/scripts/create-vrt-clocks.sh b/scripts/create-vrt-clocks.sh new file mode 100755 index 0000000..602f27f --- /dev/null +++ b/scripts/create-vrt-clocks.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Create per-node stand-in mock PHCs that act as virtual CLOCK_REALTIME targets +# for Kind/netdevsim CI (used with the phc2sys LD_PRELOAD shim). +# +# Usage: +# ./create-vrt-clocks.sh [node1] [node2] [node3] +# Defaults: kind-netdevsim-worker kind-netdevsim-worker2 kind-netdevsim-worker3 +# +# For each node, creates a 1-port netdevsim device with a unique logical clock, +# writes /var/run/ptp/vrt/device inside that Kind node (seen by linuxptp pods as +# /var/run/vrt/device via the existing hostPath mount). + +set -euo pipefail + +if [[ $# -eq 0 ]]; then + NODES=(kind-netdevsim-worker kind-netdevsim-worker2 kind-netdevsim-worker3) +else + NODES=("$@") +fi + +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + PCI_PREFIX="0000:1f" +elif [[ "$ARCH" == "aarch64" ]]; then + PCI_PREFIX="0001:1f" +else + echo "Unsupported architecture: $ARCH" >&2 + exit 1 +fi + +NSIM_NEW=/sys/bus/netdevsim/new_device +NSIM_DEL=/sys/bus/netdevsim/del_device + +# Reserved IDs well above the topology devices (1..18) in configSwitch2.sh. +BASE_ID=90 +# PCI function slot 0x10+ also above topology (0x01..0x0f). +BASE_FUNC=0x10 + +runtime_exec() { + local name=$1 + shift + if podman inspect "$name" &>/dev/null; then + podman exec "$name" "$@" + elif docker inspect "$name" &>/dev/null; then + docker exec "$name" "$@" + else + echo "Error: Kind node container '$name' not found" >&2 + return 1 + fi +} + +phc_index_for_nsim() { + local nsim_id=$1 + local netdir iface idx + netdir="/sys/bus/netdevsim/devices/netdevsim${nsim_id}/net" + iface=$(find "$netdir" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | head -1) + if [[ -z "$iface" ]]; then + echo "Error: no netdev for netdevsim${nsim_id}" >&2 + return 1 + fi + idx=$(ethtool -T "$iface" 2>/dev/null | awk '/PTP Hardware Clock:/ {print $4}') + if [[ -z "$idx" || "$idx" == "none" ]]; then + echo "Error: no PHC on $iface (netdevsim${nsim_id})" >&2 + return 1 + fi + echo "$idx" +} + +i=0 +for node in "${NODES[@]}"; do + id=$((BASE_ID + i)) + clk=$id + func=$((BASE_FUNC + i)) + pci=$(printf '%s:%02x.0' "$PCI_PREFIX" "$func") + + # Replace any previous VRT device with this id. + echo "$id" >"$NSIM_DEL" 2>/dev/null || true + if ! echo "$id $pci $clk 1" >"$NSIM_NEW"; then + echo "Error: failed to create netdevsim id=$id pci=$pci" >&2 + exit 1 + fi + udevadm settle 2>/dev/null || sleep 0.5 + chmod 666 /dev/nsim_ptp* 2>/dev/null || true + + phc=$(phc_index_for_nsim "$id") + dev="/dev/ptp${phc}" + if [[ ! -e "$dev" ]]; then + echo "Error: $dev does not exist after creating netdevsim${id}" >&2 + exit 1 + fi + + # Keep the unused netdev down; only the PHC chardev is needed. + netdir="/sys/bus/netdevsim/devices/netdevsim${id}/net" + iface=$(find "$netdir" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | head -1) + ip link set dev "$iface" down 2>/dev/null || true + + # Node-local path under the hostPath root used by linuxptp-daemon (/var/run/ptp). + runtime_exec "$node" mkdir -p /var/run/ptp/vrt + runtime_exec "$node" bash -c "echo -n '$dev' > /var/run/ptp/vrt/device" + runtime_exec "$node" bash -c "ln -sfn '$dev' /var/run/ptp/vrt/ptpRT" + + echo "VRT: node=$node nsim=$id pci=$pci clk=$clk phc=$dev" + i=$((i + 1)) +done + +echo "Created ${i} virtual RT stand-in PHC(s)" diff --git a/shim/.gitignore b/shim/.gitignore new file mode 100644 index 0000000..140f8cf --- /dev/null +++ b/shim/.gitignore @@ -0,0 +1 @@ +*.so diff --git a/shim/Makefile b/shim/Makefile new file mode 100644 index 0000000..fdee2d7 --- /dev/null +++ b/shim/Makefile @@ -0,0 +1,25 @@ +CC ?= gcc +CFLAGS ?= -O2 -fPIC -Wall -Wextra +LDFLAGS ?= -shared -ldl -lpthread + +LIB = libptp_vrt_shim.so +TESTCLI = test_vrt_cli + +.PHONY: all clean install + +all: $(LIB) $(TESTCLI) + +$(LIB): ptp_vrt_shim.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +$(TESTCLI): test_vrt_cli.c + $(CC) -O2 -Wall -Wextra -o $@ $< + +clean: + rm -f $(LIB) $(TESTCLI) + +install: $(LIB) + install -d $(DESTDIR)/usr/local/lib + install -m 755 $(LIB) $(DESTDIR)/usr/local/lib/ + install -d $(DESTDIR)/usr/local/sbin + install -m 755 phc2sys-wrapper $(DESTDIR)/usr/local/sbin/phc2sys-wrapper diff --git a/shim/README.md b/shim/README.md new file mode 100644 index 0000000..39bc838 --- /dev/null +++ b/shim/README.md @@ -0,0 +1,23 @@ +# phc2sys virtual CLOCK_REALTIME shim (CI only) + +Kind workers share one host `CLOCK_REALTIME`. This LD_PRELOAD library makes +`phc2sys -a -r` discipline a **per-node stand-in mock PHC** instead, while +keeping log lines as `CLOCK_REALTIME` so cloud-event-proxy metrics/events work. + +## Components + +| File | Role | +|------|------| +| `ptp_vrt_shim.c` | Redirects `clock_gettime/settime/adjtime(CLOCK_REALTIME)` and `adjtimex` to the stand-in PHC; fakes `PTP_SYS_OFFSET*` against that PHC | +| `phc2sys-wrapper` | Installed as `/usr/sbin/phc2sys`; enables preload when `/var/run/vrt/device` exists | +| `../scripts/create-vrt-clocks.sh` | Creates one netdevsim mock PHC per Kind worker and writes the device path into each node | + +## Activation + +1. Build/load netdevsim-dkms modules. +2. Bring up Kind + topology (`configSwitch2.sh`). +3. Run `create-vrt-clocks.sh` (writes `/var/run/ptp/vrt/device` per node → pod path `/var/run/vrt/device`). +4. linuxptp image must contain the shim + wrapper (see ptp-operator `Dockerfile.lptpd`). +5. Tests: set `DisableAllSlaveRTUpdate: false` (netdevsim CI default when `PTP_VRT_ENABLE=true`). + +This is **not** a second kernel realtime clock. Host `date` is unchanged. diff --git a/shim/phc2sys-wrapper b/shim/phc2sys-wrapper new file mode 100755 index 0000000..1be40f7 --- /dev/null +++ b/shim/phc2sys-wrapper @@ -0,0 +1,24 @@ +#!/bin/bash +# Wrapper installed as /usr/sbin/phc2sys in the linuxptp-daemon image. +# When a per-node VRT device file is present, preload the stand-in RT shim. +set -euo pipefail + +REAL_PHC2SYS="${PTP_VRT_REAL_PHC2SYS:-/usr/sbin/phc2sys.real}" +SHIM="${PTP_VRT_SHIM:-/usr/local/lib/libptp_vrt_shim.so}" +DEVICE_FILE="${PTP_VRT_DEVICE_FILE:-/var/run/vrt/device}" + +if [[ -z "${PTP_VRT_PHC:-}" && -f "$DEVICE_FILE" ]]; then + PTP_VRT_PHC="$(tr -d '[:space:]' <"$DEVICE_FILE")" + export PTP_VRT_PHC +fi + +if [[ -n "${PTP_VRT_PHC:-}" && -e "${PTP_VRT_PHC}" && -f "$SHIM" ]]; then + export PTP_VRT_PHC + if [[ -n "${LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="${SHIM}:${LD_PRELOAD}" + else + export LD_PRELOAD="${SHIM}" + fi +fi + +exec "$REAL_PHC2SYS" "$@" diff --git a/shim/ptp_vrt_shim.c b/shim/ptp_vrt_shim.c new file mode 100644 index 0000000..a1d9fe3 --- /dev/null +++ b/shim/ptp_vrt_shim.c @@ -0,0 +1,258 @@ +/* + * CI-only LD_PRELOAD shim: redirect CLOCK_REALTIME ops from phc2sys onto a + * per-node stand-in mock PHC, and rewrite PTP_SYS_OFFSET* so measurement uses + * that virtual RT instead of the shared host CLOCK_REALTIME. + * + * Activation: set PTP_VRT_PHC=/dev/ptpN (or PTP_VRT_DEVICE_FILE pointing at a + * file that contains that path). Inactive when unset/missing. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef CLOCKFD +#define CLOCKFD 3 +#endif +#ifndef FD_TO_CLOCKID +#define FD_TO_CLOCKID(fd) ((clockid_t)((((unsigned int)~(fd)) << 3) | CLOCKFD)) +#endif + +typedef int (*clock_gettime_fn)(clockid_t, struct timespec *); +typedef int (*clock_settime_fn)(clockid_t, const struct timespec *); +typedef int (*clock_adjtime_fn)(clockid_t, struct timex *); +typedef int (*adjtimex_fn)(struct timex *); +typedef int (*ioctl_fn)(int, unsigned long, ...); + +static clock_gettime_fn real_clock_gettime; +static clock_settime_fn real_clock_settime; +static clock_adjtime_fn real_clock_adjtime; +static adjtimex_fn real_adjtimex; +static ioctl_fn real_ioctl; + +static pthread_once_t once = PTHREAD_ONCE_INIT; +static int vrt_fd = -1; +static clockid_t vrt_clk = CLOCK_REALTIME; +static bool vrt_active; + +static void resolve_reals(void) +{ + real_clock_gettime = (clock_gettime_fn)dlsym(RTLD_NEXT, "clock_gettime"); + real_clock_settime = (clock_settime_fn)dlsym(RTLD_NEXT, "clock_settime"); + real_clock_adjtime = (clock_adjtime_fn)dlsym(RTLD_NEXT, "clock_adjtime"); + real_adjtimex = (adjtimex_fn)dlsym(RTLD_NEXT, "adjtimex"); + real_ioctl = (ioctl_fn)dlsym(RTLD_NEXT, "ioctl"); +} + +static void init_vrt(void) +{ + const char *path = NULL; + char buf[256]; + const char *file; + + resolve_reals(); + + path = getenv("PTP_VRT_PHC"); + if (!path || !path[0]) { + file = getenv("PTP_VRT_DEVICE_FILE"); + if (!file || !file[0]) + file = "/var/run/vrt/device"; + FILE *f = fopen(file, "r"); + if (!f) + return; + if (!fgets(buf, sizeof(buf), f)) { + fclose(f); + return; + } + fclose(f); + buf[strcspn(buf, "\r\n")] = '\0'; + if (!buf[0]) + return; + path = buf; + } + + vrt_fd = open(path, O_RDWR); + if (vrt_fd < 0) + return; + + vrt_clk = FD_TO_CLOCKID(vrt_fd); + vrt_active = true; +} + +static void ensure_init(void) +{ + pthread_once(&once, init_vrt); +} + +static bool is_rt(clockid_t clk) +{ + return clk == CLOCK_REALTIME; +} + +static void ts_to_ptp(const struct timespec *ts, struct ptp_clock_time *pct) +{ + pct->sec = ts->tv_sec; + pct->nsec = (unsigned int)ts->tv_nsec; + pct->reserved = 0; +} + +static int read_vrt(struct timespec *ts) +{ + return real_clock_gettime(vrt_clk, ts); +} + +static int read_phc_fd(int fd, struct timespec *ts) +{ + clockid_t clk = FD_TO_CLOCKID(fd); + return real_clock_gettime(clk, ts); +} + +static int fake_sys_offset(int fd, struct ptp_sys_offset *sysoff) +{ + unsigned int i, n; + struct timespec ts; + + if (!sysoff) + return -1; + n = sysoff->n_samples; + if (n > PTP_MAX_SAMPLES) { + errno = EINVAL; + return -1; + } + + /* Interleaved sys, phc, ... ending with an extra sys sample. */ + for (i = 0; i < n; i++) { + if (read_vrt(&ts) < 0) + return -1; + ts_to_ptp(&ts, &sysoff->ts[2 * i]); + if (read_phc_fd(fd, &ts) < 0) + return -1; + ts_to_ptp(&ts, &sysoff->ts[2 * i + 1]); + } + if (read_vrt(&ts) < 0) + return -1; + ts_to_ptp(&ts, &sysoff->ts[2 * n]); + return 0; +} + +static int fake_sys_offset_extended(int fd, struct ptp_sys_offset_extended *ex) +{ + unsigned int i, n; + struct timespec pre, mid, post; + + if (!ex) + return -1; + n = ex->n_samples; + if (n > PTP_MAX_SAMPLES) { + errno = EINVAL; + return -1; + } + for (i = 0; i < n; i++) { + if (read_vrt(&pre) < 0) + return -1; + if (read_phc_fd(fd, &mid) < 0) + return -1; + if (read_vrt(&post) < 0) + return -1; + ts_to_ptp(&pre, &ex->ts[i][0]); + ts_to_ptp(&mid, &ex->ts[i][1]); + ts_to_ptp(&post, &ex->ts[i][2]); + } + return 0; +} + +static int fake_sys_offset_precise(int fd, struct ptp_sys_offset_precise *pr) +{ + struct timespec dev, sys; + + if (!pr) + return -1; + if (read_phc_fd(fd, &dev) < 0) + return -1; + if (read_vrt(&sys) < 0) + return -1; + ts_to_ptp(&dev, &pr->device); + ts_to_ptp(&sys, &pr->sys_realtime); + /* sys_monoraw unused by phc2sys RT path; leave zeroed. */ + memset(&pr->sys_monoraw, 0, sizeof(pr->sys_monoraw)); + return 0; +} + +int clock_gettime(clockid_t clk, struct timespec *tp) +{ + ensure_init(); + if (vrt_active && is_rt(clk)) + return real_clock_gettime(vrt_clk, tp); + return real_clock_gettime(clk, tp); +} + +int clock_settime(clockid_t clk, const struct timespec *tp) +{ + ensure_init(); + if (vrt_active && is_rt(clk)) + return real_clock_settime(vrt_clk, tp); + return real_clock_settime(clk, tp); +} + +int clock_adjtime(clockid_t clk, struct timex *buf) +{ + ensure_init(); + if (vrt_active && is_rt(clk)) + return real_clock_adjtime(vrt_clk, buf); + return real_clock_adjtime(clk, buf); +} + +int adjtimex(struct timex *buf) +{ + ensure_init(); + if (vrt_active) + return real_clock_adjtime(vrt_clk, buf); + return real_adjtimex(buf); +} + +int ioctl(int fd, unsigned long request, ...) +{ + va_list ap; + void *arg; + int rc; + + ensure_init(); + + va_start(ap, request); + arg = va_arg(ap, void *); + va_end(ap); + + if (vrt_active && fd >= 0 && fd != vrt_fd) { + switch (request) { + case PTP_SYS_OFFSET: + case PTP_SYS_OFFSET2: + rc = fake_sys_offset(fd, arg); + return rc; + case PTP_SYS_OFFSET_EXTENDED: + case PTP_SYS_OFFSET_EXTENDED2: + rc = fake_sys_offset_extended(fd, arg); + return rc; + case PTP_SYS_OFFSET_PRECISE: + case PTP_SYS_OFFSET_PRECISE2: + rc = fake_sys_offset_precise(fd, arg); + return rc; + default: + break; + } + } + + return real_ioctl(fd, request, arg); +} diff --git a/shim/test-shim-smoke.sh b/shim/test-shim-smoke.sh new file mode 100755 index 0000000..3610f27 --- /dev/null +++ b/shim/test-shim-smoke.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Smoke-test the VRT shim against a mock PHC (requires loaded netdevsim-dkms). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +make -C "$ROOT" all + +NSIM_NEW=/sys/bus/netdevsim/new_device +NSIM_DEL=/sys/bus/netdevsim/del_device +ID=99 +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + PCI="0000:1f:1f.0" +elif [[ "$ARCH" == "aarch64" ]]; then + PCI="0001:1f:1f.0" +else + echo "skip: unsupported arch $ARCH" + exit 0 +fi + +if [[ ! -w "$NSIM_NEW" ]]; then + echo "skip: netdevsim not available (need root + modules)" + exit 0 +fi + +cleanup() { echo "$ID" >"$NSIM_DEL" 2>/dev/null || true; } +trap cleanup EXIT + +echo "$ID" >"$NSIM_DEL" 2>/dev/null || true +echo "$ID $PCI $ID 1" >"$NSIM_NEW" +udevadm settle 2>/dev/null || sleep 0.5 +chmod 666 /dev/nsim_ptp* 2>/dev/null || true + +IFACE=$(find /sys/bus/netdevsim/devices/netdevsim${ID}/net -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | head -1) +PHC=$(ethtool -T "$IFACE" | awk '/PTP Hardware Clock:/ {print $4}') +DEV="/dev/ptp${PHC}" +[[ -e "$DEV" ]] + +export PTP_VRT_PHC="$DEV" +export LD_PRELOAD="$ROOT/libptp_vrt_shim.so" + +BEFORE=$("$ROOT/test_vrt_cli" gettime) +"$ROOT/test_vrt_cli" step >/dev/null +AFTER=$("$ROOT/test_vrt_cli" gettime) + +# Host CLOCK_REALTIME should be essentially unchanged by the step (shim only). +HOST_NOW=$(date +%s) +BEFORE_SEC=${BEFORE%%.*} +# Stand-in PHC was stepped by 1us; values should be near wall clock still. +echo "vrt_before=$BEFORE vrt_after=$AFTER host_sec=$HOST_NOW" +python3 - <)g%TbMN<_bMC#Lci)|PvwQ7^b>UD5xESy>P-}3iuf$YG%OztzF<1>#AO@Gi ze3*#+4S2C%(4JFaY$u~H6$3AdTOFoWt)1`7ID%hzbg~naR?oCt;;U@ITaEvp<{BII z`(vYiMz+V)Jl;^23!bUn*Vix+$u+PWPHM z_6A=Dwj6g8#?d@4wlpfoEo!{+xD2cLv*r7+G?Pv&&7_uO(z*VDC6?tR`&PC_i}~m> zf8Db&Ki%VwO*g|24xV`JCs(X&I=T7nH=A!gKCfrvoww7xX%3W+e5jEBB(LV{)W_o~ z`J#*g)8Gsz(jp9)gDt*cen%IYS(*=5Y214HA5o96lV;OcU!=9UO8cQ+o&@Sm?4;Sx zjpI)(jy|Cy&BuS{0Y5$oLkz48xXe*K(2rs?`Q^@gVN*+~PO)O0l!Z`?DQu8v4gKd0&XzVB0d z80SOh3zveXD->SWfTrv79Mp9EXn8=>6&f$kkfy7TQcvHf>H50%Yr6Uf^ZXBJ`V3F; z{~glwnVNn?(|J!&_ri$r+UTz54vrd7Is~w1bl39-h7I%h2x=n$BTc{PC^z2wrBWok z@HqMM<@G&|KDMIk=A|@+ihC=Wo#$3_AA)F zS8E@~_G4Q6O>7_4+9$Am+HY$pE~;AjX2YLOL`!yYD7K_rL$X`4xTkaPsAZs zKD8UlAsBpRVFTQmodn(P#2B_=sQF~Hbkdmp#9u3wz56FZD{0hj-oLc;I%#2OJ&AeY zgN||-`;y^UxoulZ2{30I2DiM3J~6bX(5D&4etUPUya@Y}5Pm<5<5?4+dvsUV!LwU$ zI*4;D(Y?aQe)H*HLa8Bg-xlNRgRh)=97^we0iKCL_{q0#gmMffJQ)MPD}`1(o#`rFQCTzO1D?wdKcjJt^rS+ zGGgV^IQDx6lstatI?4w2HQ;p{=ArzaKGK2fg~J8RNo+VD9+yK9gA#tF9co5G9TkP6c2v&LXJkwDg#5iwW4Y6`7&Y$)NG1}G!*Pfjg!+m%3 zz;l>~=3_24UL*dLo1a&HRsjp0_f+^3wEFYZ>+=3xl}SafYPmL(Qil(IP8F!b_vg=4 z0ILT-!0^+`E<~zA`ioYteO=_LV&qme)ZOHC8>I`)H?Ciemd?kQ&Q~fgq22IlrP77= zhiFrzqowZ-(<_zA!)U*X_AuH;yr_hBZh?k@1)=%V7kb(})IJ)mROovJ#3wBvziaSn z!8|RbVIeJs7mdZ=e-~rB8?OP>y3j&=Nqk1+M-4wV+8zmSI`P)4Gf+W!HePq*wIBWT z&zY-H+k@A+Pb!r{2rKa!`@`)jeorVh^{o@H{uy)VXEl0B5duO$2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{As_^VfDjM@LO=)z0U;m+gn$qb0zyCt2mv7=1cZPP5CTF#2nYcoAOwVf5D)@F zKnMr{A@Cm&7@Fq0$CM4J=)KBv`%%@NuI!mfe&rI?-lyU}quQ}iKXSfmPgeG`%C1-a zmk0X)@o6Pb(!Q|YF+uhBHTac@s(nQDPgU(%YNBcyFi+{1Da*!Cm9B~(pwhr(6+c_` ztL4F5)jp{PW_?6WjMp`y{H7?&<1bKc_WPvbuMdawq)%0QzsgrpZJyVNYVTD2Wz|+V zRQCUa;RXDUZI{{y7rg=o90-^UKCzDW_Uj^7-<;^rx&4u4E21l+ElXDRd(GwFSl$|K zX^k%Xyy^&~sA(Dyg4KK@v|a

aMFBFi=--xUiqKG+@94;C)=JPXylY)%qmheNe4W z2Hro_`V`=OTCGn7-p|$gG~nmDTF0lJ@4MDv06zV7?uP+0fX@TX9oIV(4)Fu3br{eH zt$eUl=}l0mtIq;HUo>~je<|>}QmxO1di%_P%Ye^;YX3Rdti6#2o(7F|b$;5%ve0qT z03TMy6u;)$RNeT&_1C&VM+NKZ5>Y z9G7)I7k&TQed6o2_kGE~9d{a>;{z4_sBoU22KGrS_SKs|ulGp6pVt-KpUjQ6u3p;5 z$vb>NMYa_x)YZ|HDe~ zx&Au^)HzQ=2rz`t&@pDFVEO{$lsg##__*r9Q_g0>3#)y ze&DTZ?0lwkipsum^89?9JkKaS`1$gJmuKvAmi7mg7f~k;LI2~ZM?8<}eIOgD?&`J7$Ew5hz=o5?4vjG1!tPSLdbF@HW=$k?u(ini4EFniLu zv}rkxwZpV?uCoJr94l*^ss3zs2Zm@Z6FuE3SJPa#CBAX3xpvbU(*$$PEt}#SJKHg; zW7Ey%+V!e;{hBRcc5K+(7T;iQUbk-R+HSKu-nL;aMwpp&B54*~3zwO-(z#g2hR(Kj zb9r<{D->+U$vYR6WvjsK?A~aOExvnWJ1xE2N@Q$Il_(ZfCQ6(fuxM#kCcV?vb0Dqc zcC*)Z-E_99qv&Gh>f!8^cVt~Ff!1|=+s8HJ zXFCOm=JKu`?alQ^6aDE-YDqe!q-ar)*-kN?&s8-{ z>~rjlMFFZ-$hZ*o<`i}90j$%m$G*IGhog3%+UI>KjOCUe;cs|9g6o*r%BGVzY94d> zIgk@X@o~%IQ&fE@{1+{HpQj@Tj}xz~Ru#7VzOTAHU#^qA5-g zmR4d#kLT~pY$NQbY%qQe>L24fG>^YeRoL?H5$bh2__YA&dmnwZKFF5&@%-73?RJc(-yFEj?~QEf z`zsZiKl$+dNu#n8FJj8^{C;`Ck`oplpW}J`_hBE!w~-STeh+2Kzr#>G{dNP`O;CDjVYvwaWy^!TwSiPYNt#&KRJ{`&wg(s+FS{@3Vb9eZBb-_Ob|fDf@sTFaQ^ z)i|Es*Y(2jy5+s6`^D7w{Ju@U^TzSw`SWv0j| +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + struct timespec ts, ts2; + struct timex tx; + const char *cmd = argc > 1 ? argv[1] : "gettime"; + + if (strcmp(cmd, "gettime") == 0) { + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) { + perror("clock_gettime"); + return 1; + } + printf("%ld.%09ld\n", (long)ts.tv_sec, ts.tv_nsec); + return 0; + } + if (strcmp(cmd, "step") == 0) { + memset(&tx, 0, sizeof(tx)); + tx.modes = ADJ_SETOFFSET | ADJ_NANO; + tx.time.tv_sec = 0; + tx.time.tv_usec = 1000; /* 1000 ns when ADJ_NANO */ + if (clock_adjtime(CLOCK_REALTIME, &tx) < 0) { + perror("clock_adjtime"); + return 1; + } + if (clock_gettime(CLOCK_REALTIME, &ts2) != 0) { + perror("clock_gettime"); + return 1; + } + printf("after_step %ld.%09ld\n", (long)ts2.tv_sec, ts2.tv_nsec); + return 0; + } + fprintf(stderr, "usage: %s [gettime|step]\n", argv[0]); + return 2; +} From 53017ec4789586266271411c3c52b23c113e404d Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 7 Aug 2026 20:22:25 +0000 Subject: [PATCH 2/2] Add unit test scripts and Makefile targets for DPLL, mock PHC, and GNSS/UBX Introduce scripts/test-dpll.sh, scripts/test-phc.sh, and scripts/test-gnss-ubx.sh with embedded Python helpers to exercise the netdevsim DPLL, mock PHC, and GNSS/UBX emulation via sysfs, generic netlink, and device ioctls. Add Makefile targets (test-dpll, test-phc, test-gnss-ubx, test-all, dkms-install, dkms-uninstall) and update README.md and ARCHITECTURE.md with DKMS install shortcuts and unit-test instructions. Also make the clean target remove build artifacts across ptp/dpll/netdevsim without invoking the kernel build system. --- ARCHITECTURE.md | 3 + Makefile | 47 +- README.md | 42 +- scripts/test-dpll.sh | 788 ++++++++++++++++++++++++++ scripts/test-gnss-ubx.sh | 560 +++++++++++++++++++ scripts/test-phc.sh | 1122 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 2549 insertions(+), 13 deletions(-) create mode 100755 scripts/test-dpll.sh create mode 100755 scripts/test-gnss-ubx.sh create mode 100755 scripts/test-phc.sh diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2208282..27b4c23 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -259,6 +259,9 @@ netdevsim-dkms/ │ └── macsec.c # MACsec offload (optional) ├── scripts/ │ ├── create-vrt-clocks.sh # Per-Kind-node stand-in mock PHCs (virtual RT) +│ ├── test-dpll.sh # DPLL unit tests (make test-dpll) +│ ├── test-phc.sh # Mock PHC unit tests (make test-phc) +│ ├── test-gnss-ubx.sh # GNSS/UBX unit tests (make test-gnss-ubx) │ └── test-utm-ubuntu.sh # Local macOS testing via UTM ├── shim/ # CI LD_PRELOAD: phc2sys CLOCK_REALTIME → mock PHC │ ├── ptp_vrt_shim.c diff --git a/Makefile b/Makefile index 977636b..9a76fa4 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,8 @@ VERSION := 6.9.5 CONTAINER_IMAGE ?= fedora:39 CONTAINER_CMD ?= podman -.PHONY: all clean modules_install tarball rpm rpm-container test-utm +.PHONY: all clean modules_install tarball rpm rpm-container test-utm \ + test-dpll test-phc test-gnss-ubx test-all dkms-install dkms-uninstall all: $(MAKE) -C $(KDIR) M=$(PWD)/ptp DKMS_INCLUDE=$(DKMS_INCLUDE) modules @@ -34,9 +35,12 @@ all: modules clean: - $(MAKE) -C $(KDIR) M=$(PWD)/ptp clean - $(MAKE) -C $(KDIR) M=$(PWD)/dpll clean - $(MAKE) -C $(KDIR) M=$(PWD)/netdevsim clean + find $(PWD)/ptp $(PWD)/dpll $(PWD)/netdevsim \ + \( -name '*.o' -o -name '*.ko' -o -name '*.ko.*' \ + -o -name '*.mod' -o -name '*.mod.c' -o -name '*.mod.o' \ + -o -name '.*.cmd' -o -name 'modules.order' \ + -o -name 'Module.symvers' -o -name '.tmp_versions' \) \ + -exec rm -rf {} + 2>/dev/null || true rm -rf $(PWD)/rpmbuild $(PWD)/$(NAME)-$(VERSION).tar.gz tarball: @@ -68,6 +72,41 @@ rpm-container: tarball bash -c "dnf install -y rpm-build && rpmbuild -bb --define '_topdir /src/rpmbuild' $(NAME)-dkms.spec" @echo "RPMs written to rpmbuild/RPMS/" +DKMS_SRC := /usr/src/$(NAME)-$(VERSION) + +dkms-install: ## Install DKMS modules (requires root) + sudo mkdir -p $(DKMS_SRC) + sudo cp -a Makefile dkms.conf install-udev-rule.sh 99-nsim-ptp.rules \ + include ptp dpll netdevsim $(DKMS_SRC)/ + sudo dkms add $(NAME)/$(VERSION) 2>/dev/null || true + sudo dkms build --force $(NAME)/$(VERSION) + sudo dkms install --force $(NAME)/$(VERSION) + sudo cp $(DKMS_SRC)/99-nsim-ptp.rules /etc/udev/rules.d/ + sudo udevadm control --reload-rules + @echo "Installed $(NAME)/$(VERSION) via DKMS" + +dkms-uninstall: ## Uninstall DKMS modules (requires root) + -sudo rmmod netdevsim nsim_dpll nsim_ptp_mock nsim_ptp 2>/dev/null + -sudo dkms remove $(NAME)/$(VERSION) --all 2>/dev/null + -sudo rm -rf $(DKMS_SRC) + -sudo rm -f /etc/udev/rules.d/99-nsim-ptp.rules + -sudo udevadm control --reload-rules + @echo "Uninstalled $(NAME)/$(VERSION)" + +test-dpll: ## Run DPLL unit tests (requires root, modules loaded) + sudo ./scripts/test-dpll.sh + +test-phc: ## Run mock PHC unit tests (requires root, modules loaded) + sudo ./scripts/test-phc.sh + +test-gnss-ubx: ## Run GNSS/UBX protocol unit tests (requires root, modules loaded) + sudo ./scripts/test-gnss-ubx.sh + +test-all: ## Run all unit tests (dpll + phc + gnss-ubx) + sudo ./scripts/test-dpll.sh + sudo ./scripts/test-phc.sh + sudo ./scripts/test-gnss-ubx.sh + test-utm: rpm-container ./scripts/test-utm.sh \ --rpm $$(ls rpmbuild/RPMS/noarch/$(NAME)-dkms-*.noarch.rpm | head -1) \ diff --git a/README.md b/README.md index f7f7f17..6457cc1 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,14 @@ substantially different kernel version will likely require source modifications. ### Using DKMS (recommended) ```bash -# Copy source tree to the DKMS source directory -sudo cp -r . /usr/src/netdevsim-6.9.5 - -# Register and build -sudo dkms add netdevsim/6.9.5 -sudo dkms build netdevsim/6.9.5 -sudo dkms install netdevsim/6.9.5 +# One-shot install (copy sources, dkms add/build/install, udev rules) +make dkms-install + +# Or manually: +# sudo cp -r . /usr/src/netdevsim-6.9.5 +# sudo dkms add netdevsim/6.9.5 +# sudo dkms build netdevsim/6.9.5 +# sudo dkms install netdevsim/6.9.5 ``` ### Manual build (without DKMS) @@ -54,8 +55,11 @@ sudo make modules_install ## Uninstallation ```bash -sudo dkms remove netdevsim/6.9.5 --all -sudo rm -rf /usr/src/netdevsim-6.9.5 +make dkms-uninstall + +# Or manually: +# sudo dkms remove netdevsim/6.9.5 --all +# sudo rm -rf /usr/src/netdevsim-6.9.5 ``` ## Loading the modules @@ -132,6 +136,26 @@ When using netdevsim inside containers or Kubernetes pods: See [shim/README.md](shim/README.md). This is not a second kernel realtime clock. +## Unit Tests + +Requires root and DKMS modules installed (`make dkms-install`). Scripts load +modules unless `--no-load` is passed. + +```bash +make test-dpll # DPLL sysfs / netlink / lock_status +make test-phc # mock PHC get/set/adj/EXTTS / sharing +make test-gnss-ubx # GNSS UBX + NMEA signal block/restore +make test-all # all of the above +``` + +Or run the scripts directly: + +```bash +sudo ./scripts/test-dpll.sh [--no-load] [--verbose] +sudo ./scripts/test-phc.sh [--no-load] [--verbose] +sudo ./scripts/test-gnss-ubx.sh [--no-load] [--verbose] +``` + ## Local libvirt/KVM VMs (Linux x86_64) ### Prerequisites diff --git a/scripts/test-dpll.sh b/scripts/test-dpll.sh new file mode 100755 index 0000000..5dd55dd --- /dev/null +++ b/scripts/test-dpll.sh @@ -0,0 +1,788 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Unit tests for the netdevsim DPLL emulation (netdevsim/dpll.c). +# +# Exercises: +# - Module loading (nsim_dpll + netdevsim) +# - Device creation with wpc=1 (DPLL activation) +# - Sysfs lock_status (read / write / invalid input) +# - Generic netlink DPLL interface (device get, pin get) +# - Pin topology (GNSS, EXT, SyncE) +# - Lock status transitions and notifications +# - GNSS device presence and NMEA echo +# - Device teardown and re-creation +# +# Requirements: +# - Root privileges +# - DKMS modules installed and loaded (nsim_ptp, nsim_ptp_mock, +# nsim_dpll, netdevsim) +# - python3 (for generic netlink JSON parsing) +# +# Usage: +# sudo ./scripts/test-dpll.sh [--no-load] [--verbose] +# +# --no-load Skip module load/unload (assume already loaded) +# --verbose Print all commands as they execute +# +set -eo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +NO_LOAD=false +VERBOSE=false +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 +FAILURES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-load) NO_LOAD=true; shift ;; + --verbose) VERBOSE=true; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +[[ "$VERBOSE" == true ]] && set -x + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +NC='\033[0m' + +log() { echo -e "${BOLD}==> $*${NC}"; } + +pass() { + ((TOTAL++)) || true + ((PASS++)) || true + echo -e " ${GREEN}PASS${NC}: $1" +} + +fail() { + ((TOTAL++)) || true + ((FAIL++)) || true + FAILURES="${FAILURES}\n - $1" + echo -e " ${RED}FAIL${NC}: $1" +} + +skip() { + ((TOTAL++)) || true + ((SKIP++)) || true + echo -e " ${YELLOW}SKIP${NC}: $1" +} + +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + pass "$desc" + else + fail "$desc (expected='$expected', got='$actual')" + fi +} + +assert_contains() { + local desc="$1" haystack="$2" needle="$3" + if echo "$haystack" | grep -qF "$needle"; then + pass "$desc" + else + fail "$desc (output does not contain '$needle')" + fi +} + +assert_match() { + local desc="$1" haystack="$2" pattern="$3" + if echo "$haystack" | grep -qE "$pattern"; then + pass "$desc" + else + fail "$desc (output does not match pattern '$pattern')" + fi +} + +assert_file_exists() { + local desc="$1" path="$2" + if [[ -e "$path" ]]; then + pass "$desc" + else + fail "$desc ($path does not exist)" + fi +} + +assert_file_not_exists() { + local desc="$1" path="$2" + if [[ ! -e "$path" ]]; then + pass "$desc" + else + fail "$desc ($path unexpectedly exists)" + fi +} + +# Discover PCI bus domain from netdevsim module parameter +get_pci_domain() { + local bus_nr bus fake_root domain + bus_nr=$(cat /sys/module/netdevsim/parameters/pci_bus_nr 2>/dev/null || echo 31) + bus=$(printf "%02x" "$bus_nr") + fake_root=$(ls /sys/bus/pci/devices/ 2>/dev/null | grep ":${bus}:00\.0" | head -1 || true) + if [[ -n "$fake_root" ]]; then + domain=$(echo "$fake_root" | cut -d: -f1) + else + domain="0000" + fi + echo "${domain}:${bus}" +} + +# Create a netdevsim device with DPLL (wpc=1) +# Args: device_id [clock_id] [port_count] [num_queues] [wpc] +create_device() { + local id="${1:-1}" + local pci_prefix + pci_prefix=$(get_pci_domain) + local pci_addr="${pci_prefix}:$(printf '%02x' "$((id + 1))").0" + local clock_id="${2:-1}" + local ports="${3:-2}" + local queues="${4:-1}" + local wpc="${5:-1}" + echo "${id} ${pci_addr} ${clock_id} ${ports} ${queues} ${wpc}" \ + > /sys/bus/netdevsim/new_device + sleep 1 +} + +delete_device() { + local id="${1:-1}" + echo "$id" > /sys/bus/netdevsim/del_device 2>/dev/null || true + sleep 0.5 +} + +cleanup_all_devices() { + for dev in /sys/bus/netdevsim/devices/netdevsim*; do + [[ -d "$dev" ]] || continue + local id + id=$(basename "$dev" | sed 's/netdevsim//') + echo "$id" > /sys/bus/netdevsim/del_device 2>/dev/null || true + done + sleep 0.5 +} + +# Return the sysfs lock_status path for a given device id. +# Path: /sys/bus/pci/devices//dpll/lock_status +dpll_sysfs_path() { + local id="${1:-1}" + local pci_prefix + pci_prefix=$(get_pci_domain) + local pci_addr="${pci_prefix}:$(printf '%02x' "$((id + 1))").0" + echo "/sys/bus/pci/devices/${pci_addr}/dpll/lock_status" +} + +HAS_GENL=false +check_genl_tool() { + if command -v python3 >/dev/null 2>&1; then + HAS_GENL=true + setup_genl_helper + fi +} + +# Shared python netlink helper written to a temp file at startup +GENL_HELPER="" +setup_genl_helper() { + GENL_HELPER=$(mktemp /tmp/dpll-genl-XXXXXX.py) + cat > "$GENL_HELPER" <<'PYEOF' +import socket, struct, json, sys + +NETLINK_GENERIC = 16 +NLM_F_REQUEST = 0x0001 +NLM_F_DUMP = 0x0300 +GENL_ID_CTRL = 0x10 + +def nl_msg(msg_type, flags, seq, payload): + hdr = struct.pack('=IHHII', len(payload) + 16, msg_type, flags, seq, 0) + return hdr + payload + +def genl_msg(cmd, version, attrs=b""): + return struct.pack('=BBH', cmd, version, 0) + attrs + +def nl_attr(attr_type, data): + alen = 4 + len(data) + pad = (4 - (alen % 4)) % 4 + return struct.pack('=HH', alen, attr_type) + data + b'\x00' * pad + +def parse_attrs(data): + attrs = {} + while len(data) >= 4: + alen, atype = struct.unpack('=HH', data[:4]) + if alen < 4: break + attrs[atype] = data[4:alen] + data = data[((alen + 3) & ~3):] + return attrs + +def resolve_family(sock, name): + payload = genl_msg(3, 1, nl_attr(2, name.encode() + b'\x00')) + sock.send(nl_msg(GENL_ID_CTRL, NLM_F_REQUEST, 1, payload)) + resp = sock.recv(65536) + nltype = struct.unpack('=H', resp[4:6])[0] + if nltype == 2: return None + attrs = parse_attrs(resp[20:]) + if 1 not in attrs: return None + return struct.unpack('=H', attrs[1])[0] + +def genl_dump(sock, family_id, cmd, seq=2): + payload = genl_msg(cmd, 1) + sock.send(nl_msg(family_id, NLM_F_REQUEST | NLM_F_DUMP, seq, payload)) + results = [] + while True: + resp = sock.recv(65536) + offset = 0 + while offset + 16 <= len(resp): + msg_len = struct.unpack('=I', resp[offset:offset+4])[0] + if msg_len < 16: break + msg_type = struct.unpack('=H', resp[offset+4:offset+6])[0] + if msg_type == 3: return results # NLMSG_DONE + if msg_type == 2: return results # NLMSG_ERROR + data = resp[offset+16:offset+msg_len] + if len(data) >= 4: + results.append(parse_attrs(data[4:])) + offset += (msg_len + 3) & ~3 + return results + +def main(): + cmd = sys.argv[1] if len(sys.argv) > 1 else "devices" + sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC) + sock.settimeout(3) + sock.bind((0, 0)) + fam = resolve_family(sock, "dpll") + if fam is None: + print("[]"); sock.close(); return + + # DPLL UAPI attribute IDs (from linux/dpll.h) + # Device: ID=1, MODULE_NAME=2, PAD=3, CLOCK_ID=4, MODE=5, + # MODE_SUPPORTED=6, LOCK_STATUS=7, TEMP=8, TYPE=9 + # Pin: ID=1, PARENT_ID=2, MODULE_NAME=3, PAD=4, CLOCK_ID=5, + # BOARD_LABEL=6, PANEL_LABEL=7, PACKAGE_LABEL=8, TYPE=9 + + if cmd == "devices": + raw = genl_dump(sock, fam, 2) # DPLL_CMD_DEVICE_GET + devs = [] + for attrs in raw: + d = {} + if 1 in attrs and len(attrs[1]) >= 4: + d["id"] = struct.unpack("=I", attrs[1][:4])[0] + if 7 in attrs and len(attrs[7]) >= 4: + d["lock-status"] = struct.unpack("=I", attrs[7][:4])[0] + if 5 in attrs and len(attrs[5]) >= 4: + d["mode"] = struct.unpack("=I", attrs[5][:4])[0] + if 9 in attrs and len(attrs[9]) >= 4: + d["type"] = struct.unpack("=I", attrs[9][:4])[0] + devs.append(d) + print(json.dumps(devs)) + + elif cmd == "pins": + raw = genl_dump(sock, fam, 8) # DPLL_CMD_PIN_GET + pins = [] + for attrs in raw: + p = {} + if 1 in attrs and len(attrs[1]) >= 4: + p["id"] = struct.unpack("=I", attrs[1][:4])[0] + if 6 in attrs: + p["board-label"] = attrs[6].rstrip(b'\x00').decode(errors="replace") + if 9 in attrs and len(attrs[9]) >= 4: + p["type"] = struct.unpack("=I", attrs[9][:4])[0] + pins.append(p) + print(json.dumps(pins)) + + sock.close() + +if __name__ == "__main__": + main() +PYEOF +} + +genl_dpll_device_dump() { + python3 "$GENL_HELPER" devices 2>/dev/null || echo "[]" +} + +genl_dpll_pin_dump() { + python3 "$GENL_HELPER" pins 2>/dev/null || echo "[]" +} + +# --------------------------------------------------------------------------- +# Trap: cleanup on exit +# --------------------------------------------------------------------------- +trap_cleanup() { + cleanup_all_devices + [[ -n "${GENL_HELPER:-}" && -f "${GENL_HELPER:-}" ]] && rm -f "$GENL_HELPER" +} +trap trap_cleanup EXIT + +# =================================================================== +# TEST SUITE +# =================================================================== + +log "netdevsim DPLL unit tests" +echo " Kernel: $(uname -r)" +echo " Date: $(date -u)" +echo + +check_genl_tool + +# ------------------------------------------------------------------- +# 1. Module loading +# ------------------------------------------------------------------- +log "1. Module loading" + +if [[ "$NO_LOAD" == false ]]; then + rmmod netdevsim 2>/dev/null || true + rmmod nsim_dpll 2>/dev/null || true + rmmod nsim_ptp_mock 2>/dev/null || true + rmmod nsim_ptp 2>/dev/null || true + + modprobe gnss 2>/dev/null || true + modprobe nsim_ptp && pass "nsim_ptp loaded" || fail "nsim_ptp load failed" + modprobe nsim_ptp_mock && pass "nsim_ptp_mock loaded" || fail "nsim_ptp_mock load failed" + modprobe nsim_dpll && pass "nsim_dpll loaded" || fail "nsim_dpll load failed" + modprobe netdevsim pci_bus_nr=0x1f && pass "netdevsim loaded" || fail "netdevsim load failed" +else + if grep -q '^nsim_dpll ' /proc/modules 2>/dev/null; then + pass "nsim_dpll already loaded" + else + fail "nsim_dpll not loaded" + fi + if grep -q '^netdevsim ' /proc/modules 2>/dev/null; then + pass "netdevsim already loaded" + else + fail "netdevsim not loaded" + fi +fi + +echo + +# ------------------------------------------------------------------- +# 2. Device creation with DPLL (wpc=1) +# ------------------------------------------------------------------- +log "2. Device creation with wpc=1" + +cleanup_all_devices +create_device 1 1 2 1 1 + +DPLL_SYSFS=$(dpll_sysfs_path 1) + +assert_file_exists "netdevsim1 bus device exists" \ + /sys/bus/netdevsim/devices/netdevsim1 + +assert_file_exists "sysfs dpll dir exists" \ + "$(dirname "$DPLL_SYSFS")" + +assert_file_exists "sysfs lock_status attribute exists" \ + "$DPLL_SYSFS" + +echo + +# ------------------------------------------------------------------- +# 3. Sysfs lock_status — default value +# ------------------------------------------------------------------- +log "3. Sysfs lock_status — default value" + +STATUS=$(cat $DPLL_SYSFS) +assert_eq "default lock_status is 'locked'" "locked" "$STATUS" + +echo + +# ------------------------------------------------------------------- +# 4. Sysfs lock_status — write transitions +# ------------------------------------------------------------------- +log "4. Sysfs lock_status — write transitions" + +echo "holdover" > $DPLL_SYSFS +STATUS=$(cat $DPLL_SYSFS) +assert_eq "write 'holdover' -> read 'holdover'" "holdover" "$STATUS" + +echo "freerun" > $DPLL_SYSFS +STATUS=$(cat $DPLL_SYSFS) +assert_eq "write 'freerun' -> read 'freerun'" "freerun" "$STATUS" + +echo "locked" > $DPLL_SYSFS +STATUS=$(cat $DPLL_SYSFS) +assert_eq "write 'locked' -> read 'locked'" "locked" "$STATUS" + +echo + +# ------------------------------------------------------------------- +# 5. Sysfs lock_status — idempotent write (same value) +# ------------------------------------------------------------------- +log "5. Sysfs lock_status — idempotent write" + +echo "locked" > $DPLL_SYSFS +STATUS=$(cat $DPLL_SYSFS) +assert_eq "writing same value is idempotent" "locked" "$STATUS" + +echo + +# ------------------------------------------------------------------- +# 6. Sysfs lock_status — invalid input +# ------------------------------------------------------------------- +log "6. Sysfs lock_status — invalid input" + +if echo "bogus" > $DPLL_SYSFS 2>/dev/null; then + fail "writing 'bogus' should have returned error" +else + pass "writing 'bogus' correctly rejected" +fi + +STATUS=$(cat $DPLL_SYSFS) +assert_eq "lock_status unchanged after invalid write" "locked" "$STATUS" + +if echo "" > $DPLL_SYSFS 2>/dev/null; then + fail "writing empty string should have returned error" +else + pass "writing empty string correctly rejected" +fi + +if echo "LOCKED" > $DPLL_SYSFS 2>/dev/null; then + fail "writing 'LOCKED' (uppercase) should have returned error" +else + pass "writing 'LOCKED' (uppercase) correctly rejected" +fi + +echo + +# ------------------------------------------------------------------- +# 7. Sysfs lock_status — full state cycle +# ------------------------------------------------------------------- +log "7. Sysfs lock_status — full state cycle" + +for state in locked holdover freerun holdover locked freerun locked; do + echo "$state" > $DPLL_SYSFS + STATUS=$(cat $DPLL_SYSFS) + assert_eq "cycle: write '$state' -> read '$state'" "$state" "$STATUS" +done + +echo + +# ------------------------------------------------------------------- +# 8. GNSS device presence +# ------------------------------------------------------------------- +log "8. GNSS device presence" + +GNSS_DEVS=$(ls /sys/class/gnss/ 2>/dev/null || true) +if [[ -n "$GNSS_DEVS" ]]; then + pass "GNSS device registered in /sys/class/gnss/" + GNSS_DEV=$(echo "$GNSS_DEVS" | head -1) + assert_file_exists "/dev/${GNSS_DEV} character device" "/dev/${GNSS_DEV}" + + GNSS_TYPE=$(cat "/sys/class/gnss/${GNSS_DEV}/type" 2>/dev/null || true) + assert_eq "GNSS type is NMEA" "NMEA" "$GNSS_TYPE" +else + fail "no GNSS device found in /sys/class/gnss/" +fi + +echo + +# ------------------------------------------------------------------- +# 9. GNSS NMEA echo +# ------------------------------------------------------------------- +log "9. GNSS NMEA echo (write → read)" + +if [[ -n "${GNSS_DEV:-}" && -c "/dev/${GNSS_DEV}" ]]; then + chmod 666 "/dev/${GNSS_DEV}" 2>/dev/null || true + + GGA='$GNGGA,120000.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47' + echo "$GGA" > "/dev/${GNSS_DEV}" 2>/dev/null || true + + READBACK=$(timeout 2 dd if="/dev/${GNSS_DEV}" bs=512 count=1 2>/dev/null || true) + if [[ -n "$READBACK" ]]; then + pass "GNSS device returned data after write" + else + skip "GNSS read returned empty (may need longer wait)" + fi +else + skip "GNSS device not available for echo test" +fi + +echo + +# ------------------------------------------------------------------- +# 10. PTP clock for DPLL device +# ------------------------------------------------------------------- +log "10. PTP clock" + +PTP_CLASS=$(ls /sys/class/nsim_ptp/ 2>/dev/null | head -1 || true) +if [[ -n "$PTP_CLASS" ]]; then + pass "nsim_ptp class device exists: $PTP_CLASS" +else + skip "nsim_ptp class device not found" +fi + +PTP_DEV=$(ls /dev/ptp* 2>/dev/null | head -1 || true) +if [[ -n "$PTP_DEV" ]]; then + pass "PTP device node exists: $PTP_DEV" +else + skip "PTP device node not found" +fi + +echo + +# ------------------------------------------------------------------- +# 11. Network interface for netdevsim device +# ------------------------------------------------------------------- +log "11. Network interface" + +PCI_PREFIX=$(get_pci_domain) +PCI_ADDR="${PCI_PREFIX}:02.0" +IFACE=$(ls "/sys/bus/pci/devices/${PCI_ADDR}/net/" 2>/dev/null | head -1 || true) +if [[ -n "$IFACE" ]]; then + pass "netdev interface found: $IFACE" + + ETHTOOL_OUT=$(ethtool -T "$IFACE" 2>/dev/null || true) + if echo "$ETHTOOL_OUT" | grep -qi "hardware"; then + pass "ethtool -T reports hardware timestamping" + else + skip "ethtool -T did not report hardware timestamping" + fi +else + skip "no network interface found for $PCI_ADDR" +fi + +echo + +# ------------------------------------------------------------------- +# 12. Generic netlink — DPLL device dump +# ------------------------------------------------------------------- +log "12. Generic netlink — DPLL device dump" + +if [[ "$HAS_GENL" == true ]]; then + DEVICES_JSON=$(genl_dpll_device_dump) + DEVICE_COUNT=$(echo "$DEVICES_JSON" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + + if [[ "$DEVICE_COUNT" -ge 2 ]]; then + pass "DPLL device dump returned $DEVICE_COUNT devices (expected >=2: PPS+EEC)" + elif [[ "$DEVICE_COUNT" -ge 1 ]]; then + pass "DPLL device dump returned $DEVICE_COUNT device(s)" + else + fail "DPLL device dump returned 0 devices" + fi + + if [[ "$DEVICE_COUNT" -ge 1 ]]; then + LOCK_STATUS=$(echo "$DEVICES_JSON" | python3 -c " +import sys, json +devs = json.load(sys.stdin) +ls = devs[0].get('lock-status', -1) +print(ls) +" 2>/dev/null || echo "-1") + # DPLL_LOCK_STATUS_LOCKED_HO_ACQ = 3 (kernel UAPI: UNLOCKED=1, LOCKED=2, LOCKED_HO_ACQ=3, HOLDOVER=4) + assert_eq "DPLL device lock-status via netlink is 3 (LOCKED_HO_ACQ)" "3" "$LOCK_STATUS" + + MODE=$(echo "$DEVICES_JSON" | python3 -c " +import sys, json +devs = json.load(sys.stdin) +print(devs[0].get('mode', -1)) +" 2>/dev/null || echo "-1") + assert_eq "DPLL device mode via netlink is 2 (AUTOMATIC)" "2" "$MODE" + fi +else + skip "python3 not available — skipping netlink DPLL device tests" +fi + +echo + +# ------------------------------------------------------------------- +# 13. Generic netlink — sysfs/netlink lock_status consistency +# ------------------------------------------------------------------- +log "13. Sysfs/netlink lock_status consistency" + +if [[ "$HAS_GENL" == true ]]; then + for sysfs_val in holdover freerun locked; do + echo "$sysfs_val" > $DPLL_SYSFS + sleep 0.2 + + DEVICES_JSON=$(genl_dpll_device_dump) + NL_STATUS=$(echo "$DEVICES_JSON" | python3 -c " +import sys, json +devs = json.load(sys.stdin) +if devs: + print(devs[0].get('lock-status', -1)) +else: + print(-1) +" 2>/dev/null || echo "-1") + + # Kernel UAPI: UNLOCKED=1, LOCKED=2, LOCKED_HO_ACQ=3, HOLDOVER=4 + case "$sysfs_val" in + locked) EXPECTED_NL=3 ;; # DPLL_LOCK_STATUS_LOCKED_HO_ACQ + holdover) EXPECTED_NL=4 ;; # DPLL_LOCK_STATUS_HOLDOVER + freerun) EXPECTED_NL=1 ;; # DPLL_LOCK_STATUS_UNLOCKED + esac + + assert_eq "sysfs '$sysfs_val' -> netlink status=$EXPECTED_NL" \ + "$EXPECTED_NL" "$NL_STATUS" + done + + echo "locked" > $DPLL_SYSFS +else + skip "python3 not available — skipping sysfs/netlink consistency tests" +fi + +echo + +# ------------------------------------------------------------------- +# 14. Generic netlink — DPLL pin dump +# ------------------------------------------------------------------- +log "14. Generic netlink — DPLL pin dump" + +if [[ "$HAS_GENL" == true ]]; then + PINS_JSON=$(genl_dpll_pin_dump) + PIN_COUNT=$(echo "$PINS_JSON" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + + if [[ "$PIN_COUNT" -ge 7 ]]; then + pass "DPLL pin dump returned $PIN_COUNT pins (expected >=7: 1 GNSS + 4 EXT + 2 SyncE)" + elif [[ "$PIN_COUNT" -ge 5 ]]; then + pass "DPLL pin dump returned $PIN_COUNT pins (expected >=5: 1 GNSS + 4 EXT)" + elif [[ "$PIN_COUNT" -ge 1 ]]; then + pass "DPLL pin dump returned $PIN_COUNT pin(s)" + else + fail "DPLL pin dump returned 0 pins" + fi + + if [[ "$PIN_COUNT" -ge 1 ]]; then + GNSS_LABEL=$(echo "$PINS_JSON" | python3 -c " +import sys, json +pins = json.load(sys.stdin) +for p in pins: + if p.get('board-label', '') == 'GNSS-1PPS': + print('found') + break +else: + print('missing') +" 2>/dev/null || echo "error") + assert_eq "GNSS-1PPS pin present in pin dump" "found" "$GNSS_LABEL" + + EXT_LABELS=$(echo "$PINS_JSON" | python3 -c " +import sys, json +pins = json.load(sys.stdin) +labels = sorted([p.get('board-label','') for p in pins + if p.get('board-label','').startswith(('SMA','U.FL'))]) +print(' '.join(labels)) +" 2>/dev/null || echo "") + + assert_contains "SMA1 pin present" "$EXT_LABELS" "SMA1" + assert_contains "SMA2 pin present" "$EXT_LABELS" "SMA2" + assert_contains "U.FL1 pin present" "$EXT_LABELS" "U.FL1" + assert_contains "U.FL2 pin present" "$EXT_LABELS" "U.FL2" + fi +else + skip "python3 not available — skipping netlink DPLL pin tests" +fi + +echo + +# ------------------------------------------------------------------- +# 15. Device without DPLL (wpc=0) +# ------------------------------------------------------------------- +log "15. Device without DPLL (wpc=0)" + +cleanup_all_devices +create_device 2 1 2 1 0 + +assert_file_exists "netdevsim2 bus device exists (wpc=0)" \ + /sys/bus/netdevsim/devices/netdevsim2 + +WPC0_DPLL_DIR=$(dirname "$(dpll_sysfs_path 2)") +if [[ -d "$WPC0_DPLL_DIR" ]]; then + fail "sysfs dpll dir should NOT exist with wpc=0" +else + pass "sysfs dpll dir correctly absent with wpc=0" +fi + +delete_device 2 + +echo + +# ------------------------------------------------------------------- +# 16. Device teardown and re-creation +# ------------------------------------------------------------------- +log "16. Device teardown and re-creation" + +cleanup_all_devices + +DPLL_SYSFS=$(dpll_sysfs_path 1) +assert_file_not_exists "sysfs dpll dir absent after teardown" \ + "$(dirname "$DPLL_SYSFS")" + +create_device 1 1 2 1 1 + +assert_file_exists "sysfs dpll dir re-appears after re-creation" \ + "$(dirname "$DPLL_SYSFS")" + +STATUS=$(cat $DPLL_SYSFS) +assert_eq "lock_status defaults to 'locked' after re-creation" "locked" "$STATUS" + +echo "holdover" > $DPLL_SYSFS +STATUS=$(cat $DPLL_SYSFS) +assert_eq "lock_status writable after re-creation" "holdover" "$STATUS" + +echo + +# ------------------------------------------------------------------- +# 17. Rapid state transitions +# ------------------------------------------------------------------- +log "17. Rapid state transitions (stress)" + +RAPID_PASS=true +for _ in $(seq 1 50); do + for state in locked holdover freerun; do + echo "$state" > $DPLL_SYSFS + done +done + +FINAL=$(cat $DPLL_SYSFS) +assert_eq "lock_status consistent after 150 rapid writes" "freerun" "$FINAL" + +echo + +# ------------------------------------------------------------------- +# 18. dmesg sanity — no kernel warnings/errors from netdevsim DPLL +# ------------------------------------------------------------------- +log "18. dmesg sanity check" + +DMESG_DPLL=$(dmesg | grep -i "netdevsim.*dpll\|nsim_dpll" || true) +if echo "$DMESG_DPLL" | grep -qiE "error|warning|bug|oops|panic|call.trace"; then + fail "dmesg contains errors/warnings related to DPLL" + echo "$DMESG_DPLL" | grep -iE "error|warning|bug|oops|panic|call.trace" | head -5 +else + pass "no DPLL errors/warnings in dmesg" +fi + +echo + +# ------------------------------------------------------------------- +# 19. Cleanup +# ------------------------------------------------------------------- +log "19. Cleanup" + +cleanup_all_devices +pass "all devices cleaned up" + +echo + +# =================================================================== +# Summary +# =================================================================== +echo -e "${BOLD}============================================${NC}" +echo -e "${BOLD} DPLL Test Results${NC}" +echo -e "${BOLD}============================================${NC}" +echo -e " Total: ${TOTAL}" +echo -e " ${GREEN}Passed: ${PASS}${NC}" +echo -e " ${RED}Failed: ${FAIL}${NC}" +echo -e " ${YELLOW}Skipped: ${SKIP}${NC}" + +if [[ $FAIL -gt 0 ]]; then + echo -e "\n${RED} Failures:${NC}${FAILURES}" + echo + exit 1 +fi + +echo +echo -e "${GREEN}All tests passed.${NC}" +exit 0 diff --git a/scripts/test-gnss-ubx.sh b/scripts/test-gnss-ubx.sh new file mode 100755 index 0000000..1393401 --- /dev/null +++ b/scripts/test-gnss-ubx.sh @@ -0,0 +1,560 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Unit tests for GNSS device emulation and UBX protocol (netdevsim/dpll.c). +# +# Exercises: +# - UBX MON-VER request → firmware version response +# - UBX CFG-MSG → periodic NAV-STATUS / NAV-CLOCK injection +# - UBX CFG-VALSET INFIL_NCNOTHRS → signal block / restore +# - DPLL lock_status transitions during signal cycle +# - Sysfs isolation when signal_blocked +# - NMEA GGA fix quality parsing +# - NMEA forwarding during signal block +# - GGA parsing skipped during signal block +# - Full signal loss/recovery cycle +# - Multiple block/restore stress +# - UBX ACK for generic commands +# - Netlink consistency throughout +# +# Usage: sudo ./scripts/test-gnss-ubx.sh [--no-load] [--verbose] +# +set -eo pipefail + +NO_LOAD=false +VERBOSE=false +PASS=0; FAIL=0; SKIP=0; TOTAL=0; FAILURES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-load) NO_LOAD=true; shift ;; + --verbose) VERBOSE=true; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done +[[ "$VERBOSE" == true ]] && set -x + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BOLD='\033[1m'; NC='\033[0m' +log() { echo -e "${BOLD}==> $*${NC}"; } +pass() { ((TOTAL++)) || true; ((PASS++)) || true; echo -e " ${GREEN}PASS${NC}: $1"; } +fail() { ((TOTAL++)) || true; ((FAIL++)) || true; FAILURES="${FAILURES}\n - $1"; echo -e " ${RED}FAIL${NC}: $1"; } +skip() { ((TOTAL++)) || true; ((SKIP++)) || true; echo -e " ${YELLOW}SKIP${NC}: $1"; } +assert_eq() { local d="$1" e="$2" a="$3"; if [[ "$e" == "$a" ]]; then pass "$d"; else fail "$d (expected='$e', got='$a')"; fi; } + +get_pci_domain() { + local bus_nr bus fake_root domain + bus_nr=$(cat /sys/module/netdevsim/parameters/pci_bus_nr 2>/dev/null || echo 31) + bus=$(printf "%02x" "$bus_nr") + fake_root=$(ls /sys/bus/pci/devices/ 2>/dev/null | grep ":${bus}:00\.0" | head -1 || true) + domain="${fake_root%%:*}" + [[ -z "$domain" ]] && domain="0000" + echo "${domain}:${bus}" +} +create_device() { + local id="${1:-1}" pci_prefix; pci_prefix=$(get_pci_domain) + local pci_addr="${pci_prefix}:$(printf '%02x' "$((id+1))").0" + echo "${id} ${pci_addr} ${2:-1} ${3:-2} ${4:-1} ${5:-1}" > /sys/bus/netdevsim/new_device; sleep 1 +} +cleanup_all_devices() { + for dev in /sys/bus/netdevsim/devices/netdevsim*; do + [[ -d "$dev" ]] || continue + echo "$(basename "$dev" | sed 's/netdevsim//')" > /sys/bus/netdevsim/del_device 2>/dev/null || true + done; sleep 0.5 +} +dpll_sysfs_path() { + local id="${1:-1}" pci_prefix; pci_prefix=$(get_pci_domain) + echo "/sys/bus/pci/devices/${pci_prefix}:$(printf '%02x' "$((id+1))").0/dpll/lock_status" +} + +# ---- Python UBX helper ---- +UBX_HELPER="" +setup_ubx_helper() { + UBX_HELPER=$(mktemp /tmp/ubx-helper-XXXXXX.py) + cat > "$UBX_HELPER" <<'PYEOF' +import sys, os, struct, time, select, json + +S1, S2 = 0xB5, 0x62 +CLS_NAV, CLS_ACK, CLS_CFG, CLS_MON = 0x01, 0x05, 0x06, 0x0A +NAV_STATUS, NAV_CLOCK, ACK_ACK = 0x03, 0x22, 0x01 +CFG_MSG, CFG_VALSET, MON_VER = 0x01, 0x8A, 0x04 +KEY_NCNOTHRS = 0x201100aa + +def ck(data): + a = b = 0 + for x in data: a = (a+x)&0xFF; b = (b+a)&0xFF + return a, b + +def build(cls, mid, pl=b""): + h = bytes([S1, S2, cls, mid, len(pl)&0xFF, (len(pl)>>8)&0xFF]) + d = bytes([cls, mid, len(pl)&0xFF, (len(pl)>>8)&0xFF]) + pl + a, b = ck(d) + return h + pl + bytes([a, b]) + +def parse(data): + frames, i = [], 0 + while i+8 <= len(data): + if data[i]==S1 and data[i+1]==S2: + c, m = data[i+2], data[i+3] + plen = data[i+4]|(data[i+5]<<8) + t = 6+plen+2 + if i+t <= len(data): + frames.append({"class":c,"id":m,"payload":data[i+6:i+6+plen].hex(),"len":plen}) + i += t; continue + i += 1 + return frames + +def wr(dev, data, tmo=1.5): + fd = os.open(dev, os.O_RDWR|os.O_NONBLOCK) + try: + os.write(fd, data); time.sleep(0.1) + r = b""; dl = time.monotonic()+tmo + while time.monotonic()2 else None + + if cmd == "mon_ver": + r = wr(dev, build(CLS_MON, MON_VER)) + fs = parse(r) + print(json.dumps(fs)) + + elif cmd == "cfg_msg_nav": + f1 = build(CLS_CFG, CFG_MSG, bytes([CLS_NAV, NAV_STATUS, 1])) + f2 = build(CLS_CFG, CFG_MSG, bytes([CLS_NAV, NAV_CLOCK, 1])) + r = wr(dev, f1+f2, tmo=2.0) + fs = parse(r) + print(json.dumps(fs)) + + elif cmd == "signal_block": + v = int(sys.argv[3]) if len(sys.argv)>3 else 50 + pl = bytes([0,1,0,0]) + struct.pack('=5: + print(json.dumps({"gps_fix":p[4]})); return + buf = b"" # reset to avoid re-parsing + finally: + os.close(fd) + print(json.dumps({"gps_fix":-1})) + + elif cmd == "read_nav": + # Keep fd open and enable NAV injection, then read + fd = os.open(dev, os.O_RDWR|os.O_NONBLOCK) + try: + f1 = build(CLS_CFG, CFG_MSG, bytes([CLS_NAV, NAV_STATUS, 1])) + f2 = build(CLS_CFG, CFG_MSG, bytes([CLS_NAV, NAV_CLOCK, 1])) + os.write(fd, f1+f2) + time.sleep(0.2) + try: os.read(fd, 4096) # drain ACKs + except BlockingIOError: pass + dl = time.monotonic() + 3.0 + buf = b"" + while time.monotonic() < dl: + s, _, _ = select.select([fd],[],[],min(dl-time.monotonic(),0.3)) + if fd in s: + try: buf += os.read(fd, 4096) + except BlockingIOError: pass + fs = [f for f in parse(buf) if f["class"]==CLS_NAV] + print(json.dumps(fs)) + finally: + os.close(fd) + + elif cmd == "send_generic": + c, m = int(sys.argv[3],0), int(sys.argv[4],0) + r = wr(dev, build(c, m, b"\x00\x00")) + print(json.dumps(parse(r))) + +if __name__=="__main__": + main() +PYEOF + chmod +x "$UBX_HELPER" +} +ubx() { python3 "$UBX_HELPER" "$@" 2>/dev/null; } + +# ---- Netlink helper ---- +GENL_HELPER="" +setup_genl_helper() { + GENL_HELPER=$(mktemp /tmp/dpll-genl2-XXXXXX.py) + cat > "$GENL_HELPER" <<'PYEOF' +import socket, struct, json, sys +def nl(t,fl,sq,p): return struct.pack('=IHHII',len(p)+16,t,fl,sq,0)+p +def gm(c,v,a=b""): return struct.pack('=BBH',c,v,0)+a +def at(t,d): + l=4+len(d); p=(4-(l%4))%4; return struct.pack('=HH',l,t)+d+b'\x00'*p +def pa(d): + a={} + while len(d)>=4: + l,t=struct.unpack('=HH',d[:4]) + if l<4: break + a[t]=d[4:l]; d=d[((l+3)&~3):] + return a +s=socket.socket(socket.AF_NETLINK,socket.SOCK_RAW,16); s.settimeout(3); s.bind((0,0)) +p=gm(3,1,at(2,b"dpll\x00")); s.send(nl(0x10,1,1,p)); r=s.recv(65536) +if struct.unpack('=H',r[4:6])[0]==2: print("-1"); s.close(); sys.exit() +fa=pa(r[20:]); fam=struct.unpack('=H',fa[1])[0] +p=gm(2,1); s.send(nl(fam,0x301,2,p)); res=[] +while True: + r=s.recv(65536); o=0 + while o+16<=len(r): + ml=struct.unpack('=I',r[o:o+4])[0] + if ml<16: break + mt=struct.unpack('=H',r[o+4:o+6])[0] + if mt in (2,3): + if res: ls=struct.unpack('=I',res[0].get(7,b'\xff\xff\xff\xff')[:4])[0]; print(ls) + else: print("-1") + s.close(); sys.exit() + d=r[o+16:o+ml] + if len(d)>=4: res.append(pa(d[4:])) + o+=(ml+3)&~3 +PYEOF +} +genl_lock_status() { python3 "$GENL_HELPER" 2>/dev/null || echo "-1"; } + +trap_cleanup() { + cleanup_all_devices + [[ -f "${UBX_HELPER:-}" ]] && rm -f "$UBX_HELPER" + [[ -f "${GENL_HELPER:-}" ]] && rm -f "$GENL_HELPER" +} +trap trap_cleanup EXIT + +# =================================================================== +log "GNSS / UBX Protocol unit tests" +echo " Kernel: $(uname -r)"; echo " Date: $(date -u)"; echo + +[[ ! $(command -v python3) ]] && echo "ERROR: python3 required" && exit 1 +setup_ubx_helper; setup_genl_helper + +# --- 1. Module check --- +log "1. Module check" +if [[ "$NO_LOAD" == false ]]; then + rmmod netdevsim nsim_dpll nsim_ptp_mock nsim_ptp 2>/dev/null || true; sleep 0.5 + modprobe gnss 2>/dev/null || true + modprobe nsim_ptp && pass "nsim_ptp" || fail "nsim_ptp" + modprobe nsim_ptp_mock && pass "nsim_ptp_mock" || fail "nsim_ptp_mock" + modprobe nsim_dpll && pass "nsim_dpll" || fail "nsim_dpll" + modprobe netdevsim pci_bus_nr=0x1f && pass "netdevsim" || fail "netdevsim" +else + lsmod | grep -q netdevsim && pass "netdevsim loaded" || fail "netdevsim" +fi +echo + +# --- 2. Device + GNSS discovery --- +log "2. Device creation + GNSS discovery" +cleanup_all_devices; create_device 1 1 2 1 1 +DPLL_SYSFS=$(dpll_sysfs_path 1) +GNSS_DEV="" +for g in /sys/class/gnss/gnss*; do + [[ -d "$g" ]] || continue + n=$(basename "$g") + if [[ -c "/dev/$n" ]]; then GNSS_DEV="/dev/$n"; chmod 666 "$GNSS_DEV" 2>/dev/null; break; fi +done +[[ -n "$GNSS_DEV" ]] && pass "GNSS device: $GNSS_DEV" || fail "no GNSS chardev" +echo + +# --- 3. UBX MON-VER --- +log "3. UBX MON-VER request/response" +if [[ -n "$GNSS_DEV" ]]; then + R=$(ubx mon_ver "$GNSS_DEV") + C=$(echo "$R" | python3 -c "import sys,json;print(sum(1 for f in json.load(sys.stdin) if f['class']==0x0A and f['id']==0x04))") + (( C>=1 )) && pass "MON-VER response ($C)" || fail "no MON-VER response" + if (( C>=1 )); then + SW=$(echo "$R" | python3 -c " +import sys,json +for f in json.load(sys.stdin): + if f['class']==0x0A: + print(bytes.fromhex(f['payload'])[:30].split(b'\x00')[0].decode()); break") + echo "$SW" | grep -qF "SIM" && pass "MON-VER contains 'SIM'" || fail "MON-VER missing 'SIM' ($SW)" + fi +else skip "no GNSS"; fi +echo + +# --- 4. UBX CFG-MSG NAV enable --- +log "4. UBX CFG-MSG NAV-STATUS + NAV-CLOCK enable" +if [[ -n "$GNSS_DEV" ]]; then + R=$(ubx cfg_msg_nav "$GNSS_DEV") + A=$(echo "$R" | python3 -c "import sys,json;print(sum(1 for f in json.load(sys.stdin) if f['class']==0x05))") + (( A>=2 )) && pass "2 ACKs for CFG-MSG" || { (( A>=1 )) && pass "1 ACK for CFG-MSG" || fail "no ACK"; } +else skip "no GNSS"; fi +echo + +# --- 5. NAV periodic injection --- +log "5. NAV periodic injection (1 Hz)" +if [[ -n "$GNSS_DEV" ]]; then + sleep 1.5 + R=$(ubx read_nav "$GNSS_DEV") + C=$(echo "$R" | python3 -c "import sys,json;print(len(json.load(sys.stdin)))") + (( C>=1 )) && pass "NAV frames received ($C)" || fail "no NAV frames" + if (( C>=1 )); then + HS=$(echo "$R" | python3 -c "import sys,json;print(1 if any(f['id']==0x03 for f in json.load(sys.stdin)) else 0)") + HC=$(echo "$R" | python3 -c "import sys,json;print(1 if any(f['id']==0x22 for f in json.load(sys.stdin)) else 0)") + [[ "$HS" == "1" ]] && pass "NAV-STATUS present" || fail "NAV-STATUS missing" + [[ "$HC" == "1" ]] && pass "NAV-CLOCK present" || fail "NAV-CLOCK missing" + fi +else skip "no GNSS"; fi +echo + +# --- 6. gpsFix = 3 in normal state --- +log "6. gpsFix = 3 (3D) in normal state" +if [[ -n "$GNSS_DEV" ]]; then + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + [[ "$F" == "3" ]] && pass "gpsFix=3" || { [[ "$F" == "-1" ]] && skip "no NAV-STATUS" || fail "gpsFix=$F"; } +else skip "no GNSS"; fi +echo + +# --- 7. NMEA echo --- +log "7. NMEA echo (write → read)" +if [[ -n "$GNSS_DEV" ]]; then + GGA='$GNGGA,120000.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47' + E=$(ubx write_nmea "$GNSS_DEV" "$GGA" | python3 -c "import sys,json;print(json.load(sys.stdin)['echoed'])") + [[ "$E" == "True" ]] && pass "NMEA echoed" || fail "NMEA not echoed" +else skip "no GNSS"; fi +echo + +# --- 8. NMEA GGA fix quality parsing --- +log "8. NMEA GGA fix quality parsing" +if [[ -n "$GNSS_DEV" ]]; then + # fix=0 → gpsFix=0 + ubx write_nmea "$GNSS_DEV" '$GNGGA,120000.00,4807.038,N,01131.000,E,0,08,0.9,545.4,M,47.0,M,,*46' >/dev/null + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + [[ "$F" == "0" ]] && pass "GGA fix=0 → gpsFix=0" || { [[ "$F" == "-1" ]] && skip "no NAV" || fail "gpsFix=$F (expected 0)"; } + + # fix=1 → gpsFix=3 + ubx write_nmea "$GNSS_DEV" '$GNGGA,120000.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47' >/dev/null + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + [[ "$F" == "3" ]] && pass "GGA fix=1 → gpsFix=3" || { [[ "$F" == "-1" ]] && skip "no NAV" || fail "gpsFix=$F (expected 3)"; } +else skip "no GNSS"; fi +echo + +# --- 9. Signal block via UBX CFG-VALSET --- +log "9. UBX CFG-VALSET signal block (INFIL_NCNOTHRS=50)" +if [[ -n "$GNSS_DEV" ]]; then + S=$(cat "$DPLL_SYSFS"); assert_eq "pre-block is 'locked'" "locked" "$S" + + R=$(ubx signal_block "$GNSS_DEV" 50) + A=$(echo "$R" | python3 -c "import sys,json;print(sum(1 for f in json.load(sys.stdin) if f['class']==0x05))") + (( A>=1 )) && pass "ACK for signal_block" || fail "no ACK" + sleep 0.3 + + S=$(cat "$DPLL_SYSFS"); assert_eq "post-block is 'holdover'" "holdover" "$S" + NL=$(genl_lock_status); assert_eq "netlink=4 (HOLDOVER)" "4" "$NL" +else skip "no GNSS"; fi +echo + +# --- 10. Sysfs isolated during signal_blocked --- +log "10. Sysfs isolation during signal_blocked" +if [[ -n "$GNSS_DEV" ]]; then + echo "locked" > "$DPLL_SYSFS" 2>/dev/null || true + S=$(cat "$DPLL_SYSFS"); assert_eq "write 'locked' ignored" "holdover" "$S" + echo "freerun" > "$DPLL_SYSFS" 2>/dev/null || true + S=$(cat "$DPLL_SYSFS"); assert_eq "write 'freerun' ignored" "holdover" "$S" +else skip "no GNSS"; fi +echo + +# --- 11. gpsFix = 0 during signal block --- +log "11. gpsFix = 0 during signal block" +if [[ -n "$GNSS_DEV" ]]; then + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + [[ "$F" == "0" ]] && pass "gpsFix=0 during block" || { [[ "$F" == "-1" ]] && skip "no NAV" || fail "gpsFix=$F"; } +else skip "no GNSS"; fi +echo + +# --- 12. NMEA forwarded during signal block --- +log "12. NMEA forwarding during signal block" +if [[ -n "$GNSS_DEV" ]]; then + E=$(ubx write_nmea "$GNSS_DEV" '$GNGGA,130000.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*56' | python3 -c "import sys,json;print(json.load(sys.stdin)['echoed'])") + [[ "$E" == "True" ]] && pass "NMEA forwarded during block" || fail "NMEA NOT forwarded" +else skip "no GNSS"; fi +echo + +# --- 13. GGA parsing skipped during block --- +log "13. GGA parsing skipped during signal block" +if [[ -n "$GNSS_DEV" ]]; then + ubx write_nmea "$GNSS_DEV" '$GNGGA,140000.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*50' >/dev/null + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + [[ "$F" == "0" ]] && pass "GGA parsing skipped (gpsFix stays 0)" || fail "GGA NOT skipped (gpsFix=$F)" +else skip "no GNSS"; fi +echo + +# --- 14. Signal restore via UBX CFG-VALSET --- +log "14. UBX CFG-VALSET signal restore (INFIL_NCNOTHRS=0)" +if [[ -n "$GNSS_DEV" ]]; then + R=$(ubx signal_restore "$GNSS_DEV") + A=$(echo "$R" | python3 -c "import sys,json;print(sum(1 for f in json.load(sys.stdin) if f['class']==0x05))") + (( A>=1 )) && pass "ACK for signal_restore" || fail "no ACK" + sleep 0.3 + + S=$(cat "$DPLL_SYSFS"); assert_eq "post-restore is 'locked'" "locked" "$S" + NL=$(genl_lock_status); assert_eq "netlink=3 (LOCKED_HO_ACQ)" "3" "$NL" +else skip "no GNSS"; fi +echo + +# --- 15. Sysfs unblocked after restore --- +log "15. Sysfs unblocked after restore" +if [[ -n "$GNSS_DEV" ]]; then + echo "holdover" > "$DPLL_SYSFS"; S=$(cat "$DPLL_SYSFS"); assert_eq "write 'holdover' works" "holdover" "$S" + echo "locked" > "$DPLL_SYSFS"; S=$(cat "$DPLL_SYSFS"); assert_eq "write 'locked' works" "locked" "$S" +else skip "no GNSS"; fi +echo + +# --- 16. gpsFix = 3 after restore --- +log "16. gpsFix = 3 after restore" +if [[ -n "$GNSS_DEV" ]]; then + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + [[ "$F" == "3" ]] && pass "gpsFix=3 restored" || fail "gpsFix=$F (expected 3)" +else skip "no GNSS"; fi +echo + +# --- 17. Full signal loss/recovery cycle --- +log "17. Full signal loss/recovery cycle" +if [[ -n "$GNSS_DEV" ]]; then + S=$(cat "$DPLL_SYSFS"); assert_eq "cycle: initial 'locked'" "locked" "$S" + + ubx signal_block "$GNSS_DEV" 50 >/dev/null; sleep 0.3 + S=$(cat "$DPLL_SYSFS"); assert_eq "cycle: block → 'holdover'" "holdover" "$S" + NL=$(genl_lock_status); assert_eq "cycle: netlink=4" "4" "$NL" + + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + assert_eq "cycle: gpsFix=0 during block" "0" "$F" + + ubx signal_restore "$GNSS_DEV" >/dev/null; sleep 0.3 + S=$(cat "$DPLL_SYSFS"); assert_eq "cycle: restore → 'locked'" "locked" "$S" + NL=$(genl_lock_status); assert_eq "cycle: netlink=3" "3" "$NL" + + sleep 1.5 + F=$(ubx read_nav_fix "$GNSS_DEV" | python3 -c "import sys,json;print(json.load(sys.stdin)['gps_fix'])") + assert_eq "cycle: gpsFix=3 after restore" "3" "$F" +else skip "no GNSS"; fi +echo + +# --- 18. Multiple block/restore cycles (stress) --- +log "18. Multiple block/restore cycles (x10)" +if [[ -n "$GNSS_DEV" ]]; then + OK=true + for i in $(seq 1 10); do + ubx signal_block "$GNSS_DEV" 50 >/dev/null; sleep 0.15 + S=$(cat "$DPLL_SYSFS") + [[ "$S" != "holdover" ]] && OK=false && fail "stress #$i block: got '$S'" && break + ubx signal_restore "$GNSS_DEV" >/dev/null; sleep 0.15 + S=$(cat "$DPLL_SYSFS") + [[ "$S" != "locked" ]] && OK=false && fail "stress #$i restore: got '$S'" && break + done + [[ "$OK" == true ]] && pass "10 block/restore cycles OK" +else skip "no GNSS"; fi +echo + +# --- 19. ACK for generic/unknown UBX commands --- +log "19. ACK for generic UBX command" +if [[ -n "$GNSS_DEV" ]]; then + R=$(ubx send_generic "$GNSS_DEV" 0x0B 0x01) + A=$(echo "$R" | python3 -c "import sys,json;print(sum(1 for f in json.load(sys.stdin) if f['class']==0x05))") + (( A>=1 )) && pass "ACK for class=0x0B" || fail "no ACK" +else skip "no GNSS"; fi +echo + +# --- 20. Teardown cleans GNSS --- +log "20. Teardown cleans GNSS" +cleanup_all_devices +G=$(ls /sys/class/gnss/ 2>/dev/null || true) +[[ -z "$G" ]] && pass "GNSS removed after teardown" || fail "GNSS still present: $G" +echo + +# --- 21. Re-creation restores everything --- +log "21. Re-creation restores GNSS + signal block" +create_device 1 1 2 1 1 +DPLL_SYSFS=$(dpll_sysfs_path 1) +G2="" +for g in /sys/class/gnss/gnss*; do + [[ -d "$g" ]] || continue; n=$(basename "$g") + [[ -c "/dev/$n" ]] && G2="/dev/$n" && chmod 666 "$G2" 2>/dev/null && break +done +[[ -n "$G2" ]] && pass "GNSS re-registered" || fail "no GNSS after re-create" +S=$(cat "$DPLL_SYSFS"); assert_eq "lock_status defaults locked" "locked" "$S" +if [[ -n "$G2" ]]; then + ubx signal_block "$G2" 50 >/dev/null; sleep 0.3 + S=$(cat "$DPLL_SYSFS"); assert_eq "block works after re-create" "holdover" "$S" + ubx signal_restore "$G2" >/dev/null; sleep 0.3 + S=$(cat "$DPLL_SYSFS"); assert_eq "restore works after re-create" "locked" "$S" +fi +echo + +# --- 22. dmesg sanity --- +log "22. dmesg sanity" +D=$(dmesg | grep -iE "gnss|ubx|dpll" | tail -20 || true) +echo "$D" | grep -qiE "bug|oops|panic|call.trace|rcu.*stall" && fail "dmesg errors" || pass "dmesg clean" +echo + +# --- 23. Final cleanup --- +log "23. Cleanup" +cleanup_all_devices; pass "cleaned up" +echo + +# =================================================================== +echo -e "${BOLD}============================================${NC}" +echo -e "${BOLD} GNSS / UBX Test Results${NC}" +echo -e "${BOLD}============================================${NC}" +echo -e " Total: ${TOTAL}" +echo -e " ${GREEN}Passed: ${PASS}${NC}" +echo -e " ${RED}Failed: ${FAIL}${NC}" +echo -e " ${YELLOW}Skipped: ${SKIP}${NC}" +[[ $FAIL -gt 0 ]] && echo -e "\n${RED} Failures:${NC}${FAILURES}" && echo && exit 1 +echo; echo -e "${GREEN}All tests passed.${NC}"; exit 0 diff --git a/scripts/test-phc.sh b/scripts/test-phc.sh new file mode 100755 index 0000000..f10ccc3 --- /dev/null +++ b/scripts/test-phc.sh @@ -0,0 +1,1122 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Unit tests for the mock PTP Hardware Clock (ptp/ptp_mock.c). +# +# Exercises: +# - PHC device discovery via nsim_ptp class +# - gettime64 / settime64 (time read / write) +# - adjtime (time step) +# - adjfine (frequency adjustment via scaled_ppm) +# - EXTTS enable, event delivery, second-boundary alignment +# - EXTTS self-correction after PHC time step +# - PHC sharing across ports (same logical_clk_id) +# - Pin configuration (2 pins: NONE, GNSS1PPS) +# +# Requirements: +# - Root privileges +# - DKMS modules installed and loaded +# - python3 (for PTP ioctl helpers) +# +# Usage: +# sudo ./scripts/test-phc.sh [--no-load] [--verbose] +# +set -eo pipefail + +NO_LOAD=false +VERBOSE=false +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 +FAILURES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-load) NO_LOAD=true; shift ;; + --verbose) VERBOSE=true; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +[[ "$VERBOSE" == true ]] && set -x + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +NC='\033[0m' + +log() { echo -e "${BOLD}==> $*${NC}"; } + +pass() { + ((TOTAL++)) || true + ((PASS++)) || true + echo -e " ${GREEN}PASS${NC}: $1" +} + +fail() { + ((TOTAL++)) || true + ((FAIL++)) || true + FAILURES="${FAILURES}\n - $1" + echo -e " ${RED}FAIL${NC}: $1" +} + +skip() { + ((TOTAL++)) || true + ((SKIP++)) || true + echo -e " ${YELLOW}SKIP${NC}: $1" +} + +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + pass "$desc" + else + fail "$desc (expected='$expected', got='$actual')" + fi +} + +assert_range() { + local desc="$1" value="$2" min="$3" max="$4" + if (( value >= min && value <= max )); then + pass "$desc (value=$value in [$min, $max])" + else + fail "$desc (value=$value NOT in [$min, $max])" + fi +} + +get_pci_domain() { + local bus_nr bus fake_root domain + bus_nr=$(cat /sys/module/netdevsim/parameters/pci_bus_nr 2>/dev/null || echo 31) + bus=$(printf "%02x" "$bus_nr") + fake_root=$(ls /sys/bus/pci/devices/ 2>/dev/null | grep ":${bus}:00\.0" | head -1 || true) + if [[ -n "$fake_root" ]]; then + domain=$(echo "$fake_root" | cut -d: -f1) + else + domain="0000" + fi + echo "${domain}:${bus}" +} + +create_device() { + local id="${1:-1}" + local pci_prefix + pci_prefix=$(get_pci_domain) + local pci_addr="${pci_prefix}:$(printf '%02x' "$((id + 1))").0" + local clock_id="${2:-1}" + local ports="${3:-2}" + local queues="${4:-1}" + local wpc="${5:-1}" + echo "${id} ${pci_addr} ${clock_id} ${ports} ${queues} ${wpc}" \ + > /sys/bus/netdevsim/new_device + sleep 1 +} + +delete_device() { + local id="${1:-1}" + echo "$id" > /sys/bus/netdevsim/del_device 2>/dev/null || true + sleep 0.5 +} + +cleanup_all_devices() { + for dev in /sys/bus/netdevsim/devices/netdevsim*; do + [[ -d "$dev" ]] || continue + local id + id=$(basename "$dev" | sed 's/netdevsim//') + echo "$id" > /sys/bus/netdevsim/del_device 2>/dev/null || true + done + sleep 0.5 +} + +# --------------------------------------------------------------------------- +# Python PTP ioctl helper (embedded) +# --------------------------------------------------------------------------- +PTP_HELPER="" +setup_ptp_helper() { + PTP_HELPER=$(mktemp /tmp/ptp-helper-XXXXXX.py) + cat > "$PTP_HELPER" <<'PYEOF' +#!/usr/bin/env python3 +""" +Minimal PTP Hardware Clock ioctl helper for testing mock PHC. +Supports: gettime, settime, adjtime, adjfine, enable_extts, + disable_extts, read_extts, get_pins +""" +import sys, os, struct, fcntl, time, select + +# PTP ioctl numbers (from linux/ptp_clock.h) +PTP_CLK_MAGIC = ord('=') + +# struct ptp_clock_caps +PTP_CLOCK_GETCAPS = 0x80503d01 # _IOR('=', 1, 80 bytes) + +def _iowr(nr, size): + return 0xc0003d00 | (size << 16) | nr + +def _iow(nr, size): + return 0x40003d00 | (size << 16) | nr + +def _ior(nr, size): + return 0x80003d00 | (size << 16) | nr + +# struct ptp_sys_offset_precise: 3 * ptp_clock_time (3*16=48 bytes) +PTP_SYS_OFFSET_PRECISE = _iowr(8, 48) + +# PTP_CLOCK_SETTIME: struct timespec (16 bytes) +PTP_CLOCK_SETTIME = _iow(4, 16) + +# PTP_CLOCK_GETTIME: struct timespec (16 bytes) +PTP_CLOCK_GETTIME = _ior(9, 16) + +# PTP_CLOCK_ADJ: s64 (8 bytes) but uses struct ptp_clock_adj +# Actually adjtime uses PTP_CLOCK_ADJTIME +# struct timex is used for adjtime via clock_adjtime syscall. +# For simplicity, use clock_settime/clock_gettime via POSIX clock fd. + +# struct ptp_extts_request: 3 fields (index, flags, rsv) = 12 bytes → padded to 16 +PTP_EXTTS_REQUEST = _iow(2, 16) # _IOW('=', 2, struct ptp_extts_request) + +# struct ptp_extts_event: timestamp (16 bytes) + index (4) + flags (4) + rsv (4) = 28 → pad to 32 +PTP_EXTTS_EVENT_SIZE = 32 + +# PTP_PIN_GETFUNC — struct ptp_pin_desc: char[64] + 8*uint = 96 bytes +PTP_PIN_GETFUNC = _iowr(6, 96) + +# Flags +PTP_ENABLE_FEATURE = 1 +PTP_RISING_EDGE = 2 + +CLOCK_REALTIME = 0 + +import ctypes +import ctypes.util + +libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) + +# clock_gettime/clock_settime with dynamic clock ID +class timespec(ctypes.Structure): + _fields_ = [("tv_sec", ctypes.c_long), ("tv_nsec", ctypes.c_long)] + +def fd_to_clockid(fd): + return (~fd << 3) | 3 + +def phc_gettime(fd): + clk_id = fd_to_clockid(fd) + ts = timespec() + ret = libc.clock_gettime(clk_id, ctypes.byref(ts)) + if ret != 0: + raise OSError(ctypes.get_errno(), "clock_gettime failed") + return ts.tv_sec, ts.tv_nsec + +def phc_settime(fd, sec, nsec): + clk_id = fd_to_clockid(fd) + ts = timespec(sec, nsec) + ret = libc.clock_settime(clk_id, ctypes.byref(ts)) + if ret != 0: + raise OSError(ctypes.get_errno(), "clock_settime failed") + +# clock_adjtime for adjfine/adjtime +ADJ_SETOFFSET = 0x0100 +ADJ_FREQUENCY = 0x0002 +ADJ_NANO = 0x2000 + +class timex(ctypes.Structure): + _fields_ = [ + ("modes", ctypes.c_uint), + ("offset", ctypes.c_long), + ("freq", ctypes.c_long), + ("maxerror", ctypes.c_long), + ("esterror", ctypes.c_long), + ("status", ctypes.c_int), + ("constant", ctypes.c_long), + ("precision", ctypes.c_long), + ("tolerance", ctypes.c_long), + ("time_tv_sec", ctypes.c_long), + ("time_tv_usec", ctypes.c_long), + ("tick", ctypes.c_long), + ("ppsfreq", ctypes.c_long), + ("jitter", ctypes.c_long), + ("shift", ctypes.c_int), + ("stabil", ctypes.c_long), + ("jitcnt", ctypes.c_long), + ("calcnt", ctypes.c_long), + ("errcnt", ctypes.c_long), + ("stbcnt", ctypes.c_long), + ("tai", ctypes.c_int), + ("_pad", ctypes.c_int * 11), + ] + +def phc_adjtime(fd, delta_ns): + clk_id = fd_to_clockid(fd) + tx = timex() + tx.modes = ADJ_SETOFFSET | ADJ_NANO + if delta_ns >= 0: + tx.time_tv_sec = delta_ns // 1000000000 + tx.time_tv_usec = delta_ns % 1000000000 + else: + tx.time_tv_sec = -((-delta_ns - 1) // 1000000000 + 1) + tx.time_tv_usec = 1000000000 - ((-delta_ns) % 1000000000) + if tx.time_tv_usec == 1000000000: + tx.time_tv_usec = 0 + tx.time_tv_sec += 1 + ret = libc.clock_adjtime(clk_id, ctypes.byref(tx)) + if ret < 0: + raise OSError(ctypes.get_errno(), "clock_adjtime (adjtime) failed") + +def phc_adjfine(fd, scaled_ppm): + clk_id = fd_to_clockid(fd) + tx = timex() + tx.modes = ADJ_FREQUENCY + tx.freq = int(scaled_ppm) + ret = libc.clock_adjtime(clk_id, ctypes.byref(tx)) + if ret < 0: + raise OSError(ctypes.get_errno(), "clock_adjtime (adjfine) failed") + +def phc_enable_extts(fd, index=0, flags=PTP_RISING_EDGE | PTP_ENABLE_FEATURE): + buf = struct.pack('=IIii', index, flags, 0, 0) + fcntl.ioctl(fd, PTP_EXTTS_REQUEST, buf) + +def phc_disable_extts(fd, index=0): + buf = struct.pack('=IIii', index, 0, 0, 0) + fcntl.ioctl(fd, PTP_EXTTS_REQUEST, buf) + +def phc_read_extts(fd, timeout_s=3.0): + """Read EXTTS events from the PTP device (uses poll/read).""" + events = [] + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + remaining = max(0, deadline - time.monotonic()) + r, _, _ = select.select([fd], [], [], min(remaining, 0.5)) + if fd in r: + try: + data = os.read(fd, PTP_EXTTS_EVENT_SIZE * 16) + off = 0 + while off + PTP_EXTTS_EVENT_SIZE <= len(data): + chunk = data[off:off + PTP_EXTTS_EVENT_SIZE] + sec, nsec = struct.unpack_from('=qI', chunk, 0) + idx = struct.unpack_from('=I', chunk, 12)[0] + events.append((sec, nsec, idx)) + off += PTP_EXTTS_EVENT_SIZE + except Exception: + break + if len(events) >= 3: + break + return events + +def phc_get_pin(fd, index): + buf = bytearray(96) + struct.pack_into('=I', buf, 64, index) + result = fcntl.ioctl(fd, PTP_PIN_GETFUNC, bytes(buf)) + name = result[:64].split(b'\x00')[0].decode() + idx, func, chan = struct.unpack_from('=III', result, 64) + return {"name": name, "index": idx, "func": func, "chan": chan} + +def main(): + import json + cmd = sys.argv[1] + dev = sys.argv[2] + + fd = os.open(dev, os.O_RDWR) + try: + if cmd == "gettime": + sec, nsec = phc_gettime(fd) + print(json.dumps({"sec": sec, "nsec": nsec})) + + elif cmd == "settime": + sec = int(sys.argv[3]) + nsec = int(sys.argv[4]) if len(sys.argv) > 4 else 0 + phc_settime(fd, sec, nsec) + print(json.dumps({"ok": True})) + + elif cmd == "adjtime": + delta_ns = int(sys.argv[3]) + phc_adjtime(fd, delta_ns) + print(json.dumps({"ok": True})) + + elif cmd == "adjfine": + freq = int(sys.argv[3]) + phc_adjfine(fd, freq) + print(json.dumps({"ok": True})) + + elif cmd == "enable_extts": + idx = int(sys.argv[3]) if len(sys.argv) > 3 else 0 + phc_enable_extts(fd, idx) + print(json.dumps({"ok": True})) + + elif cmd == "disable_extts": + idx = int(sys.argv[3]) if len(sys.argv) > 3 else 0 + phc_disable_extts(fd, idx) + print(json.dumps({"ok": True})) + + elif cmd == "read_extts": + timeout = float(sys.argv[3]) if len(sys.argv) > 3 else 3.0 + events = phc_read_extts(fd, timeout) + print(json.dumps([{"sec": s, "nsec": n, "index": i} + for s, n, i in events])) + + elif cmd == "getpin": + idx = int(sys.argv[3]) + info = phc_get_pin(fd, idx) + print(json.dumps(info)) + + elif cmd == "read_extts_timed": + timeout = float(sys.argv[3]) if len(sys.argv) > 3 else 5.0 + max_events = int(sys.argv[4]) if len(sys.argv) > 4 else 10 + events = [] + deadline = time.monotonic() + timeout + while time.monotonic() < deadline and len(events) < max_events: + remaining = max(0, deadline - time.monotonic()) + r, _, _ = select.select([fd], [], [], min(remaining, 0.5)) + if fd in r: + try: + data = os.read(fd, PTP_EXTTS_EVENT_SIZE * 16) + wall = time.monotonic() + off = 0 + while off + PTP_EXTTS_EVENT_SIZE <= len(data): + chunk = data[off:off + PTP_EXTTS_EVENT_SIZE] + sec, nsec = struct.unpack_from('=qI', chunk, 0) + idx = struct.unpack_from('=I', chunk, 12)[0] + events.append({"sec": sec, "nsec": nsec, + "index": idx, "mono": wall}) + off += PTP_EXTTS_EVENT_SIZE + except Exception: + break + print(json.dumps(events)) + + else: + print(json.dumps({"error": f"unknown command: {cmd}"})) + sys.exit(1) + finally: + os.close(fd) + +if __name__ == "__main__": + main() +PYEOF + chmod +x "$PTP_HELPER" +} + +phc_cmd() { + python3 "$PTP_HELPER" "$@" 2>/dev/null +} + +phc_gettime_sec() { + phc_cmd gettime "$1" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])" +} + +phc_gettime_nsec() { + phc_cmd gettime "$1" | python3 -c "import sys,json; print(json.load(sys.stdin)['nsec'])" +} + +# --------------------------------------------------------------------------- +# Trap +# --------------------------------------------------------------------------- +trap_cleanup() { + cleanup_all_devices + [[ -n "${PTP_HELPER:-}" && -f "${PTP_HELPER:-}" ]] && rm -f "$PTP_HELPER" +} +trap trap_cleanup EXIT + +# =================================================================== +# TEST SUITE +# =================================================================== + +log "Mock PHC unit tests" +echo " Kernel: $(uname -r)" +echo " Date: $(date -u)" +echo + +if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 required" + exit 1 +fi + +setup_ptp_helper + +# ------------------------------------------------------------------- +# 1. Module loading +# ------------------------------------------------------------------- +log "1. Module check" + +if [[ "$NO_LOAD" == false ]]; then + rmmod netdevsim 2>/dev/null || true + rmmod nsim_dpll 2>/dev/null || true + rmmod nsim_ptp_mock 2>/dev/null || true + rmmod nsim_ptp 2>/dev/null || true + sleep 0.5 + + modprobe gnss 2>/dev/null || true + modprobe nsim_ptp && pass "nsim_ptp loaded" || fail "nsim_ptp load failed" + modprobe nsim_ptp_mock && pass "nsim_ptp_mock loaded" || fail "nsim_ptp_mock load failed" + modprobe nsim_dpll && pass "nsim_dpll loaded" || fail "nsim_dpll load failed" + modprobe netdevsim pci_bus_nr=0x1f && pass "netdevsim loaded" || fail "netdevsim load failed" +else + lsmod | grep -q nsim_ptp_mock && pass "nsim_ptp_mock loaded" || fail "nsim_ptp_mock not loaded" +fi + +echo + +# ------------------------------------------------------------------- +# 2. Device creation and PTP device discovery +# ------------------------------------------------------------------- +log "2. Device creation + PTP discovery" + +cleanup_all_devices +create_device 1 1 2 1 1 + +PCI_PREFIX=$(get_pci_domain) +PCI_ADDR="${PCI_PREFIX}:02.0" + +# Find PTP device via nsim_ptp class +PTP_CLASS_DEV=$(ls /sys/class/nsim_ptp/ 2>/dev/null | head -1 || true) +if [[ -z "$PTP_CLASS_DEV" ]]; then + echo "ERROR: No nsim_ptp class device found" + exit 1 +fi + +PTP_DEV="/dev/${PTP_CLASS_DEV}" +if [[ ! -c "$PTP_DEV" ]]; then + # Try via udev symlink + PTP_MAJOR=$(cat "/sys/class/nsim_ptp/${PTP_CLASS_DEV}/dev" | cut -d: -f1) + PTP_MINOR=$(cat "/sys/class/nsim_ptp/${PTP_CLASS_DEV}/dev" | cut -d: -f2) + PTP_DEV=$(ls /dev/ptp* 2>/dev/null | head -1 || true) + if [[ -z "$PTP_DEV" || ! -c "$PTP_DEV" ]]; then + echo "ERROR: Cannot find PTP character device" + exit 1 + fi +fi + +pass "PTP device found: $PTP_DEV" +echo + +# ------------------------------------------------------------------- +# 3. PHC gettime64 — initial time is reasonable +# ------------------------------------------------------------------- +log "3. PHC gettime64 — initial time" + +TIME_JSON=$(phc_cmd gettime "$PTP_DEV") +PHC_SEC=$(echo "$TIME_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") +NOW_SEC=$(date +%s) + +DIFF=$(( PHC_SEC - NOW_SEC )) +if (( DIFF < 0 )); then DIFF=$(( -DIFF )); fi + +# PHC is MONOTONIC-based, offset_ns starts at 0, so PHC time ≈ uptime +# After settime64 by ts2phc, it would be TAI. At creation, it's raw monotonic. +# Just verify it's a positive number. +if (( PHC_SEC > 0 )); then + pass "PHC gettime returns positive time (sec=$PHC_SEC)" +else + fail "PHC gettime returned non-positive time (sec=$PHC_SEC)" +fi + +echo + +# ------------------------------------------------------------------- +# 4. PHC settime64 / gettime64 round-trip +# ------------------------------------------------------------------- +log "4. PHC settime64 + gettime64 round-trip" + +TARGET_SEC=1700000000 +TARGET_NSEC=500000000 + +phc_cmd settime "$PTP_DEV" $TARGET_SEC $TARGET_NSEC >/dev/null + +READBACK=$(phc_cmd gettime "$PTP_DEV") +RB_SEC=$(echo "$READBACK" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") +RB_NSEC=$(echo "$READBACK" | python3 -c "import sys,json; print(json.load(sys.stdin)['nsec'])") + +# Allow 50ms tolerance for syscall latency +DIFF_SEC=$(( RB_SEC - TARGET_SEC )) +DIFF_NS=$(( (DIFF_SEC * 1000000000 + RB_NSEC) - TARGET_NSEC )) +if (( DIFF_NS < 0 )); then DIFF_NS=$(( -DIFF_NS )); fi + +if (( DIFF_NS < 50000000 )); then + pass "settime64/gettime64 round-trip (drift=${DIFF_NS}ns < 50ms)" +else + fail "settime64/gettime64 round-trip (drift=${DIFF_NS}ns > 50ms)" +fi + +# Verify settime resets freq_ppb to 0 (PHC should track monotonic rate after set) +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +sleep 1 +RB1=$(phc_cmd gettime "$PTP_DEV") +sleep 1 +RB2=$(phc_cmd gettime "$PTP_DEV") + +SEC1=$(echo "$RB1" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") +SEC2=$(echo "$RB2" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") +ELAPSED=$(( SEC2 - SEC1 )) + +# After 1 second sleep, PHC should advance ~1 second (freq_ppb=0) +if (( ELAPSED >= 0 && ELAPSED <= 2 )); then + pass "PHC advances ~1s per second after settime (elapsed=${ELAPSED}s)" +else + fail "PHC time advance unexpected after settime (elapsed=${ELAPSED}s)" +fi + +echo + +# ------------------------------------------------------------------- +# 5. PHC adjtime — time step +# ------------------------------------------------------------------- +log "5. PHC adjtime (time step)" + +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +sleep 0.1 + +BEFORE=$(phc_cmd gettime "$PTP_DEV") +BEFORE_SEC=$(echo "$BEFORE" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") + +# Step by +5 seconds +phc_cmd adjtime "$PTP_DEV" 5000000000 >/dev/null + +AFTER=$(phc_cmd gettime "$PTP_DEV") +AFTER_SEC=$(echo "$AFTER" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") + +STEP=$(( AFTER_SEC - BEFORE_SEC )) +if (( STEP >= 4 && STEP <= 6 )); then + pass "adjtime +5s applied correctly (measured step=${STEP}s)" +else + fail "adjtime +5s step incorrect (measured step=${STEP}s)" +fi + +# Negative step +phc_cmd adjtime "$PTP_DEV" -3000000000 >/dev/null + +AFTER_NEG=$(phc_cmd gettime "$PTP_DEV") +AFTER_NEG_SEC=$(echo "$AFTER_NEG" | python3 -c "import sys,json; print(json.load(sys.stdin)['sec'])") + +STEP_NEG=$(( AFTER_NEG_SEC - AFTER_SEC )) +if (( STEP_NEG >= -4 && STEP_NEG <= -2 )); then + pass "adjtime -3s applied correctly (measured step=${STEP_NEG}s)" +else + fail "adjtime -3s step incorrect (measured step=${STEP_NEG}s)" +fi + +echo + +# ------------------------------------------------------------------- +# 6. PHC adjfine — frequency adjustment +# ------------------------------------------------------------------- +log "6. PHC adjfine (frequency adjustment)" + +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +phc_cmd adjfine "$PTP_DEV" 0 >/dev/null +sleep 0.1 + +# Set a large positive frequency: +100 ppm = +6553600 scaled_ppm +# (scaled_ppm = ppm * 65536) +FREQ_SPM=6553600 # +100 ppm +phc_cmd adjfine "$PTP_DEV" $FREQ_SPM >/dev/null + +T1_JSON=$(phc_cmd gettime "$PTP_DEV") +sleep 2 +T2_JSON=$(phc_cmd gettime "$PTP_DEV") + +T1_NS=$(echo "$T1_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['sec']*1000000000+d['nsec'])") +T2_NS=$(echo "$T2_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['sec']*1000000000+d['nsec'])") + +ELAPSED_NS=$(( T2_NS - T1_NS )) + +# At +100 ppm over 2 seconds: expect ~2.0002s (200µs extra drift) +# PHC should advance MORE than 2.0 seconds +# Minimum: 2s baseline + some extra from freq correction +if (( ELAPSED_NS > 2000000000 )); then + pass "adjfine +100ppm: PHC advances faster than real time (${ELAPSED_NS}ns > 2e9)" +else + fail "adjfine +100ppm: PHC did not advance faster (${ELAPSED_NS}ns)" +fi + +# Reset frequency +phc_cmd adjfine "$PTP_DEV" 0 >/dev/null + +echo + +# ------------------------------------------------------------------- +# 7. Pin configuration +# ------------------------------------------------------------------- +log "7. Pin configuration" + +PIN0=$(phc_cmd getpin "$PTP_DEV" 0) +PIN0_NAME=$(echo "$PIN0" | python3 -c "import sys,json; print(json.load(sys.stdin)['name'])") +assert_eq "Pin 0 name is 'NONE'" "NONE" "$PIN0_NAME" + +PIN1=$(phc_cmd getpin "$PTP_DEV" 1) +PIN1_NAME=$(echo "$PIN1" | python3 -c "import sys,json; print(json.load(sys.stdin)['name'])") +assert_eq "Pin 1 name is 'GNSS1PPS'" "GNSS1PPS" "$PIN1_NAME" + +echo + +# ------------------------------------------------------------------- +# 8. EXTTS enable + event delivery +# ------------------------------------------------------------------- +log "8. EXTTS enable + event delivery" + +# Set PHC to known time near a second boundary +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +phc_cmd adjfine "$PTP_DEV" 0 >/dev/null +sleep 0.1 + +# Enable EXTTS on channel 0 +phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null + +# Read events (wait up to 4 seconds for at least 2 events) +EVENTS=$(phc_cmd read_extts "$PTP_DEV" 4.0) +EVENT_COUNT=$(echo "$EVENTS" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") + +if (( EVENT_COUNT >= 2 )); then + pass "EXTTS delivered $EVENT_COUNT events in 4s" +else + fail "EXTTS delivered only $EVENT_COUNT events (expected >= 2)" +fi + +# Disable EXTTS +phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null + +echo + +# ------------------------------------------------------------------- +# 9. EXTTS second-boundary alignment +# ------------------------------------------------------------------- +log "9. EXTTS second-boundary alignment" + +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +phc_cmd adjfine "$PTP_DEV" 0 >/dev/null +sleep 0.1 + +phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null +EVENTS=$(phc_cmd read_extts "$PTP_DEV" 5.0) +phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null + +# Check that EXTTS timestamps have nsec < 10ms (within 10ms of a second boundary) +ALIGNMENT_OK=$(echo "$EVENTS" | python3 -c " +import sys, json +events = json.load(sys.stdin) +if not events: + print('no_events') + sys.exit() +all_ok = True +for e in events: + nsec = e['nsec'] + off = min(nsec, 1000000000 - nsec) + if off > 10000000: + all_ok = False + print(f'bad:{nsec}') + break +if all_ok: + print('ok') +") + +if [[ "$ALIGNMENT_OK" == "ok" ]]; then + pass "EXTTS timestamps aligned to second boundary (< 10ms offset)" +elif [[ "$ALIGNMENT_OK" == "no_events" ]]; then + skip "No EXTTS events to check alignment" +else + fail "EXTTS timestamp NOT aligned: $ALIGNMENT_OK" +fi + +echo + +# ------------------------------------------------------------------- +# 10. EXTTS self-correction after PHC time step +# ------------------------------------------------------------------- +log "10. EXTTS self-correction after time step" + +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +phc_cmd adjfine "$PTP_DEV" 0 >/dev/null +sleep 0.1 + +phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null + +# Wait for first event to confirm timer is running +EVENTS_PRE=$(phc_cmd read_extts "$PTP_DEV" 2.0) +PRE_COUNT=$(echo "$EVENTS_PRE" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") + +if (( PRE_COUNT < 1 )); then + skip "No pre-step EXTTS events; cannot test self-correction" +else + # Step PHC by +500ms (deliberately misalign) + phc_cmd adjtime "$PTP_DEV" 500000000 >/dev/null + + # Read events for 3 more seconds — the timer should self-correct + EVENTS_POST=$(phc_cmd read_extts "$PTP_DEV" 4.0) + + CORRECTION_OK=$(echo "$EVENTS_POST" | python3 -c " +import sys, json +events = json.load(sys.stdin) +if len(events) < 2: + print('insufficient') + sys.exit() +last = events[-1] +nsec = last['nsec'] +off = min(nsec, 1000000000 - nsec) +if off < 50000000: + print('ok') +else: + print(f'bad:{nsec}') +") + + if [[ "$CORRECTION_OK" == "ok" ]]; then + pass "EXTTS self-corrected after 500ms PHC step" + elif [[ "$CORRECTION_OK" == "insufficient" ]]; then + skip "Insufficient post-step EXTTS events" + else + fail "EXTTS did NOT self-correct after step: $CORRECTION_OK" + fi +fi + +phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null + +echo + +# ------------------------------------------------------------------- +# 11. EXTTS with frequency adjustment +# ------------------------------------------------------------------- +log "11. EXTTS with frequency adjustment" + +phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null +phc_cmd adjfine "$PTP_DEV" 6553600 >/dev/null # +100 ppm +sleep 0.1 + +phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null +EVENTS=$(phc_cmd read_extts "$PTP_DEV" 5.0) +phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null +phc_cmd adjfine "$PTP_DEV" 0 >/dev/null + +# Even with +100ppm, EXTTS should still fire near second boundaries +FREQ_ALIGN=$(echo "$EVENTS" | python3 -c " +import sys, json +events = json.load(sys.stdin) +if len(events) < 2: + print('insufficient') + sys.exit() +last = events[-1] +nsec = last['nsec'] +off = min(nsec, 1000000000 - nsec) +if off < 50000000: + print('ok') +else: + print(f'bad:{nsec}') +") + +if [[ "$FREQ_ALIGN" == "ok" ]]; then + pass "EXTTS aligned at second boundary even with +100ppm freq adj" +elif [[ "$FREQ_ALIGN" == "insufficient" ]]; then + skip "Insufficient EXTTS events with freq adj" +else + fail "EXTTS misaligned with freq adj: $FREQ_ALIGN" +fi + +echo + +# ------------------------------------------------------------------- +# 12. PHC sharing (same logical_clk_id) +# ------------------------------------------------------------------- +log "12. PHC sharing across ports (same logical_clk_id)" + +cleanup_all_devices +create_device 1 1 2 1 1 # 2 ports, clock_id=1 + +PCI_PREFIX=$(get_pci_domain) +PCI_ADDR1="${PCI_PREFIX}:02.0" + +# Both ports should share the same PHC. +# Check via ethtool -T on both interfaces +IFACE1=$(ls "/sys/bus/pci/devices/${PCI_ADDR1}/net/" 2>/dev/null | head -1 || true) +IFACE2=$(ls "/sys/bus/pci/devices/${PCI_ADDR1}/net/" 2>/dev/null | tail -1 || true) + +if [[ -n "$IFACE1" && -n "$IFACE2" ]]; then + PHC_IDX1=$(ethtool -T "$IFACE1" 2>/dev/null | grep "PTP Hardware Clock" | awk '{print $NF}' || echo "-1") + PHC_IDX2=$(ethtool -T "$IFACE2" 2>/dev/null | grep "PTP Hardware Clock" | awk '{print $NF}' || echo "-1") + + if [[ "$PHC_IDX1" != "-1" && "$PHC_IDX1" == "$PHC_IDX2" ]]; then + pass "Both ports share PHC index $PHC_IDX1" + elif [[ "$PHC_IDX1" == "-1" ]]; then + skip "Cannot determine PHC index from ethtool" + else + fail "Ports have different PHC indices ($PHC_IDX1 vs $PHC_IDX2)" + fi +else + skip "Cannot find both network interfaces" +fi + +echo + +# ------------------------------------------------------------------- +# 13. Multiple settime64 operations +# ------------------------------------------------------------------- +log "13. Multiple settime64 operations (no corruption)" + +cleanup_all_devices +create_device 1 1 2 1 1 +PTP_DEV=$(ls /dev/ptp* 2>/dev/null | head -1 || true) + +if [[ -n "$PTP_DEV" && -c "$PTP_DEV" ]]; then + MULTI_OK=true + for t in 1000000000 2000000000 1500000000 1800000000 999999999; do + phc_cmd settime "$PTP_DEV" "$t" 0 >/dev/null + RB_SEC=$(phc_gettime_sec "$PTP_DEV") + DIFF=$(( RB_SEC - t )) + if (( DIFF < 0 )); then DIFF=$(( -DIFF )); fi + if (( DIFF > 1 )); then + MULTI_OK=false + break + fi + done + + if [[ "$MULTI_OK" == true ]]; then + pass "5 consecutive settime64 ops all read back correctly" + else + fail "settime64 corruption detected (t=$t, readback=$RB_SEC)" + fi +else + skip "No PTP device for multi-set test" +fi + +echo + +# ------------------------------------------------------------------- +# 14. REGRESSION: EXTTS with realistic TAI time (large offset_ns) +# ------------------------------------------------------------------- +log "14. REGRESSION: EXTTS with realistic TAI time (offset_ns ≈ 56 years)" + +# Bug: with CLOCK_MONOTONIC_RAW base, setting PHC to TAI time created +# offset_ns ≈ 1.78e18. ts2phc's servo drove freq_ppb to -500M, causing +# EXTTS to rapid-fire (5 events in 200ms) and ts2phc couldn't converge. +# The CLOCK_MONOTONIC fix keeps freq_ppb near 0. + +cleanup_all_devices +create_device 1 1 2 1 1 +PTP_DEV=$(ls /dev/ptp* 2>/dev/null | head -1 || true) + +if [[ -n "$PTP_DEV" && -c "$PTP_DEV" ]]; then + # Set PHC to realistic TAI time (≈ June 2026 TAI) + TAI_TIME=1780690000 + phc_cmd settime "$PTP_DEV" $TAI_TIME 0 >/dev/null + phc_cmd adjfine "$PTP_DEV" 0 >/dev/null + sleep 0.1 + + phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null + EVENTS=$(phc_cmd read_extts_timed "$PTP_DEV" 5.0 5) + phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null + + # Verify: events are ~1 second apart in monotonic time (not rapid-fire) + TAI_RESULT=$(echo "$EVENTS" | python3 -c " +import sys, json +events = json.load(sys.stdin) +if len(events) < 2: + print('insufficient') + sys.exit() +# Check monotonic spacing between events +spacings = [] +for i in range(1, len(events)): + dt = events[i]['mono'] - events[i-1]['mono'] + spacings.append(dt) +min_sp = min(spacings) +max_sp = max(spacings) +# Each spacing should be ~1 second (0.5 to 1.5 is acceptable) +if min_sp < 0.5: + print(f'rapid_fire:min_spacing={min_sp:.3f}s') +elif max_sp > 2.0: + print(f'too_slow:max_spacing={max_sp:.3f}s') +else: + print(f'ok:spacings={[round(s,3) for s in spacings]}') +") + + if [[ "$TAI_RESULT" == ok:* ]]; then + pass "EXTTS fires ~1Hz with TAI time (${TAI_RESULT#ok:})" + elif [[ "$TAI_RESULT" == "insufficient" ]]; then + skip "Not enough EXTTS events for TAI time test" + elif [[ "$TAI_RESULT" == rapid_fire:* ]]; then + fail "EXTTS rapid-fire with TAI time! ${TAI_RESULT#rapid_fire:}" + else + fail "EXTTS timing issue with TAI time: $TAI_RESULT" + fi + + # Verify EXTTS timestamps are near second boundaries + TAI_ALIGN=$(echo "$EVENTS" | python3 -c " +import sys, json +events = json.load(sys.stdin) +if not events: print('no_events'); sys.exit() +worst = 0 +for e in events: + off = min(e['nsec'], 1000000000 - e['nsec']) + worst = max(worst, off) +if worst < 10000000: + print(f'ok:worst_offset={worst}ns') +else: + print(f'bad:worst_offset={worst}ns') +") + if [[ "$TAI_ALIGN" == ok:* ]]; then + pass "EXTTS aligned at TAI second boundaries (${TAI_ALIGN#ok:})" + else + fail "EXTTS misaligned with TAI time: $TAI_ALIGN" + fi +else + skip "No PTP device for TAI time regression test" +fi + +echo + +# ------------------------------------------------------------------- +# 15. REGRESSION: EXTTS must not rapid-fire with extreme freq_ppb +# ------------------------------------------------------------------- +log "15. REGRESSION: EXTTS with extreme freq_ppb = -500M" + +# Bug: when freq_ppb = -500000000 (max negative), PHC advances at 50% +# speed. With CLOCK_MONOTONIC_RAW the EXTTS delay was doubled, but the +# self-correction loop produced 5 rapid-fire events in 200ms before +# converging. This test verifies that even with extreme freq, events +# don't rapid-fire below 200ms spacing. + +if [[ -n "$PTP_DEV" && -c "$PTP_DEV" ]]; then + phc_cmd settime "$PTP_DEV" 1780690000 0 >/dev/null + # Set extreme negative freq: -500M ppb = scaled_ppm -500M*65536/1000 = -32768000000 + # But max adjfine accepts is limited by max_adj (500M ppb). + # scaled_ppm = ppb * 65536 / 1000 + EXTREME_SPM=$(python3 -c "print(int(-500000000 * 65536 / 1000))") + phc_cmd adjfine "$PTP_DEV" $EXTREME_SPM >/dev/null + sleep 0.1 + + phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null + + # With freq_ppb = -500M, PHC runs at 50% speed, so EXTTS should fire + # every ~2 seconds in wall time (1 PHC second = 2 real seconds). + # Read for 8 seconds to get at least 2-3 events. + EVENTS=$(phc_cmd read_extts_timed "$PTP_DEV" 8.0 6) + phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null + phc_cmd adjfine "$PTP_DEV" 0 >/dev/null + + EXTREME_RESULT=$(echo "$EVENTS" | python3 -c " +import sys, json +events = json.load(sys.stdin) +if len(events) < 2: + print('insufficient') + sys.exit() +spacings = [events[i]['mono'] - events[i-1]['mono'] for i in range(1, len(events))] +min_sp = min(spacings) +# With -500M ppb, minimum inter-event spacing should be > 0.2s +# (the old bug caused 5 events in 0.2s, i.e. ~40ms spacing) +rapid_count = sum(1 for s in spacings if s < 0.2) +if rapid_count > 0: + print(f'rapid_fire:{rapid_count}_events_below_200ms,spacings={[round(s,3) for s in spacings]}') +else: + print(f'ok:min_spacing={min_sp:.3f}s,spacings={[round(s,3) for s in spacings]}') +") + + if [[ "$EXTREME_RESULT" == ok:* ]]; then + pass "No rapid-fire with freq_ppb=-500M (${EXTREME_RESULT#ok:})" + elif [[ "$EXTREME_RESULT" == "insufficient" ]]; then + skip "Not enough events for extreme freq test" + elif [[ "$EXTREME_RESULT" == rapid_fire:* ]]; then + fail "EXTTS rapid-fire with -500M ppb! ${EXTREME_RESULT#rapid_fire:}" + else + fail "EXTTS extreme freq issue: $EXTREME_RESULT" + fi +else + skip "No PTP device for extreme freq regression test" +fi + +echo + +# ------------------------------------------------------------------- +# 16. REGRESSION: freq_ppb stays small after realistic settime +# ------------------------------------------------------------------- +log "16. REGRESSION: freq_ppb stays near zero with MONOTONIC base" + +# Bug: with CLOCK_MONOTONIC_RAW, after settime to TAI, the PHC drifted +# and ts2phc pushed freq_ppb to -500M. With CLOCK_MONOTONIC base, the +# PHC should track real time closely and freq_ppb should stay near 0. + +if [[ -n "$PTP_DEV" && -c "$PTP_DEV" ]]; then + phc_cmd settime "$PTP_DEV" 1780690000 0 >/dev/null + phc_cmd adjfine "$PTP_DEV" 0 >/dev/null + sleep 0.1 + + # Read time twice 2s apart, measure drift + T1=$(phc_cmd gettime "$PTP_DEV") + WALL1=$(python3 -c "import time; print(time.monotonic())") + sleep 2 + T2=$(phc_cmd gettime "$PTP_DEV") + WALL2=$(python3 -c "import time; print(time.monotonic())") + + DRIFT_RESULT=$(python3 -c " +import json +t1 = json.loads('$T1') +t2 = json.loads('$T2') +phc1 = t1['sec'] + t1['nsec']/1e9 +phc2 = t2['sec'] + t2['nsec']/1e9 +wall_elapsed = $WALL2 - $WALL1 +phc_elapsed = phc2 - phc1 +# drift_ppm = (phc_elapsed / wall_elapsed - 1) * 1e6 +if wall_elapsed > 0: + drift_ppm = (phc_elapsed / wall_elapsed - 1.0) * 1e6 + if abs(drift_ppm) < 1000: + print(f'ok:drift={drift_ppm:.1f}ppm') + else: + print(f'bad:drift={drift_ppm:.1f}ppm') +else: + print('error') +") + + if [[ "$DRIFT_RESULT" == ok:* ]]; then + pass "PHC drift < 1000 ppm with MONOTONIC base (${DRIFT_RESULT#ok:})" + elif [[ "$DRIFT_RESULT" == bad:* ]]; then + fail "PHC drift too large! ${DRIFT_RESULT#bad:}" + else + fail "Could not measure PHC drift: $DRIFT_RESULT" + fi +else + skip "No PTP device for drift test" +fi + +echo + +# ------------------------------------------------------------------- +# 17. dmesg sanity +# ------------------------------------------------------------------- +log "17. dmesg sanity check" + +DMESG_PHC=$(dmesg | grep -i "ptp_mock\|mock_phc\|nsim_ptp" | tail -20 || true) +if echo "$DMESG_PHC" | grep -qiE "bug|oops|panic|call.trace|rcu.*stall"; then + fail "dmesg contains errors related to mock PHC" + echo "$DMESG_PHC" | grep -iE "bug|oops|panic|call.trace" | head -5 +else + pass "no PHC errors in dmesg" +fi + +echo + +# ------------------------------------------------------------------- +# 18. Cleanup +# ------------------------------------------------------------------- +log "18. Cleanup" + +cleanup_all_devices +pass "all devices cleaned up" + +echo + +# =================================================================== +# Summary +# =================================================================== +echo -e "${BOLD}============================================${NC}" +echo -e "${BOLD} Mock PHC Test Results${NC}" +echo -e "${BOLD}============================================${NC}" +echo -e " Total: ${TOTAL}" +echo -e " ${GREEN}Passed: ${PASS}${NC}" +echo -e " ${RED}Failed: ${FAIL}${NC}" +echo -e " ${YELLOW}Skipped: ${SKIP}${NC}" + +if [[ $FAIL -gt 0 ]]; then + echo -e "\n${RED} Failures:${NC}${FAILURES}" + echo + exit 1 +fi + +echo +echo -e "${GREEN}All tests passed.${NC}" +exit 0