[PATCH v5 08/11] test: add tests for python bindings
Alan Maguire <[email protected]>
| Newsgroups | dev.linux.lists.dtrace |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> --- 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