[Lldb-commits] [lldb] r323636 - [lldb] Generic base for testing gdb-remote behavior

Pavel Labath via lldb-commits lldb-commits at lists.llvm.org
Mon Jan 29 02:02:40 PST 2018


Author: labath
Date: Mon Jan 29 02:02:40 2018
New Revision: 323636

URL: http://llvm.org/viewvc/llvm-project?rev=323636&view=rev
Log:
[lldb] Generic base for testing gdb-remote behavior

Summary:
Adds new utilities that make it easier to write test cases for lldb acting as a client over a gdb-remote connection.

- A GDBRemoteTestBase class that starts a mock GDB server and provides an easy way to check client packets
- A MockGDBServer that, via MockGDBServerResponder, can be made to issue server responses that test client behavior.
- Utility functions for handling common data encoding/decoding
- Utility functions for creating dummy targets from YAML files

----

Split from the review at https://reviews.llvm.org/D42145, which was a new feature that necessitated the new testing capabilities.

Reviewers: clayborg, labath

Reviewed By: clayborg, labath

Subscribers: hintonda, davide, jingham, krytarowski, mgorny, lldb-commits

Differential Revision: https://reviews.llvm.org/D42195
Patch by Owen Shaw <llvm at owenpshaw.net>

Added:
    lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/
    lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py
    lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml
    lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py
Modified:
    lldb/trunk/packages/Python/lldbsuite/test/lldbtest.py
    lldb/trunk/test/CMakeLists.txt

Added: lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py?rev=323636&view=auto
==============================================================================
--- lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py (added)
+++ lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/TestGDBRemoteClient.py Mon Jan 29 02:02:40 2018
@@ -0,0 +1,13 @@
+import lldb
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test.decorators import *
+from gdbclientutils import *
+
+
+class TestGDBRemoteClient(GDBRemoteTestBase):
+
+    def test_connect(self):
+        """Test connecting to a remote gdb server"""
+        target = self.createTarget("a.yaml")
+        process = self.connect(target)
+        self.assertPacketLogContains(["qProcessInfo", "qfThreadInfo"])

Added: lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml?rev=323636&view=auto
==============================================================================
--- lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml (added)
+++ lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/a.yaml Mon Jan 29 02:02:40 2018
@@ -0,0 +1,34 @@
+!ELF
+FileHeader:
+  Class:           ELFCLASS32
+  Data:            ELFDATA2LSB
+  Type:            ET_EXEC
+  Machine:         EM_ARM
+Sections:
+  - Name:            .text
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC, SHF_EXECINSTR ]
+    Address:         0x1000
+    AddressAlign:    0x4
+    Content:         "c3c3c3c3"
+  - Name:            .data
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC ]
+    Address:         0x2000
+    AddressAlign:    0x4
+    Content:         "3232"
+ProgramHeaders:
+  - Type: PT_LOAD
+    Flags: [ PF_X, PF_R ]
+    VAddr: 0x1000
+    PAddr: 0x1000
+    Align: 0x4
+    Sections:
+      - Section: .text
+  - Type: PT_LOAD
+    Flags: [ PF_R, PF_W ]
+    VAddr: 0x2000
+    PAddr: 0x1004
+    Align: 0x4
+    Sections:
+      - Section: .data

Added: lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py?rev=323636&view=auto
==============================================================================
--- lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py (added)
+++ lldb/trunk/packages/Python/lldbsuite/test/functionalities/gdb_remote_client/gdbclientutils.py Mon Jan 29 02:02:40 2018
@@ -0,0 +1,442 @@
+import os
+import os.path
+import subprocess
+import threading
+import socket
+import lldb
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbtest_config
+
+
+def checksum(message):
+    """
+    Calculate the GDB server protocol checksum of the message.
+
+    The GDB server protocol uses a simple modulo 256 sum.
+    """
+    check = 0
+    for c in message:
+        check += ord(c)
+    return check % 256
+
+
+def frame_packet(message):
+    """
+    Create a framed packet that's ready to send over the GDB connection
+    channel.
+
+    Framing includes surrounding the message between $ and #, and appending
+    a two character hex checksum.
+    """
+    return "$%s#%02x" % (message, checksum(message))
+
+
+def escape_binary(message):
+    """
+    Escape the binary message using the process described in the GDB server
+    protocol documentation.
+
+    Most bytes are sent through as-is, but $, #, and { are escaped by writing
+    a { followed by the original byte mod 0x20.
+    """
+    out = ""
+    for c in message:
+        d = ord(c)
+        if d in (0x23, 0x24, 0x7d):
+            out += chr(0x7d)
+            out += chr(d ^ 0x20)
+        else:
+            out += c
+    return out
+
+
+def hex_encode_bytes(message):
+    """
+    Encode the binary message by converting each byte into a two-character
+    hex string.
+    """
+    out = ""
+    for c in message:
+        out += "%02x" % ord(c)
+    return out
+
+
+def hex_decode_bytes(hex_bytes):
+    """
+    Decode the hex string into a binary message by converting each two-character
+    hex string into a single output byte.
+    """
+    out = ""
+    hex_len = len(hex_bytes)
+    while i < hex_len - 1:
+        out += chr(int(hex_bytes[i:i + 2]), 16)
+        i += 2
+    return out
+
+
+class MockGDBServerResponder:
+    """
+    A base class for handing client packets and issuing server responses for
+    GDB tests.
+
+    This handles many typical situations, while still allowing subclasses to
+    completely customize their responses.
+
+    Most subclasses will be interested in overriding the other() method, which
+    handles any packet not recognized in the common packet handling code.
+    """
+
+    registerCount = 40
+    packetLog = None
+
+    def __init__(self):
+        self.packetLog = []
+
+    def respond(self, packet):
+        """
+        Return the unframed packet data that the server should issue in response
+        to the given packet received from the client.
+        """
+        self.packetLog.append(packet)
+        if packet == "g":
+            return self.readRegisters()
+        if packet[0] == "G":
+            return self.writeRegisters(packet[1:])
+        if packet[0] == "p":
+            return self.readRegister(int(packet[1:], 16))
+        if packet[0] == "P":
+            register, value = packet[1:].split("=")
+            return self.readRegister(int(register, 16), value)
+        if packet[0] == "m":
+            addr, length = [int(x, 16) for x in packet[1:].split(',')]
+            return self.readMemory(addr, length)
+        if packet[0] == "M":
+            location, encoded_data = packet[1:].split(":")
+            addr, length = [int(x, 16) for x in location.split(',')]
+            return self.writeMemory(addr, encoded_data)
+        if packet[0:7] == "qSymbol":
+            return self.qSymbol(packet[8:])
+        if packet[0:10] == "qSupported":
+            return self.qSupported(packet[11:].split(";"))
+        if packet == "qfThreadInfo":
+            return self.qfThreadInfo()
+        if packet == "qC":
+            return self.qC()
+        if packet == "?":
+            return self.haltReason()
+        if packet[0] == "H":
+            return self.selectThread(packet[1], int(packet[2:], 16))
+        if packet[0:6] == "qXfer:":
+            obj, read, annex, location = packet[6:].split(":")
+            offset, length = [int(x, 16) for x in location.split(',')]
+            data, has_more = self.qXferRead(obj, annex, offset, length)
+            if data is not None:
+                return self._qXferResponse(data, has_more)
+            return ""
+        return self.other(packet)
+
+    def readRegisters(self):
+        return "00000000" * self.registerCount
+
+    def readRegister(self, register):
+        return "00000000"
+
+    def writeRegisters(self, registers_hex):
+        return "OK"
+
+    def writeRegister(self, register, value_hex):
+        return "OK"
+
+    def readMemory(self, addr, length):
+        return "00" * length
+
+    def writeMemory(self, addr, data_hex):
+        return "OK"
+
+    def qSymbol(self, symbol_args):
+        return "OK"
+
+    def qSupported(self, client_supported):
+        return "PacketSize=3fff;QStartNoAckMode+"
+
+    def qfThreadInfo(self):
+        return "l"
+
+    def qC(self):
+        return "QC0"
+
+    def haltReason(self):
+        # SIGINT is 2, return type is 2 digit hex string
+        return "S02"
+
+    def qXferRead(self, obj, annex, offset, length):
+        return None, False
+
+    def _qXferResponse(self, data, has_more):
+        return "%s%s" % ("m" if has_more else "l", escape_binary(data))
+
+    def selectThread(self, op, thread_id):
+        return "OK"
+
+    def other(self, packet):
+        # empty string means unsupported
+        return ""
+
+
+class MockGDBServer:
+    """
+    A simple TCP-based GDB server that can test client behavior by receiving
+    commands and issuing custom-tailored responses.
+
+    Responses are generated via the .responder property, which should be an
+    instance of a class based on MockGDBServerResponder.
+    """
+
+    responder = None
+    port = 0
+    _socket = None
+    _client = None
+    _thread = None
+    _receivedData = None
+    _receivedDataOffset = None
+    _shouldSendAck = True
+    _isExpectingAck = False
+
+    def __init__(self, port = 0):
+        self.responder = MockGDBServerResponder()
+        self.port = port
+        self._socket = socket.socket()
+
+    def start(self):
+        # Block until the socket is up, so self.port is available immediately.
+        # Then start a thread that waits for a client connection.
+        addr = ("127.0.0.1", self.port)
+        self._socket.bind(addr)
+        self.port = self._socket.getsockname()[1]
+        self._socket.listen(0)
+        self._thread = threading.Thread(target=self._run)
+        self._thread.start()
+
+    def stop(self):
+        self._socket.close()
+        self._thread.join()
+        self._thread = None
+
+    def _run(self):
+        # For testing purposes, we only need to worry about one client
+        # connecting just one time.
+        try:
+            # accept() is stubborn and won't fail even when the socket is
+            # shutdown, so we'll use a timeout
+            self._socket.settimeout(2.0)
+            client, client_addr = self._socket.accept()
+            self._client = client
+            # The connected client inherits its timeout from self._socket,
+            # but we'll use a blocking socket for the client
+            self._client.settimeout(None)
+        except:
+            return
+        self._shouldSendAck = True
+        self._isExpectingAck = False
+        self._receivedData = ""
+        self._receivedDataOffset = 0
+        data = None
+        while True:
+            try:
+                data = self._client.recv(4096)
+                if data is None or len(data) == 0:
+                    break
+            except Exception as e:
+                self._client.close()
+                break
+            self._receive(data)
+
+    def _receive(self, data):
+        """
+        Collects data, parses and responds to as many packets as exist.
+        Any leftover data is kept for parsing the next time around.
+        """
+        self._receivedData += data
+        try:
+            packet = self._parsePacket()
+            while packet is not None:
+                self._handlePacket(packet)
+                packet = self._parsePacket()
+        except self.InvalidPacketException:
+            self._client.close()
+
+    def _parsePacket(self):
+        """
+        Reads bytes from self._receivedData, returning:
+        - a packet's contents if a valid packet is found
+        - the PACKET_ACK unique object if we got an ack
+        - None if we only have a partial packet
+
+        Raises an InvalidPacketException if unexpected data is received
+        or if checksums fail.
+
+        Once a complete packet is found at the front of self._receivedData,
+        its data is removed form self._receivedData.
+        """
+        data = self._receivedData
+        i = self._receivedDataOffset
+        data_len = len(data)
+        if data_len == 0:
+            return None
+        if i == 0:
+            # If we're looking at the start of the received data, that means
+            # we're looking for the start of a new packet, denoted by a $.
+            # It's also possible we'll see an ACK here, denoted by a +
+            if data[0] == '+':
+                self._receivedData = data[1:]
+                return self.PACKET_ACK
+            if data[0] == '$':
+                i += 1
+            else:
+                raise self.InvalidPacketException(
+                        "Unexexpected leading byte: %s" % data[0])
+
+        # If we're looking beyond the start of the received data, then we're
+        # looking for the end of the packet content, denoted by a #.
+        # Note that we pick up searching from where we left off last time
+        while i < data_len and data[i] != '#':
+            i += 1
+
+        # If there isn't enough data left for a checksum, just remember where
+        # we left off so we can pick up there the next time around
+        if i > data_len - 3:
+            self._receivedDataOffset = i
+            return None
+
+        # If we have enough data remaining for the checksum, extract it and
+        # compare to the packet contents
+        packet = data[1:i]
+        i += 1
+        try:
+            check = int(data[i:i + 2], 16)
+        except ValueError:
+            raise self.InvalidPacketException("Checksum is not valid hex")
+        i += 2
+        if check != checksum(packet):
+            raise self.InvalidPacketException(
+                    "Checksum %02x does not match content %02x" %
+                    (check, checksum(packet)))
+        # remove parsed bytes from _receivedData and reset offset so parsing
+        # can start on the next packet the next time around
+        self._receivedData = data[i:]
+        self._receivedDataOffset = 0
+        return packet
+
+    def _handlePacket(self, packet):
+        if packet is self.PACKET_ACK:
+            # If we are expecting an ack, we'll just ignore it because there's
+            # nothing else we're supposed to do.
+            #
+            # However, if we aren't expecting an ack, it's likely the initial
+            # ack that lldb client sends, and observations of real servers
+            # suggest we're supposed to ack back.
+            if not self._isExpectingAck:
+                self._client.sendall('+')
+            return
+        response = ""
+        # We'll handle the ack stuff here since it's not something any of the
+        # tests will be concerned about, and it'll get turned off quicly anyway.
+        if self._shouldSendAck:
+            self._client.sendall('+')
+            self._isExpectingAck = True
+        if packet == "QStartNoAckMode":
+            self._shouldSendAck = False
+            response = "OK"
+        elif self.responder is not None:
+            # Delegate everything else to our responder
+            response = self.responder.respond(packet)
+        # Handle packet framing since we don't want to bother tests with it.
+        framed = frame_packet(response)
+        self._client.sendall(framed)
+
+    PACKET_ACK = object()
+
+    class InvalidPacketException(Exception):
+        pass
+
+
+class GDBRemoteTestBase(TestBase):
+    """
+    Base class for GDB client tests.
+
+    This class will setup and start a mock GDB server for the test to use.
+    It also provides assertPacketLogContains, which simplifies the checking
+    of packets sent by the client.
+    """
+
+    NO_DEBUG_INFO_TESTCASE = True
+    mydir = TestBase.compute_mydir(__file__)
+    server = None
+    temp_files = None
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.temp_files = []
+        self.server = MockGDBServer()
+        self.server.start()
+
+    def tearDown(self):
+        for temp_file in self.temp_files:
+            self.RemoveTempFile(temp_file)
+        # TestBase.tearDown will kill the process, but we need to kill it early
+        # so its client connection closes and we can stop the server before
+        # finally calling the base tearDown.
+        if self.process() is not None:
+            self.process().Kill()
+        self.server.stop()
+        self.temp_files = []
+        TestBase.tearDown(self)
+
+    def createTarget(self, yaml_path):
+        """
+        Create a target by auto-generating the object based on the given yaml
+        instructions.
+
+        This will track the generated object so it can be automatically removed
+        during tearDown.
+        """
+        yaml_base, ext = os.path.splitext(yaml_path)
+        obj_path = "%s" % yaml_base
+        self.yaml2obj(yaml_path, obj_path)
+        self.temp_files.append(obj_path)
+        return self.dbg.CreateTarget(obj_path)
+
+    def connect(self, target):
+        """
+        Create a process by connecting to the mock GDB server.
+
+        Includes assertions that the process was successfully created.
+        """
+        listener = self.dbg.GetListener()
+        error = lldb.SBError()
+        url = "connect://localhost:%d" % self.server.port
+        process = target.ConnectRemote(listener, url, "gdb-remote", error)
+        self.assertTrue(error.Success(), error.description)
+        self.assertTrue(process, PROCESS_IS_VALID)
+
+    def assertPacketLogContains(self, packets):
+        """
+        Assert that the mock server's packet log contains the given packets.
+
+        The packet log includes all packets sent by the client and received
+        by the server.  This fuction makes it easy to verify that the client
+        sent the expected packets to the server.
+
+        The check does not require that the packets be consecutive, but does
+        require that they are ordered in the log as they ordered in the arg.
+        """
+        i = 0
+        j = 0
+        log = self.server.responder.packetLog
+        while i < len(packets) and j < len(log):
+            if log[j] == packets[i]:
+                i += 1
+            j += 1
+        if i < len(packets):
+            self.fail("Did not receive: %s\nLast 10 packets:\n\t%s" %
+                    (packets[i], '\n\t'.join(log[-10:])))

Modified: lldb/trunk/packages/Python/lldbsuite/test/lldbtest.py
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/packages/Python/lldbsuite/test/lldbtest.py?rev=323636&r1=323635&r2=323636&view=diff
==============================================================================
--- lldb/trunk/packages/Python/lldbsuite/test/lldbtest.py (original)
+++ lldb/trunk/packages/Python/lldbsuite/test/lldbtest.py Mon Jan 29 02:02:40 2018
@@ -1589,10 +1589,10 @@ class Base(unittest2.TestCase):
     def findBuiltClang(self):
         """Tries to find and use Clang from the build directory as the compiler (instead of the system compiler)."""
         paths_to_try = [
-            "llvm-build/Release+Asserts/x86_64/Release+Asserts/bin/clang",
-            "llvm-build/Debug+Asserts/x86_64/Debug+Asserts/bin/clang",
-            "llvm-build/Release/x86_64/Release/bin/clang",
-            "llvm-build/Debug/x86_64/Debug/bin/clang",
+            "llvm-build/Release+Asserts/x86_64/bin/clang",
+            "llvm-build/Debug+Asserts/x86_64/bin/clang",
+            "llvm-build/Release/x86_64/bin/clang",
+            "llvm-build/Debug/x86_64/bin/clang",
         ]
         lldb_root_path = os.path.join(
             os.path.dirname(__file__), "..", "..", "..", "..")
@@ -1608,6 +1608,31 @@ class Base(unittest2.TestCase):
 
         return os.environ["CC"]
 
+    def findYaml2obj(self):
+        """
+        Get the path to the yaml2obj executable, which can be used to create
+        test object files from easy to write yaml instructions.
+
+        Throws an Exception if the executable cannot be found.
+        """
+        # Tries to find yaml2obj at the same folder as clang
+        clang_dir = os.path.dirname(self.findBuiltClang())
+        path = os.path.join(clang_dir, "yaml2obj")
+        if os.path.exists(path):
+            return path
+        raise Exception("yaml2obj executable not found")
+
+
+    def yaml2obj(self, yaml_path, obj_path):
+        """
+        Create an object file at the given path from a yaml file.
+
+        Throws subprocess.CalledProcessError if the object could not be created.
+        """
+        yaml2obj = self.findYaml2obj()
+        command = [yaml2obj, "-o=%s" % obj_path, yaml_path]
+        system([command])
+
     def getBuildFlags(
             self,
             use_cpp11=True,

Modified: lldb/trunk/test/CMakeLists.txt
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/test/CMakeLists.txt?rev=323636&r1=323635&r2=323636&view=diff
==============================================================================
--- lldb/trunk/test/CMakeLists.txt (original)
+++ lldb/trunk/test/CMakeLists.txt Mon Jan 29 02:02:40 2018
@@ -34,6 +34,10 @@ if(TARGET lldb-mi)
   list(APPEND LLDB_TEST_DEPS lldb-mi)
 endif()
 
+if(NOT LLDB_BUILT_STANDALONE)
+  list(APPEND LLDB_TEST_DEPS yaml2obj)
+endif()
+
 # The default architecture with which to compile test executables is the default LLVM target
 # architecture, which itself defaults to the host architecture.
 string(TOLOWER "${LLVM_TARGET_ARCH}" LLDB_DEFAULT_TEST_ARCH)




More information about the lldb-commits mailing list