[PATCH v3 7/7] test: add tests for python bindings
Alan Maguire <[email protected]> Tue, 21 Jul 2026 16:24:51 +0100
| Newsgroups | dev.linux.lists.dtrace |
|---|---|
| Message-ID | <[email protected]> |
Add tests covering aggregation snapshot, update, handling of stack keys. Also test process creation/grabbing, and ensure we detect processes going away or exit() calls in dt.work(). Also ensure that multiple sessions can exist in the process image. Signed-off-by: Alan Maguire <[email protected]> --- test/unittest/python/tst.aggr-actions.sh | 133 ++++++++++++++++++++++ test/unittest/python/tst.aggr-change.sh | 75 ++++++++++++ test/unittest/python/tst.aggr-stack.sh | 61 ++++++++++ test/unittest/python/tst.aggr.sh | 58 ++++++++++ test/unittest/python/tst.exit.sh | 64 +++++++++++ test/unittest/python/tst.multi-session.sh | 58 ++++++++++ test/unittest/python/tst.proc-create.sh | 56 +++++++++ test/unittest/python/tst.proc-exit.sh | 57 ++++++++++ test/unittest/python/tst.proc-grab.sh | 81 +++++++++++++ 9 files changed, 643 insertions(+) create mode 100755 test/unittest/python/tst.aggr-actions.sh create mode 100755 test/unittest/python/tst.aggr-change.sh create mode 100755 test/unittest/python/tst.aggr-stack.sh create mode 100755 test/unittest/python/tst.aggr.sh create mode 100755 test/unittest/python/tst.exit.sh create mode 100755 test/unittest/python/tst.multi-session.sh create mode 100755 test/unittest/python/tst.proc-create.sh create mode 100755 test/unittest/python/tst.proc-exit.sh create mode 100755 test/unittest/python/tst.proc-grab.sh diff --git a/test/unittest/python/tst.aggr-actions.sh b/test/unittest/python/tst.aggr-actions.sh new file mode 100755 index 00000000..8fd22c8c --- /dev/null +++ b/test/unittest/python/tst.aggr-actions.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure aggregation actions return expected values. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession + +prog = r""" +BEGIN +{ + @counts = count(); + @counts = count(); + @counts = count(); + @sums = sum(100); + @sums = sum(50); + @sums = sum(32); + @avgs = avg(4); + @avgs = avg(5); + @mins = min(100); + @mins = min(99); + @mins = min(101); + @maxs = max(12); + @maxs = max(19); + @maxs = max(-4); + @stddevs = stddev(5000000000); + @stddevs = stddev(5000000100); + @stddevs = stddev(5000000200); + @stddevs = stddev(5000000300); + @stddevs = stddev(5000000400); + @stddevs = stddev(5000000500); + @stddevs = stddev(5000000600); + @stddevs = stddev(5000000700); + @stddevs = stddev(5000000800); + @stddevs = stddev(5000000900); + @quants = quantize(1); + @quants = quantize(2); + @quants = quantize(1); + @quants = quantize(3); + @quants = quantize(128); + @lquants = lquantize(1, 1, 10, 2); + @lquants = lquantize(3, 1, 10, 2); + @lquants = lquantize(1, 1, 10, 2); + @lquants = lquantize(9, 1, 10, 2); + @lquants = lquantize(9, 1, 10, 2); + @lquants = lquantize(1, 1, 10, 2); + @llquants = llquantize(1, 3, 1, 4, 3); + @llquants = llquantize(26, 3, 1, 4, 3); + @llquants = llquantize(2, 3, 1, 4, 3); + @llquants = llquantize(13, 3, 1, 4, 3); + @llquants = llquantize(6, 3, 1, 4, 3); + @llquants = llquantize(81, 3, 1, 4, 3); + exit(0); +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + pid = Popen(["/bin/sleep", "1"]).wait() + dt.stop() + dt.agg_snap() + rows = dt.agg_walk() + +if not rows: + raise SystemExit("no rows returned") + +for row in rows: + if row["name"] == "counts": + v = int(row["value"]) + if v != 3: + raise SystemExit(f"unexpected count value {v}") + if row["name"] == "avgs": + v = float(row["value"]) + if v != 4.5: + raise SystemExit(f"unexpected avg value {v}") + if row["name"] == "sums": + v = int(row["value"]) + if v != 182: + raise SystemExit(f"unexpected sum value {v}") + if row["name"] == "mins": + v = int(row["value"]) + if v != 99: + raise SystemExit(f"unexpected min value {v}") + if row["name"] == "maxs": + v = int(row["value"]) + if v != 19: + raise SystemExit(f"unexpected max value {v}") + if row["name"] == "stddevs": + v = int(row["value"]) + if v != 287: + raise SystemExit(f"unexecpted stddev value {v}") + if row["name"] == "quants": + v = row["value"] + if v != {1:2, 2:2, 128:1}: + raise SystemExit(f"unexecpted quantize value {v}") + if row["name"] == "lquants": + v = row["value"] + if v != {1:3, 3:1, 9:2}: + raise SystemExit(f"unexecpted lquantize value {v}") + if row["name"] == "llquants": + v = row["value"] + if v != {0:2, 9:1, 18:1, 27:1, 162:1}: + raise SystemExit(f"unexecpted llquantize value {v}") + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.aggr-change.sh b/test/unittest/python/tst.aggr-change.sh new file mode 100755 index 00000000..318e17ba --- /dev/null +++ b/test/unittest/python/tst.aggr-change.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure aggregation values are updated across dt.work() calls. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession + +prog = r""" +syscall:::entry +{ + @counts["syscalls"] = count(); +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + pid = Popen(["/bin/sleep", "1"]).wait() + dt.work() + dt.agg_snap() + rows = dt.agg_walk() + if not rows: + raise SystemExit("no rows returned") + entry = rows[0] + val1 = int(entry["value"]) + pid = Popen(["/bin/sleep", "1"]).wait() + dt.work() + dt.agg_snap() + rows = dt.agg_walk() + if not rows: + raise SystemExit("no rows returned") + entry = rows[0] + val2 = int(entry["value"]) + if val2 <= val1: + raise SystemExit("aggr value unchanged") + dt.stop() + + rows = dt.agg_walk() + +if not rows: + raise SystemExit("no rows returned") + +entry = rows[0] +if "keys" not in entry or "value" not in entry: + raise SystemExit("missing keys or value") + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.aggr-stack.sh b/test/unittest/python/tst.aggr-stack.sh new file mode 100755 index 00000000..a9259636 --- /dev/null +++ b/test/unittest/python/tst.aggr-stack.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure aggregation can retrieve kernel stack key. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession + +prog = r""" +syscall:::entry +{ + @counts[stack()] = count(); +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + pid = Popen(["/bin/sleep", "1"]).wait() + dt.stop() + dt.agg_snap() + rows = dt.agg_walk() + +if not rows: + raise SystemExit("no rows returned") + +entry = rows[0] +if "keys" not in entry or "value" not in entry: + raise SystemExit("missing keys or value") + +if len(entry["keys"][0]) < 2: + raise SystemExit("missing stack list") + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.aggr.sh b/test/unittest/python/tst.aggr.sh new file mode 100755 index 00000000..15c46f0c --- /dev/null +++ b/test/unittest/python/tst.aggr.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure aggregation can be retrieved from simple D script. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession + +prog = r""" +syscall:::entry +{ + @counts[execname, probefunc] = count(); +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + pid = Popen(["/bin/sleep", "1"]).wait() + dt.stop() + dt.agg_snap() + rows = dt.agg_walk() + +if not rows: + raise SystemExit("no rows returned") + +entry = rows[0] +if "keys" not in entry or "value" not in entry: + raise SystemExit("missing keys or value") + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.exit.sh b/test/unittest/python/tst.exit.sh new file mode 100755 index 00000000..def6b0ec --- /dev/null +++ b/test/unittest/python/tst.exit.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure we see exit() from script with dt.status(). + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession, DTRACE_STATUS_EXITED + +prog = r""" +syscall:::entry +{ + c++; +} + +syscall:::entry +/ c > 1024 / +{ + exit(0); +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + exited = False + iters = 0 + while iters < 10: + pid = Popen(["/bin/sleep", "1"]).wait() + if dt.status() == DTRACE_STATUS_EXITED: + exited = True + iters += 1 + dt.stop() + +if exited != True: + raise SystemExit("missing DTRACE_STATUS_EXIT") + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.multi-session.sh b/test/unittest/python/tst.multi-session.sh new file mode 100755 index 00000000..7389c126 --- /dev/null +++ b/test/unittest/python/tst.multi-session.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure multiple sessions within same process image work. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession + +prog = r""" +syscall::read:entry +{ + @counts["syscalls"] = count(); +} +""" + +dts = {} +max_sessions = 10 + +try: + for i in range(max_sessions): + dts[i] = DTraceSession() + p = dts[i].compile(prog) + dts[i].enable(p) + dts[i].go() + dts[i].work() +except Exception as exc: + raise SystemExit(f"got err with {i} multiple sessions: {exc}") +finally: + for j in range(i): + dts[i].close() + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.proc-create.sh b/test/unittest/python/tst.proc-create.sh new file mode 100755 index 00000000..e9d2d466 --- /dev/null +++ b/test/unittest/python/tst.proc-create.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Validate tracing of a spawned command. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.py <<'EOF' +#!/usr/bin/env python3 +from subprocess import Popen +from dtrace import DTraceSession + +prog = r""" +syscall::read:entry +{ + @calls[execname] = count(); +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + proc = dt.proc_create(["/bin/echo", "hello"]) + dt.proc_continue(proc) + dt.proc_release(proc) + dt.stop() + dt.agg_snap() + rows = dt.agg_walk() + +if not rows: + raise SystemExit("no aggregation rows") + +names = {tuple(entry["keys"]) for entry in rows} +if not any("echo" in key for key in names): + raise SystemExit("expected echo entry in aggregation") +EOF + +chmod +x "$tmpdir/script.py" + +$tmpdir/script.py diff --git a/test/unittest/python/tst.proc-exit.sh b/test/unittest/python/tst.proc-exit.sh new file mode 100755 index 00000000..d4962f37 --- /dev/null +++ b/test/unittest/python/tst.proc-exit.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure we see stopped state when traced process goes away. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +cat >"$tmpdir"/script.d <<EOF +#!/usr/bin/env python3 +from pathlib import Path +from subprocess import Popen + +from dtrace import DTraceSession, DTRACE_STATUS_STOPPED + +prog = r""" +syscall:::entry +{ + c++; +} +""" + +with DTraceSession() as dt: + p = dt.compile(prog) + dt.enable(p) + dt.go() + proc = dt.proc_create(["/bin/sleep", "5"]) + dt.proc_continue(proc) + Popen(["/bin/sleep", "10"]).wait() + dt.work() + status = dt.status() + dt.proc_release(proc) + dt.stop() + +if status != DTRACE_STATUS_STOPPED: + raise SystemExit(f"missing DTRACE_STATUS_STOPPED: got {status}") + +EOF + +chmod +x "$tmpdir/script.d" +PATH="$tmpdir:$PATH" + +$tmpdir/script.d diff --git a/test/unittest/python/tst.proc-grab.sh b/test/unittest/python/tst.proc-grab.sh new file mode 100755 index 00000000..21faca65 --- /dev/null +++ b/test/unittest/python/tst.proc-grab.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# +# Oracle Linux DTrace. +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# +# Ensure Python bindings can grab an existing process and trace its syscalls. + +if [ $# -ne 1 ]; then + echo "usage: $0 <dtrace>" >&2 + exit 2 +fi + +dtrace=$1 + +tmpdir=$(mktemp -d) +cleanup() { + rm -rf "$tmpdir" +} +trap cleanup EXIT + +# Start a long-lived helper shell to grab. +sleep_inf="$tmpdir/sleep.sh" +cat >"$sleep_inf" <<'EOF' +#!/bin/bash +while :; do + sleep 1 +done +EOF +chmod +x "$sleep_inf" +"$sleep_inf" & +helper=$! + +cat >"$tmpdir"/script.py <<'EOF' +#!/usr/bin/env python3 +import os +import sys +from subprocess import Popen +from dtrace import DTraceSession + +prog = r""" +syscall:::entry +/ pid == $target / +{ + @counts[execname, probefunc] = count(); +} +""" + +pid = int(os.environ["TARGET_PID"]) + +with DTraceSession() as dt: + proc = dt.proc_grab_pid(pid) + if proc.getpid() != pid: + raise SystemExit("grabbed PID mismatch") + p = dt.compile(prog) + dt.enable(p) + dt.go() + dt.proc_continue(proc) + Popen(["/bin/sleep", "5"]).wait() + dt.stop() + dt.proc_release(proc) + dt.agg_snap() + rows = dt.agg_walk() + +if not rows: + raise SystemExit("no rows returned") + +entry = rows[0] +if "keys" not in entry or "value" not in entry: + raise SystemExit("missing keys or value") +EOF + +chmod +x "$tmpdir/script.py" +export TARGET_PID=$helper +$tmpdir/script.py +ret=$? + +kill $helper +wait $helper 2>/dev/null || true +exit $ret -- 2.43.5