[DTrace-devel] [PATCH v5 08/11] test: add tests for python bindings
Alan Maguire
alan.maguire at oracle.com
Fri Aug 21 16:10:56 UTC 2026
Add tests covering aggregation snapshot, handling of keys,
values. 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, and that closing a handle does not effect operation of
another handle in the same process. Test compile-time and
general setopt() options.
Signed-off-by: Alan Maguire <alan.maguire at oracle.com>
---
test/unittest/python/test.x | 17 ++
test/unittest/python/tst.aggr-actions.sh | 152 ++++++++++++++++++
test/unittest/python/tst.aggr-change.sh | 74 +++++++++
test/unittest/python/tst.aggr-clear.sh | 44 +++++
test/unittest/python/tst.aggr-keys.sh | 89 ++++++++++
test/unittest/python/tst.aggr-stack.sh | 62 +++++++
test/unittest/python/tst.aggr.sh | 58 +++++++
test/unittest/python/tst.compile-opts.sh | 50 ++++++
test/unittest/python/tst.exit.sh | 67 ++++++++
test/unittest/python/tst.multi-session-end.sh | 89 ++++++++++
test/unittest/python/tst.multi-session.sh | 62 +++++++
test/unittest/python/tst.proc-create.sh | 71 ++++++++
test/unittest/python/tst.proc-exit.sh | 57 +++++++
test/unittest/python/tst.proc-grab.sh | 84 ++++++++++
test/unittest/python/tst.setopt.sh | 30 ++++
test/unittest/python/tst.work-error.sh | 36 +++++
16 files changed, 1042 insertions(+)
create mode 100755 test/unittest/python/test.x
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-clear.sh
create mode 100755 test/unittest/python/tst.aggr-keys.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.compile-opts.sh
create mode 100755 test/unittest/python/tst.exit.sh
create mode 100755 test/unittest/python/tst.multi-session-end.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
create mode 100755 test/unittest/python/tst.setopt.sh
create mode 100755 test/unittest/python/tst.work-error.sh
diff --git a/test/unittest/python/test.x b/test/unittest/python/test.x
new file mode 100755
index 00000000..36ea27d6
--- /dev/null
+++ b/test/unittest/python/test.x
@@ -0,0 +1,17 @@
+#!/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.
+#
+# Skip the Python binding tests when the bindings were not built or installed.
+#
+
+if ! "$DTRACE_PYTHON" -c 'import dtrace' >/dev/null 2>&1; then
+ echo "Python bindings are unavailable"
+ exit 2
+fi
+
+exit 0
diff --git a/test/unittest/python/tst.aggr-actions.sh b/test/unittest/python/tst.aggr-actions.sh
new file mode 100755
index 00000000..f09d3b13
--- /dev/null
+++ b/test/unittest/python/tst.aggr-actions.sh
@@ -0,0 +1,152 @@
+#!/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()
+
+expected = {
+ "counts", "sums", "avgs", "mins", "maxs", "stddevs", "quants",
+ "lquants", "llquants",
+}
+seen = set()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+for row in rows:
+ if row["name"] == "counts":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 3:
+ raise SystemExit(f"unexpected count value {v}")
+ if row["name"] == "avgs":
+ seen.add(row["name"])
+ v = float(row["value"])
+ if v != 4.5:
+ raise SystemExit(f"unexpected avg value {v}")
+ if row["name"] == "sums":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 182:
+ raise SystemExit(f"unexpected sum value {v}")
+ if row["name"] == "mins":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 99:
+ raise SystemExit(f"unexpected min value {v}")
+ if row["name"] == "maxs":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 19:
+ raise SystemExit(f"unexpected max value {v}")
+ if row["name"] == "stddevs":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 287:
+ raise SystemExit(f"unexecpted stddev value {v}")
+ if row["name"] == "quants":
+ seen.add(row["name"])
+ v = row["value"]
+ if v != {1:2, 2:2, 128:1}:
+ raise SystemExit(f"unexecpted quantize value {v}")
+ if row["name"] == "lquants":
+ seen.add(row["name"])
+ v = row["value"]
+ if v != {1:3, 3:1, 9:2}:
+ raise SystemExit(f"unexecpted lquantize value {v}")
+ if row["name"] == "llquants":
+ seen.add(row["name"])
+ v = row["value"]
+ if v != {0:2, 9:1, 18:1, 27:1, 162:1}:
+ raise SystemExit(f"unexecpted llquantize value {v}")
+
+missing = expected - seen
+if missing:
+ raise SystemExit(f"missing aggregation(s): {sorted(missing)}")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$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..7c3f8594
--- /dev/null
+++ b/test/unittest/python/tst.aggr-change.sh
@@ -0,0 +1,74 @@
+#!/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"
+
+"$DTRACE_PYTHON" "$tmpdir/script.d"
diff --git a/test/unittest/python/tst.aggr-clear.sh b/test/unittest/python/tst.aggr-clear.sh
new file mode 100755
index 00000000..912bab0c
--- /dev/null
+++ b/test/unittest/python/tst.aggr-clear.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Ensure aggregation buffers can be cleared through the Python binding.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+program = r'''
+BEGIN
+{
+ @counts = count();
+}
+'''
+
+with DTraceSession() as session:
+ compiled = session.compile(program)
+ session.enable(compiled)
+ session.go()
+ session.stop()
+ session.agg_snap()
+ if not session.agg_walk():
+ raise SystemExit("aggregation was not populated")
+ session.agg_clear()
+ session.agg_snap()
+ rows = session.agg_walk()
+ if not rows:
+ raise SystemExit("aggregation descriptor disappeared after agg_clear()")
+ for row in rows:
+ if row["name"] == "counts" and int(row["value"]) != 0:
+ raise SystemExit(f"aggregation was not cleared: {row}")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.aggr-keys.sh b/test/unittest/python/tst.aggr-keys.sh
new file mode 100755
index 00000000..e81b1c2d
--- /dev/null
+++ b/test/unittest/python/tst.aggr-keys.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+# Verify Python aggregation-key decoding for zero, string, numeric, and
+# mixed key tuples.
+#
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+trap 'rm -rf "$tmpdir"' EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+program = r'''
+BEGIN
+{
+ @zero = count();
+ @string["alpha"] = count();
+ @numeric[17] = count();
+ @mixed["irq", 17] = count();
+}
+
+fbt::schedule:entry
+{
+ @symkey[sym(caller)] = count();
+ @modkey[mod(caller)] = count();
+ @stackkey[stack()] = count();
+}
+
+pid$target::getopt*:entry
+{
+ @usymkey[usym(ucaller)] = count();
+ @umodkey[umod(ucaller)] = count();
+}
+
+profile:::tick-5s
+{
+ exit(0);
+}
+'''
+
+with DTraceSession() as session:
+ from dtrace import DTRACE_C_ZDEFS
+ target = session.proc_create(["/bin/sleep", "2"])
+ program_obj = session.compile(program, cflags=DTRACE_C_ZDEFS)
+ session.enable(program_obj)
+ session.go()
+ session.proc_continue(target)
+ import time
+ time.sleep(2)
+ session.proc_release(target)
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+def hashable_key(key):
+ return tuple(hashable_key(item) for item in key) if isinstance(key, list) else key
+
+seen = {
+ (row["name"], tuple(hashable_key(key) for key in row["keys"]))
+ for row in rows
+}
+expected = {
+ ("zero", ()),
+ ("string", (("alpha",),)),
+ ("numeric", ((17,),)),
+ ("mixed", (("irq",), (17,))),
+}
+missing = expected - seen
+if missing:
+ raise SystemExit(f"missing aggregation keys: {missing}; rows={rows}")
+print(f"rows={rows}")
+
+for name in ("symkey", "modkey", "usymkey", "umodkey"):
+ matching = [row for row in rows if row["name"] == name]
+ if not matching or not isinstance(matching[0]["keys"][0], list):
+ raise SystemExit(f"symbolic key was not formatted: {name}: {matching}")
+stack_rows = [row for row in rows if row["name"] == "stackkey"]
+if not stack_rows or not isinstance(stack_rows[0]["keys"][0], list):
+ raise SystemExit(f"stack key was not formatted: {stack_rows}")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.aggr-stack.sh b/test/unittest/python/tst.aggr-stack.sh
new file mode 100755
index 00000000..1b398a04
--- /dev/null
+++ b/test/unittest/python/tst.aggr-stack.sh
@@ -0,0 +1,62 @@
+#!/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 not entry["keys"] or not isinstance(entry["keys"][0], list) or \
+ not entry["keys"][0] or len(entry["keys"][0][0]) < 2:
+ raise SystemExit("missing stack list")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$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..235de73d
--- /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"
+
+"$DTRACE_PYTHON" "$tmpdir/script.d"
diff --git a/test/unittest/python/tst.compile-opts.sh b/test/unittest/python/tst.compile-opts.sh
new file mode 100755
index 00000000..7e0c3e61
--- /dev/null
+++ b/test/unittest/python/tst.compile-opts.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# Ensure compile flags and preprocessor macro arguments are accepted.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTRACE_C_ZDEFS, DTraceSession
+
+program = r'''
+missing-provider:::missing-probe
+{
+ @missing = count();
+}
+
+BEGIN
+{
+ @value = sum(TEST_VALUE);
+}
+'''
+
+with DTraceSession() as session:
+ compiled = session.compile(
+ program,
+ cflags=DTRACE_C_ZDEFS,
+ defines=["TEST_VALUE=42"],
+ )
+ session.enable(compiled)
+ session.go()
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+values = {row["name"]: row["value"] for row in rows}
+if int(values.get("value", -1)) != 42:
+ raise SystemExit(f"macro definition was not applied: {values}")
+if "missing" in values:
+ raise SystemExit("missing probe unexpectedly generated an aggregation")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.exit.sh b/test/unittest/python/tst.exit.sh
new file mode 100755
index 00000000..0d5dfb42
--- /dev/null
+++ b/test/unittest/python/tst.exit.sh
@@ -0,0 +1,67 @@
+#!/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, DTRACE_STATUS_STOPPED
+
+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
+ break
+ iters += 1
+ if not exited:
+ raise SystemExit("missing DTRACE_STATUS_EXIT")
+ work_status, _ = dt.work()
+ if work_status != DTRACE_STATUS_STOPPED:
+ raise SystemExit(f"exit() was not drained by work(): got {work_status}")
+ dt.stop()
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$tmpdir/script.d"
diff --git a/test/unittest/python/tst.multi-session-end.sh b/test/unittest/python/tst.multi-session-end.sh
new file mode 100755
index 00000000..cf3430c2
--- /dev/null
+++ b/test/unittest/python/tst.multi-session-end.sh
@@ -0,0 +1,89 @@
+#!/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 END probes remain attached when another DTrace session in the same
+# process closes.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir/script.d" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+
+program = r'''
+END
+{
+ @ended = count();
+}
+'''
+
+
+def start_session():
+ session = DTraceSession()
+ program_handle = session.compile(program)
+ session.enable(program_handle)
+ session.go()
+ return session
+
+
+def assert_end_fired(session, survivor):
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+ for row in rows:
+ if row["name"] == "ended" and int(row["value"]) == 1:
+ return
+
+ raise SystemExit(f"END probe did not fire for surviving handle {survivor}")
+
+
+def close_session(session):
+ if session is not None:
+ try:
+ session.close()
+ except Exception:
+ pass
+
+
+# Closing A must not remove B's shared END uprobe.
+handle_a = handle_b = None
+try:
+ handle_a = start_session()
+ handle_b = start_session()
+ handle_a.close()
+ handle_a = None
+ assert_end_fired(handle_b, "B")
+finally:
+ close_session(handle_a)
+ close_session(handle_b)
+
+# Closing B must not remove A's shared END uprobe.
+handle_a = handle_b = None
+try:
+ handle_a = start_session()
+ handle_b = start_session()
+ handle_b.close()
+ handle_b = None
+ assert_end_fired(handle_a, "A")
+finally:
+ close_session(handle_a)
+ close_session(handle_b)
+EOF
+
+chmod +x "$tmpdir/script.d"
+"$DTRACE_PYTHON" "$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..3709ca1a
--- /dev/null
+++ b/test/unittest/python/tst.multi-session.sh
@@ -0,0 +1,62 @@
+#!/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();
+}
+"""
+
+sessions = []
+max_sessions = 10
+
+try:
+ for i in range(max_sessions):
+ session = DTraceSession()
+ sessions.append(session)
+ p = session.compile(prog)
+ session.enable(p)
+ session.go()
+ session.work()
+except Exception as exc:
+ raise SystemExit(f"got err with {i} multiple sessions: {exc}")
+finally:
+ for session in sessions:
+ try:
+ session.close()
+ except Exception:
+ pass
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$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..95e9db85
--- /dev/null
+++ b/test/unittest/python/tst.proc-create.sh
@@ -0,0 +1,71 @@
+#!/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
+import time
+
+from dtrace import DTRACE_STATUS_STOPPED, 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)
+ deadline = time.monotonic() + 5
+ try:
+ while time.monotonic() < deadline:
+ status, _ = dt.work()
+ if status == DTRACE_STATUS_STOPPED:
+ break
+ time.sleep(0.05)
+ else:
+ raise SystemExit("created process did not complete")
+ finally:
+ dt.proc_release(proc)
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no aggregation rows")
+
+def flatten(key):
+ if isinstance(key, list):
+ return [item for child in key for item in flatten(child)]
+ return [key]
+
+names = [flatten(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"
+
+"$DTRACE_PYTHON" "$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..2c7d02da
--- /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"
+
+"$DTRACE_PYTHON" "$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..5655303b
--- /dev/null
+++ b/test/unittest/python/tst.proc-grab.sh
@@ -0,0 +1,84 @@
+#!/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
+helper=
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ if [ -n "$helper" ]; then
+ kill "$helper" 2>/dev/null || true
+ wait "$helper" 2>/dev/null || true
+ fi
+ 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
+"$DTRACE_PYTHON" "$tmpdir/script.py"
+ret=$?
+
+exit $ret
diff --git a/test/unittest/python/tst.setopt.sh b/test/unittest/python/tst.setopt.sh
new file mode 100755
index 00000000..1efc8c06
--- /dev/null
+++ b/test/unittest/python/tst.setopt.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+#
+# Ensure valid and invalid options are handled through the Python binding.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceError, DTraceSession
+
+with DTraceSession() as session:
+ session.setopt("bufsize", "4m")
+ session.setopt("quiet", None)
+ try:
+ session.setopt("not-a-dtrace-option", "1")
+ except DTraceError:
+ pass
+ else:
+ raise SystemExit("invalid option was accepted")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.work-error.sh b/test/unittest/python/tst.work-error.sh
new file mode 100755
index 00000000..abf6e589
--- /dev/null
+++ b/test/unittest/python/tst.work-error.sh
@@ -0,0 +1,36 @@
+#!/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 libdtrace errors from work() are reported as Python exceptions.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceError, DTraceSession
+
+
+with DTraceSession() as session:
+ try:
+ session.work()
+ except DTraceError:
+ pass
+ else:
+ raise SystemExit("work() did not report its libdtrace error")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
--
2.43.5
More information about the DTrace-devel
mailing list