]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/python: restructure the existing memory leak tests
authorAndrew Burgess <aburgess@redhat.com>
Sat, 19 Apr 2025 13:41:17 +0000 (14:41 +0100)
committerAndrew Burgess <aburgess@redhat.com>
Tue, 22 Apr 2025 16:21:58 +0000 (17:21 +0100)
We currently have two memory leak tests in gdb.python/ and there's a
lot of duplication between these two.

In the next commit I'd like to add yet another memory leak test, which
would mean a third set of scripts which duplicate the existing two.
And three is where I draw the line.

This commit factors out the core of the memory leak tests into a new
module gdb_leak_detector.py, which can then be imported by each
tests's Python file in order to make writing the memory leak tests
easier.

I've also added a helper function to lib/gdb-python.exp which captures
some of the common steps needed in the TCL file in order to run a
memory leak test.

Finally, I use this new infrastructure to rewrite the two existing
memory leak tests.

What I considered, but ultimately didn't do, is merge the two memory
leak tests into a single TCL script.  I did consider this, and for the
existing tests this would be possible, but future tests might require
different enough setup that this might not be possible for all future
tests, and now that we have helper functions in a central location,
the each individual test is actually pretty small now, so leaving them
separate seemed OK.

There should be no change in what is actually being tested after this
commit.

Approved-By: Tom Tromey <tom@tromey.com>
gdb/testsuite/gdb.python/gdb_leak_detector.py [new file with mode: 0644]
gdb/testsuite/gdb.python/py-inferior-leak.exp
gdb/testsuite/gdb.python/py-inferior-leak.py
gdb/testsuite/gdb.python/py-read-memory-leak.exp
gdb/testsuite/gdb.python/py-read-memory-leak.py
gdb/testsuite/lib/gdb-python.exp

diff --git a/gdb/testsuite/gdb.python/gdb_leak_detector.py b/gdb/testsuite/gdb.python/gdb_leak_detector.py
new file mode 100644 (file)
index 0000000..b0f6d47
--- /dev/null
@@ -0,0 +1,120 @@
+# Copyright (C) 2021-2025 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Defines a base class, which can be sub-classed, in order to run
+# memory leak tests on some aspects of GDB's Python API.  See the
+# comments on the gdb_leak_detector class for more details.
+
+import os
+import tracemalloc
+import gdb
+
+
+# This class must be sub-classed to create a memory leak test.  The
+# sub-classes __init__ method should call the parent classes __init__
+# method, and the sub-class should override allocate() and
+# deallocate().  See the comments on the various methods below for
+# more details of required arguments and expected usage.
+class gdb_leak_detector:
+
+    # Class initialisation.  FILENAME is the file in which the
+    # sub-class is defined, usually passed as just '__file__'.  This
+    # is used when looking for memory allocations; only allocations in
+    # FILENAME are considered.
+    def __init__(self, filename):
+        self.filters = [tracemalloc.Filter(True, "*" + os.path.basename(filename))]
+
+    # Internal helper function to actually run the test.  Calls the
+    # allocate() method to allocate an object from GDB's Python API.
+    # When CLEAR is True the object will then be deallocated by
+    # calling deallocate(), otherwise, deallocate() is not called.
+    #
+    # Finally, this function checks for any memory allocatios
+    # originating from 'self.filename' that have not been freed, and
+    # returns the total (in bytes) of the memory that has been
+    # allocated, but not freed.
+    def _do_test(self, clear):
+        # Start tracing, and take a snapshot of the current allocations.
+        tracemalloc.start()
+        snapshot1 = tracemalloc.take_snapshot()
+
+        # Generate the GDB Python API object by calling the allocate
+        # method.
+        self.allocate()
+
+        # Possibly clear the reference to the allocated object.
+        if clear:
+            self.deallocate()
+
+        # Now grab a second snapshot of memory allocations, and stop
+        # tracing memory allocations.
+        snapshot2 = tracemalloc.take_snapshot()
+        tracemalloc.stop()
+
+        # Filter the snapshots; we only care about allocations originating
+        # from this file.
+        snapshot1 = snapshot1.filter_traces(self.filters)
+        snapshot2 = snapshot2.filter_traces(self.filters)
+
+        # Compare the snapshots, this leaves only things that were
+        # allocated, but not deallocated since the first snapshot.
+        stats = snapshot2.compare_to(snapshot1, "traceback")
+
+        # Total up all the allocated things.
+        total = 0
+        for stat in stats:
+            total += stat.size_diff
+        return total
+
+    # Run the memory leak test.  Prints 'PASS' if successful,
+    # otherwise, raises an exception (of type GdbError).
+    def run(self):
+        # The first time we run this some global state will be allocated which
+        # shows up as memory that is allocated, but not released.  So, run the
+        # test once and discard the result.
+        self._do_test(True)
+
+        # Now run the test twice, the first time we clear our global reference
+        # to the allocated object, which should allow Python to deallocate the
+        # object.  The second time we hold onto the global reference, preventing
+        # Python from performing the deallocation.
+        bytes_with_clear = self._do_test(True)
+        bytes_without_clear = self._do_test(False)
+
+        # If there are any allocations left over when we cleared the reference
+        # (and expected deallocation) then this indicates a leak.
+        if bytes_with_clear > 0:
+            raise gdb.GdbError("memory leak when object reference was released")
+
+        # If there are no allocations showing when we hold onto a reference,
+        # then this likely indicates that the testing infrastructure is broken,
+        # and we're no longer spotting the allocations at all.
+        if bytes_without_clear == 0:
+            raise gdb.GdbError("object is unexpectedly not showing as allocated")
+
+        # Print a PASS message that the TCL script can see.
+        print("PASS")
+
+    # Sub-classes must override this method.  Allocate an object (or
+    # multiple objects) from GDB's Python API.  Store references to
+    # these objects within SELF.
+    def allocate(self):
+        raise NotImplementedError("allocate() not implemented")
+
+    # Sub-classes must override this method.  Deallocate the object(s)
+    # allocated by the allocate() method.  All that is required is for
+    # the references created in allocate() to be set to None.
+    def deallocate(self):
+        raise NotImplementedError("allocate() not implemented")
index 6710f59e9e32d3959fb6e1a8b64af7e469f61d56..15216ee76b78aaeff5fa9151110d95de5cade808 100644 (file)
@@ -24,15 +24,5 @@ standard_testfile
 
 clean_restart
 
-# Skip this test if the tracemalloc module is not available.
-if { ![gdb_py_module_available "tracemalloc"] } {
-    unsupported "tracemalloc module not available"
-    return
-}
-
-set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
-
-# Source the Python script, this runs the test (which is written
-# completely in Python), and either prints PASS, or throws an
-# exception.
-gdb_test "source ${pyfile}" "PASS" "source python script"
+gdb_py_run_memory_leak_test ${srcdir}/${subdir}/${testfile}.py \
+    "gdb.Inferior object deallocates correctly"
index 97837dccb857140ce54e312b75ac519aa7596d23..38f33c3b78acff16f3059c06f17d33253b2fd09c 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import re
-import tracemalloc
+import gdb_leak_detector
 
-import gdb
 
-# A global variable in which we store a reference to the gdb.Inferior
-# object sent to us in the new_inferior event.
-inf = None
+class inferior_leak_detector(gdb_leak_detector.gdb_leak_detector):
+    def __init__(self):
+        super().__init__(__file__)
+        self.inferior = None
+        self.__handler = lambda event: setattr(self, "inferior", event.inferior)
+        gdb.events.new_inferior.connect(self.__handler)
 
+    def __del__(self):
+        gdb.events.new_inferior.disconnect(self.__handler)
 
-# Register the new_inferior event handler.
-def new_inferior_handler(event):
-    global inf
-    inf = event.inferior
+    def allocate(self):
+        output = gdb.execute("add-inferior", False, True)
+        m = re.search(r"Added inferior (\d+)", output)
+        if m:
+            num = int(m.group(1))
+        else:
+            raise RuntimeError("no match")
 
+        gdb.execute("remove-inferiors %s" % num)
 
-gdb.events.new_inferior.connect(new_inferior_handler)
+    def deallocate(self):
+        self.inferior = None
 
-# A global filters list, we only care about memory allocations
-# originating from this script.
-filters = [tracemalloc.Filter(True, "*py-inferior-leak.py")]
 
-
-# Add a new inferior, and return the number of the new inferior.
-def add_inferior():
-    output = gdb.execute("add-inferior", False, True)
-    m = re.search(r"Added inferior (\d+)", output)
-    if m:
-        num = int(m.group(1))
-    else:
-        raise RuntimeError("no match")
-    return num
-
-
-# Run the test.  When CLEAR is True we clear the global INF variable
-# before comparing the before and after memory allocation traces.
-# When CLEAR is False we leave INF set to reference the gdb.Inferior
-# object, thus preventing the gdb.Inferior from being deallocated.
-def test(clear):
-    global filters, inf
-
-    # Start tracing, and take a snapshot of the current allocations.
-    tracemalloc.start()
-    snapshot1 = tracemalloc.take_snapshot()
-
-    # Create an inferior, this triggers the new_inferior event, which
-    # in turn holds a reference to the new gdb.Inferior object in the
-    # global INF variable.
-    num = add_inferior()
-    gdb.execute("remove-inferiors %s" % num)
-
-    # Possibly clear the global INF variable.
-    if clear:
-        inf = None
-
-    # Now grab a second snapshot of memory allocations, and stop
-    # tracing memory allocations.
-    snapshot2 = tracemalloc.take_snapshot()
-    tracemalloc.stop()
-
-    # Filter the snapshots; we only care about allocations originating
-    # from this file.
-    snapshot1 = snapshot1.filter_traces(filters)
-    snapshot2 = snapshot2.filter_traces(filters)
-
-    # Compare the snapshots, this leaves only things that were
-    # allocated, but not deallocated since the first snapshot.
-    stats = snapshot2.compare_to(snapshot1, "traceback")
-
-    # Total up all the deallocated things.
-    total = 0
-    for stat in stats:
-        total += stat.size_diff
-    return total
-
-
-# The first time we run this some global state will be allocated which
-# shows up as memory that is allocated, but not released.  So, run the
-# test once and discard the result.
-test(True)
-
-# Now run the test twice, the first time we clear our global reference
-# to the gdb.Inferior object, which should allow Python to deallocate
-# the object.  The second time we hold onto the global reference,
-# preventing Python from performing the deallocation.
-bytes_with_clear = test(True)
-bytes_without_clear = test(False)
-
-# The bug that used to exist in GDB was that even when we released the
-# global reference the gdb.Inferior object would not be deallocated.
-if bytes_with_clear > 0:
-    raise gdb.GdbError("memory leak when gdb.Inferior should be released")
-if bytes_without_clear == 0:
-    raise gdb.GdbError("gdb.Inferior object is no longer allocated")
-
-# Print a PASS message that the test script can see.
-print("PASS")
+inferior_leak_detector().run()
index 0015a5732268627cb07b94f8f2dd06bf504a72ff..9ae5eb8a5ebd0d24c0b33bb63f8ea2e31fc35624 100644 (file)
@@ -30,15 +30,5 @@ if ![runto_main] {
    return -1
 }
 
-# Skip this test if the tracemalloc module is not available.
-if { ![gdb_py_module_available "tracemalloc"] } {
-    unsupported "tracemalloc module not available"
-    return
-}
-
-set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
-
-# Source the Python script, this runs the test (which is written
-# completely in Python), and either prints PASS, or throws an
-# exception.
-gdb_test "source ${pyfile}" "PASS" "source python script"
+gdb_py_run_memory_leak_test ${srcdir}/${subdir}/${testfile}.py \
+    "buffers returned by read_memory() deallocates correctly"
index 348403d8494625e62907a0a9eebd31652d26bb22..71edf47e7dcc658538ec7165f9b24619c77e0ec4 100644 (file)
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
-import tracemalloc
+import gdb_leak_detector
 
-import gdb
 
-# A global variable in which we store a reference to the memory buffer
-# returned from gdb.Inferior.read_memory().
-mem_buf = None
+class read_leak_detector(gdb_leak_detector.gdb_leak_detector):
+    def __init__(self):
+        super().__init__(__file__)
+        self.mem_buf = None
+        self.addr = gdb.parse_and_eval("px")
+        self.inf = gdb.inferiors()[0]
 
+    def allocate(self):
+        self.mem_buf = self.inf.read_memory(self.addr, 4096)
 
-# A global filters list, we only care about memory allocations
-# originating from this script.
-filters = [tracemalloc.Filter(True, "*" + os.path.basename(__file__))]
+    def deallocate(self):
+        self.mem_buf = None
 
 
-# Run the test.  When CLEAR is True we clear the global INF variable
-# before comparing the before and after memory allocation traces.
-# When CLEAR is False we leave INF set to reference the gdb.Inferior
-# object, thus preventing the gdb.Inferior from being deallocated.
-def test(clear):
-    global filters, mem_buf
-
-    addr = gdb.parse_and_eval("px")
-    inf = gdb.inferiors()[0]
-
-    # Start tracing, and take a snapshot of the current allocations.
-    tracemalloc.start()
-    snapshot1 = tracemalloc.take_snapshot()
-
-    # Read from the inferior, this allocate a memory buffer object.
-    mem_buf = inf.read_memory(addr, 4096)
-
-    # Possibly clear the global INF variable.
-    if clear:
-        mem_buf = None
-
-    # Now grab a second snapshot of memory allocations, and stop
-    # tracing memory allocations.
-    snapshot2 = tracemalloc.take_snapshot()
-    tracemalloc.stop()
-
-    # Filter the snapshots; we only care about allocations originating
-    # from this file.
-    snapshot1 = snapshot1.filter_traces(filters)
-    snapshot2 = snapshot2.filter_traces(filters)
-
-    # Compare the snapshots, this leaves only things that were
-    # allocated, but not deallocated since the first snapshot.
-    stats = snapshot2.compare_to(snapshot1, "traceback")
-
-    # Total up all the allocated things.
-    total = 0
-    for stat in stats:
-        total += stat.size_diff
-    return total
-
-
-# The first time we run this some global state will be allocated which
-# shows up as memory that is allocated, but not released.  So, run the
-# test once and discard the result.
-test(True)
-
-# Now run the test twice, the first time we clear our global reference
-# to the memory buffer object, which should allow Python to deallocate
-# the object.  The second time we hold onto the global reference,
-# preventing Python from performing the deallocation.
-bytes_with_clear = test(True)
-bytes_without_clear = test(False)
-
-# The bug that used to exist in GDB was that even when we released the
-# global reference the gdb.Inferior object would not be deallocated.
-if bytes_with_clear > 0:
-    raise gdb.GdbError("memory leak when memory buffer should be released")
-if bytes_without_clear == 0:
-    raise gdb.GdbError("memory buffer object is no longer allocated")
-
-# Print a PASS message that the test script can see.
-print("PASS")
+read_leak_detector().run()
index b4eb40dfcec4f36fcd49eeaf130525186521f0e2..e026c1bc857b2fb0a92a3d17a2bff58c1614d458 100644 (file)
@@ -77,3 +77,24 @@ proc gdb_py_module_available { name } {
 
     return ${available}
 }
+
+# Run a memory leak test within the Python script FILENAME.  This proc
+# checks that the required Python modules are available, sets up the
+# syspath so that the helper module can be found (in the same
+# directory as FILENAME), then loads FILENAME to run the test.
+proc gdb_py_run_memory_leak_test { filename testname } {
+    if { ![gdb_py_module_available "tracemalloc"] } {
+       unsupported "$testname (tracemalloc module not available)"
+    }
+
+    gdb_test_no_output -nopass "python import sys"
+    gdb_test_no_output -nopass \
+       "python sys.path.insert(0, \"[file dirname $filename]\")" \
+       "setup sys.path"
+
+    set pyfile [gdb_remote_download host ${filename}]
+
+    # Source the Python script, this runs the test, and either prints
+    # PASS, or throws an exception.
+    gdb_test "source ${pyfile}" "^PASS" $testname
+}