[Lldb-commits] [lldb] r209845 - gdb-remote testing: new test, cleaned up socket reading.

Todd Fiala todd.fiala at gmail.com
Thu May 29 13:44:45 PDT 2014


Author: tfiala
Date: Thu May 29 15:44:45 2014
New Revision: 209845

URL: http://llvm.org/viewvc/llvm-project?rev=209845&view=rev
Log:
gdb-remote testing: new test, cleaned up socket reading.

Added new SocketPacketPump class to decouple gdb remote packet
reading from packet expectations code.  This allowed for cleaner
implementation of the separate $O output streams (non-deterministic
packaging of inferior stdout/stderr) from all the rest of the packets.

Added a packet expectation matcher that can match expected accumulated
output with a timeout.  Use a dictionary with "type":"output_match".
See lldbgdbserverutils.MatchRemoteOutputEntry for details.

Added a gdb remote test to verify that $Hc (continue thread selection)
plus signal delivery ($C{signo}) works.  Having trouble getting this
to pass with debugserver on MacOSX 10.9.  Tried different variants,
including $vCont;C{signo}:{thread-id};c.  In some cases, I get the
test exe's signal handler to run ($vCont variant first time), in others I don't
($vCont second and further times).  $C{signo} doesn't hit the signal
handler code at all in the test exe but delivers a stop.  Further
$Hc and $C{signo} deliver the stop marking the wrong thread.  For now I'm
marking the test as XFAIL on dsym/debugserver.  Will revisit this on
lldb-dev.

Updated the text exe for these tests to support thread:print-ids (each
thread announces its thread id) and provide a SIGUSR1 thread handler
that prints out the thread id on which it was signaled.


Added:
    lldb/trunk/test/tools/lldb-gdbserver/socket_packet_pump.py
Modified:
    lldb/trunk/test/tools/lldb-gdbserver/Makefile
    lldb/trunk/test/tools/lldb-gdbserver/TestLldbGdbServer.py
    lldb/trunk/test/tools/lldb-gdbserver/lldbgdbserverutils.py
    lldb/trunk/test/tools/lldb-gdbserver/main.cpp

Modified: lldb/trunk/test/tools/lldb-gdbserver/Makefile
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/test/tools/lldb-gdbserver/Makefile?rev=209845&r1=209844&r2=209845&view=diff
==============================================================================
--- lldb/trunk/test/tools/lldb-gdbserver/Makefile (original)
+++ lldb/trunk/test/tools/lldb-gdbserver/Makefile Thu May 29 15:44:45 2014
@@ -1,6 +1,6 @@
 LEVEL = ../../make
 
-CFLAGS_EXTRAS := -D__STDC_LIMIT_MACROS
+CFLAGS_EXTRAS := -D__STDC_LIMIT_MACROS -D__STDC_FORMAT_MACROS
 LD_EXTRAS := -lpthread
 CXX_SOURCES := main.cpp
 MAKE_DSYM :=NO

Modified: lldb/trunk/test/tools/lldb-gdbserver/TestLldbGdbServer.py
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/test/tools/lldb-gdbserver/TestLldbGdbServer.py?rev=209845&r1=209844&r2=209845&view=diff
==============================================================================
--- lldb/trunk/test/tools/lldb-gdbserver/TestLldbGdbServer.py (original)
+++ lldb/trunk/test/tools/lldb-gdbserver/TestLldbGdbServer.py Thu May 29 15:44:45 2014
@@ -4,6 +4,8 @@ Test lldb-gdbserver operation
 
 import unittest2
 import pexpect
+import platform
+import signal
 import socket
 import subprocess
 import sys
@@ -38,6 +40,11 @@ class LldbGdbServerTestCase(TestBase):
         self.test_sequence = GdbRemoteTestSequence(self.logger)
         self.set_inferior_startup_launch()
 
+        # Uncomment this code to force only a single test to run (by name).
+        # if self._testMethodName != "test_Hc_then_Csignal_signals_correct_thread_launch_debugserver_dsym":
+        #     # print "skipping test {}".format(self._testMethodName)
+        #     self.skipTest("focusing on one test")
+
     def reset_test_sequence(self):
         self.test_sequence = GdbRemoteTestSequence(self.logger)
 
@@ -45,11 +52,13 @@ class LldbGdbServerTestCase(TestBase):
         self.debug_monitor_exe = get_lldb_gdbserver_exe()
         if not self.debug_monitor_exe:
             self.skipTest("lldb_gdbserver exe not found")
+        self.debug_monitor_extra_args = ""
 
     def init_debugserver_test(self):
         self.debug_monitor_exe = get_debugserver_exe()
         if not self.debug_monitor_exe:
             self.skipTest("debugserver exe not found")
+        self.debug_monitor_extra_args = " --log-file=/tmp/packets-{}.log --log-flags=0x800000".format(self._testMethodName)
 
     def create_socket(self):
         sock = socket.socket()
@@ -81,7 +90,7 @@ class LldbGdbServerTestCase(TestBase):
 
     def start_server(self, attach_pid=None):
         # Create the command line
-        commandline = "{} localhost:{}".format(self.debug_monitor_exe, self.port)
+        commandline = "{}{} localhost:{}".format(self.debug_monitor_exe, self.debug_monitor_extra_args, self.port)
         if attach_pid:
             commandline += " --attach=%d" % attach_pid
             
@@ -239,7 +248,6 @@ class LldbGdbServerTestCase(TestBase):
         self.assertTrue("encoding" in reg_info)
         self.assertTrue("format" in reg_info)
 
-
     def add_threadinfo_collection_packets(self):
         self.test_sequence.add_log_lines(
             [ { "type":"multi_response", "first_query":"qfThreadInfo", "next_query":"qsThreadInfo",
@@ -247,7 +255,6 @@ class LldbGdbServerTestCase(TestBase):
               "save_key":"threadinfo_responses" } ],
             True)
 
-
     def parse_threadinfo_packets(self, context):
         """Return an array of thread ids (decimal ints), one per thread."""
         threadinfo_responses = context.get("threadinfo_responses")
@@ -259,7 +266,6 @@ class LldbGdbServerTestCase(TestBase):
             thread_ids.extend(new_thread_infos)
         return thread_ids
 
-
     def wait_for_thread_count(self, thread_count, timeout_seconds=3):
         start_time = time.time()
         timeout_time = start_time + timeout_seconds
@@ -284,7 +290,6 @@ class LldbGdbServerTestCase(TestBase):
 
         return threads
 
-
     def run_process_then_stop(self, run_seconds=1):
         # Tell the stub to continue.
         self.test_sequence.add_log_lines(
@@ -304,7 +309,8 @@ class LldbGdbServerTestCase(TestBase):
         context = self.expect_gdbremote_sequence()
         self.assertIsNotNone(context)
         self.assertIsNotNone(context.get("stop_result"))
-
+        
+        return context
 
     @debugserver_test
     def test_exe_starts_debugserver(self):
@@ -806,7 +812,6 @@ class LldbGdbServerTestCase(TestBase):
         # Ensure we have a flags register.
         self.assertTrue('flags' in generic_regs)
 
-
     @debugserver_test
     @dsym_test
     def test_qRegisterInfo_contains_required_generics_debugserver_dsym(self):
@@ -822,7 +827,6 @@ class LldbGdbServerTestCase(TestBase):
         self.buildDwarf()
         self.qRegisterInfo_contains_required_generics()
 
-
     def qRegisterInfo_contains_at_least_one_register_set(self):
         server = self.start_server()
         self.assertIsNotNone(server)
@@ -846,7 +850,6 @@ class LldbGdbServerTestCase(TestBase):
         register_sets = { reg_info['set']:1 for reg_info in reg_infos if 'set' in reg_info }
         self.assertTrue(len(register_sets) >= 1)
 
-
     @debugserver_test
     @dsym_test
     def test_qRegisterInfo_contains_at_least_one_register_set_debugserver_dsym(self):
@@ -854,7 +857,6 @@ class LldbGdbServerTestCase(TestBase):
         self.buildDsym()
         self.qRegisterInfo_contains_at_least_one_register_set()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -863,7 +865,6 @@ class LldbGdbServerTestCase(TestBase):
         self.buildDwarf()
         self.qRegisterInfo_contains_at_least_one_register_set()
 
-
     def qThreadInfo_contains_thread(self):
         procs = self.prep_debug_monitor_and_inferior()
         self.add_threadinfo_collection_packets()
@@ -879,7 +880,6 @@ class LldbGdbServerTestCase(TestBase):
         # We should have exactly one thread.
         self.assertEqual(len(threads), 1)
 
-
     @debugserver_test
     @dsym_test
     def test_qThreadInfo_contains_thread_launch_debugserver_dsym(self):
@@ -888,7 +888,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.qThreadInfo_contains_thread()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -898,7 +897,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.qThreadInfo_contains_thread()
 
-
     @debugserver_test
     @dsym_test
     def test_qThreadInfo_contains_thread_attach_debugserver_dsym(self):
@@ -907,7 +905,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.qThreadInfo_contains_thread()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -917,7 +914,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.qThreadInfo_contains_thread()
 
-
     def qThreadInfo_matches_qC(self):
         procs = self.prep_debug_monitor_and_inferior()
 
@@ -946,7 +942,6 @@ class LldbGdbServerTestCase(TestBase):
         # Those two should be the same.
         self.assertEquals(threads[0], QC_thread_id)
 
-
     @debugserver_test
     @dsym_test
     def test_qThreadInfo_matches_qC_launch_debugserver_dsym(self):
@@ -955,7 +950,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.qThreadInfo_matches_qC()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -965,7 +959,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.qThreadInfo_matches_qC()
 
-
     @debugserver_test
     @dsym_test
     def test_qThreadInfo_matches_qC_attach_debugserver_dsym(self):
@@ -974,7 +967,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.qThreadInfo_matches_qC()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -984,7 +976,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.qThreadInfo_matches_qC()
 
-
     def p_returns_correct_data_size_for_each_qRegisterInfo(self):
         procs = self.prep_debug_monitor_and_inferior()
         self.add_register_info_collection_packets()
@@ -1021,7 +1012,6 @@ class LldbGdbServerTestCase(TestBase):
             # Increment loop
             reg_index += 1
 
-
     @debugserver_test
     @dsym_test
     def test_p_returns_correct_data_size_for_each_qRegisterInfo_launch_debugserver_dsym(self):
@@ -1030,7 +1020,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.p_returns_correct_data_size_for_each_qRegisterInfo()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -1040,7 +1029,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.p_returns_correct_data_size_for_each_qRegisterInfo()
 
-
     @debugserver_test
     @dsym_test
     def test_p_returns_correct_data_size_for_each_qRegisterInfo_attach_debugserver_dsym(self):
@@ -1049,7 +1037,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.p_returns_correct_data_size_for_each_qRegisterInfo()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -1059,7 +1046,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.p_returns_correct_data_size_for_each_qRegisterInfo()
 
-
     def Hg_switches_to_3_threads(self):
         # Startup the inferior with three threads (main + 2 new ones).
         procs = self.prep_debug_monitor_and_inferior(inferior_args=["thread:new", "thread:new"])
@@ -1076,7 +1062,7 @@ class LldbGdbServerTestCase(TestBase):
             # Change to each thread, verify current thread id.
             self.reset_test_sequence()
             self.test_sequence.add_log_lines(
-                ["read packet: $Hg{}#00".format(hex(thread)),  # Set current thread.
+                ["read packet: $Hg{0:x}#00".format(thread),  # Set current thread.
                  "send packet: $OK#00",
                  "read packet: $qC#00",
                  { "direction":"send", "regex":r"^\$QC([0-9a-fA-F]+)#", "capture":{1:"thread_id"} }],
@@ -1097,7 +1083,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.Hg_switches_to_3_threads()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -1107,7 +1092,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_launch()
         self.Hg_switches_to_3_threads()
 
-
     @debugserver_test
     @dsym_test
     def test_Hg_switches_to_3_threads_attach_debugserver_dsym(self):
@@ -1116,7 +1100,6 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.Hg_switches_to_3_threads()
 
-
     @llgs_test
     @dwarf_test
     @unittest2.expectedFailure()
@@ -1126,5 +1109,87 @@ class LldbGdbServerTestCase(TestBase):
         self.set_inferior_startup_attach()
         self.Hg_switches_to_3_threads()
 
+    def Hc_then_Csignal_signals_correct_thread(self):
+        # NOTE only run this one in inferior-launched mode: we can't grab inferior stdout when running attached,
+        # and the test requires getting stdout from the exe.
+
+        NUM_THREADS = 3
+        
+        # Startup the inferior with three threads (main + NUM_THREADS-1 worker threads).
+        inferior_args=["thread:print-ids"]
+        for i in range(NUM_THREADS - 1):
+            inferior_args.append("thread:new")
+        inferior_args.append("sleep:20")
+        
+        procs = self.prep_debug_monitor_and_inferior(inferior_args=inferior_args)
+
+        # Let the inferior process have a few moments to start up the thread when launched.
+        context = self.run_process_then_stop(run_seconds=1)
+
+        # Wait at most x seconds for all threads to be present.
+        threads = self.wait_for_thread_count(NUM_THREADS, timeout_seconds=5)
+        self.assertEquals(len(threads), NUM_THREADS)
+
+        # print_thread_ids = {}
+
+        # Switch to each thread, deliver a signal, and verify signal delivery
+        for thread_id in threads:
+            # Change to each thread, verify current thread id.
+            self.reset_test_sequence()
+            self.test_sequence.add_log_lines(
+                ["read packet: $Hc{0:x}#00".format(thread_id),  # Set current thread.
+                 "send packet: $OK#00",
+                 "read packet: $C{0:x}#00".format(signal.SIGUSR1),
+                 {"direction":"send", "regex":r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);", "capture":{1:"stop_signo", 2:"stop_thread_id"} },
+                 # "read packet: $vCont;C{0:x}:{1:x};c#00".format(signal.SIGUSR1, thread_id),
+                 # "read packet: $vCont;C{0:x};c#00".format(signal.SIGUSR1, thread_id),
+                 # { "type":"output_match", "regex":r"^received SIGUSR1 on thread id: ([0-9a-fA-F]+)\r\n$", "capture":{ 1:"print_thread_id"} },
+                 "read packet: $c#00",
+                 "read packet: {}".format(chr(03)),
+                 {"direction":"send", "regex":r"^\$T([0-9a-fA-F]{2})thread:([0-9a-fA-F]+);", "capture":{1:"intr_signo", 2:"intr_thread_id"} }
+                ],
+                True)
+
+            # Run the sequence.
+            context = self.expect_gdbremote_sequence()
+            self.assertIsNotNone(context)
+
+            # Ensure the stop signal is the signal we delivered.
+            stop_signo = context.get("stop_signo")
+            self.assertIsNotNone(stop_signo)
+            self.assertEquals(int(stop_signo,16), signal.SIGUSR1)
+            
+            # Ensure the stop thread is the thread to which we delivered the signal.
+            stop_thread_id = context.get("stop_thread_id")
+            self.assertIsNotNone(stop_thread_id)
+            self.assertEquals(int(stop_thread_id,16), thread_id)
+
+            # Ensure we haven't seen this thread id yet.  The inferior's self-obtained thread ids are not guaranteed to match the stub tids (at least on MacOSX).
+            # print_thread_id = context.get("print_thread_id")
+            # self.assertIsNotNone(print_thread_id)
+            # self.assertFalse(print_thread_id in print_thread_ids)
+            
+            # Now remember this print (i.e. inferior-reflected) thread id and ensure we don't hit it again.
+            # print_thread_ids[print_thread_id] = 1
+
+    @debugserver_test
+    @dsym_test
+    @unittest2.expectedFailure() # this test is failing on MacOSX 10.9
+    def test_Hc_then_Csignal_signals_correct_thread_launch_debugserver_dsym(self):
+        self.init_debugserver_test()
+        self.buildDsym()
+        self.set_inferior_startup_launch()
+        self.Hc_then_Csignal_signals_correct_thread()
+
+    @llgs_test
+    @dwarf_test
+    @unittest2.expectedFailure()
+    def test_Hc_then_Csignal_signals_correct_thread_launch_llgs_dwarf(self):
+        self.init_llgs_test()
+        self.buildDwarf()
+        self.set_inferior_startup_launch()
+        self.Hc_then_Csignal_signals_correct_thread()
+
+
 if __name__ == '__main__':
     unittest2.main()

Modified: lldb/trunk/test/tools/lldb-gdbserver/lldbgdbserverutils.py
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/test/tools/lldb-gdbserver/lldbgdbserverutils.py?rev=209845&r1=209844&r2=209845&view=diff
==============================================================================
--- lldb/trunk/test/tools/lldb-gdbserver/lldbgdbserverutils.py (original)
+++ lldb/trunk/test/tools/lldb-gdbserver/lldbgdbserverutils.py Thu May 29 15:44:45 2014
@@ -4,12 +4,13 @@
 import os
 import os.path
 import platform
+import Queue
 import re
 import select
+import socket_packet_pump
 import subprocess
 import time
 
-
 def _get_debug_monitor_from_lldb(lldb_exe, debug_monitor_basename):
     """Return the debug monitor exe path given the lldb exe path.
 
@@ -108,7 +109,7 @@ def _is_packet_lldb_gdbserver_input(pack
         raise "Unknown packet type: {}".format(packet_type)
 
 
-def handle_O_packet(context, packet_contents):
+def handle_O_packet(context, packet_contents, logger):
     """Handle O packets."""
     if (not packet_contents) or (len(packet_contents) < 1):
         return False
@@ -117,8 +118,13 @@ def handle_O_packet(context, packet_cont
     elif packet_contents == "OK":
         return False
 
-    context["O_content"] += gdbremote_hex_decode_string(packet_contents[1:])
+    new_text = gdbremote_hex_decode_string(packet_contents[1:])
+    context["O_content"] += new_text
     context["O_count"] += 1
+    
+    if logger:
+        logger.debug("text: new \"{}\", cumulative: \"{}\"".format(new_text, context["O_content"]))
+    
     return True
 
 _STRIP_CHECKSUM_REGEX = re.compile(r'#[0-9a-fA-F]{2}$')
@@ -134,9 +140,6 @@ def assert_packets_equal(asserter, actua
     expected_stripped = _STRIP_CHECKSUM_REGEX.sub('', expected_packet)
     asserter.assertEqual(actual_stripped, expected_stripped)
 
-
-_GDB_REMOTE_PACKET_REGEX = re.compile(r'^\$([^\#]*)#[0-9a-fA-F]{2}')
-
 def expect_lldb_gdbserver_replay(
     asserter,
     sock,
@@ -178,84 +181,64 @@ def expect_lldb_gdbserver_replay(
     # Ensure we have some work to do.
     if len(test_sequence.entries) < 1:
         return {}
-    
-    received_lines = []
-    receive_buffer = ''
+
     context = {"O_count":0, "O_content":""}
+    with socket_packet_pump.SocketPacketPump(sock, logger) as pump:
+        # Grab the first sequence entry.
+        sequence_entry = test_sequence.entries.pop(0)
+        
+        # While we have an active sequence entry, send messages
+        # destined for the stub and collect/match/process responses
+        # expected from the stub.
+        while sequence_entry:
+            if sequence_entry.is_send_to_remote():
+                # This is an entry to send to the remote debug monitor.
+                send_packet = sequence_entry.get_send_packet()
+                if logger:
+                    logger.info("sending packet to remote: %s" % send_packet)
+                sock.sendall(send_packet)
+            else:
+                # This is an entry expecting to receive content from the remote debug monitor.
 
-    sequence_entry = test_sequence.entries.pop(0)
-    while sequence_entry:
-        if sequence_entry.is_send_to_remote():
-            # This is an entry to send to the remote debug monitor.
-            send_packet = sequence_entry.get_send_packet()
-            if logger:
-                logger.info("sending packet to remote: %s" % send_packet)
-            sock.sendall(send_packet)
-        else:
-            # This is an entry to expect to receive from the remote debug monitor.
-            if logger:
-                logger.info("receiving packet from remote")
-
-            start_time = time.time()
-            timeout_time = start_time + timeout_seconds
-
-            # while we don't have a complete line of input, wait
-            # for it from socket.
-            while len(received_lines) < 1:
-                # check for timeout
-                if time.time() > timeout_time:
-                    raise Exception(
-                        'timed out after {} seconds while waiting for llgs to respond, currently received: {}'.format(
-                            timeout_seconds, receive_buffer))
-                can_read, _, _ = select.select([sock], [], [], 0)
-                if can_read and sock in can_read:
+                # We'll pull from (and wait on) the queue appropriate for the type of matcher.
+                # We keep separate queues for process output (coming from non-deterministic
+                # $O packet division) and for all other packets.
+                if sequence_entry.is_output_matcher():
                     try:
-                        new_bytes = sock.recv(4096)
-                    except:
-                        new_bytes = None
-                    if new_bytes and len(new_bytes) > 0:
-                        # read the next bits from the socket
+                        # Grab next entry from the output queue.
+                        content = pump.output_queue().get(True, timeout_seconds)
+                    except Queue.Empty:
                         if logger:
-                            logger.debug("llgs responded with bytes: {}".format(new_bytes))
-                        receive_buffer += new_bytes
-
-                        # parse fully-formed packets into individual packets
-                        has_more = len(receive_buffer) > 0
-                        while has_more:
-                            if len(receive_buffer) <= 0:
-                                has_more = False
-                            # handle '+' ack
-                            elif receive_buffer[0] == '+':
-                                received_lines.append('+')
-                                receive_buffer = receive_buffer[1:]
-                                if logger:
-                                    logger.debug('parsed packet from llgs: +, new receive_buffer: {}'.format(receive_buffer))
-                            else:
-                                packet_match = _GDB_REMOTE_PACKET_REGEX.match(receive_buffer)
-                                if packet_match:
-                                    if not handle_O_packet(context, packet_match.group(1)):
-                                        # Normal packet to match.
-                                        received_lines.append(packet_match.group(0))
-                                    receive_buffer = receive_buffer[len(packet_match.group(0)):]
-                                    if logger:
-                                        logger.debug('parsed packet from llgs: {}, new receive_buffer: {}'.format(packet_match.group(0), receive_buffer))
-                                else:
-                                    has_more = False
-            # got a line - now try to match it against expected line
-            if len(received_lines) > 0:
-                received_packet = received_lines.pop(0)
-                context = sequence_entry.assert_match(asserter, received_packet, context=context)
+                            logger.warning("timeout waiting for stub output (accumulated output:{})".format(pump.get_accumulated_output()))
+                        raise Exception("timed out while waiting for output match (accumulated output: {})".format(pump.get_accumulated_output()))
+                else:
+                    try:
+                        content = pump.packet_queue().get(True, timeout_seconds)
+                    except Queue.Empty:
+                        if logger:
+                            logger.warning("timeout waiting for packet match (receive buffer: {})".format(pump.get_receive_buffer()))
+                        raise Exception("timed out while waiting for packet match (receive buffer: {})".format(pump.get_receive_buffer()))
                 
-        # Move on to next sequence entry as needed.  Some sequence entries support executing multiple
-        # times in different states (for looping over query/response packets).
-        if sequence_entry.is_consumed():
-            if len(test_sequence.entries) > 0:
-                sequence_entry = test_sequence.entries.pop(0)
-            else:
-                sequence_entry = None
+                # Give the sequence entry the opportunity to match the content.
+                # Output matchers might match or pass after more output accumulates.
+                # Other packet types generally must match.
+                asserter.assertIsNotNone(content)
+                context = sequence_entry.assert_match(asserter, content, context=context)
+
+            # Move on to next sequence entry as needed.  Some sequence entries support executing multiple
+            # times in different states (for looping over query/response packets).
+            if sequence_entry.is_consumed():
+                if len(test_sequence.entries) > 0:
+                    sequence_entry = test_sequence.entries.pop(0)
+                else:
+                    sequence_entry = None
+    
+        # Fill in the O_content entries.
+        context["O_count"] = 1
+        context["O_content"] = pump.get_accumulated_output()
+        
     return context
 
-
 def gdbremote_hex_encode_string(str):
     output = ''
     for c in str:
@@ -271,7 +254,6 @@ def gdbremote_packet_encode_string(str):
         checksum += ord(c)
     return '$' + str + '#{0:02x}'.format(checksum % 256)
 
-
 def build_gdbremote_A_packet(args_list):
     """Given a list of args, create a properly-formed $A packet containing each arg.
     """
@@ -327,8 +309,11 @@ def parse_threadinfo_response(response_p
     # Return list of thread ids
     return [int(thread_id_hex,16) for thread_id_hex in response_packet.split(",") if len(thread_id_hex) > 0]
 
+class GdbRemoteEntryBase(object):
+    def is_output_matcher(self):
+        return False
 
-class GdbRemoteEntry(object):
+class GdbRemoteEntry(GdbRemoteEntryBase):
 
     def __init__(self, is_send_to_remote=True, exact_payload=None, regex=None, capture=None, expect_captures=None):
         """Create an entry representing one piece of the I/O to/from a gdb remote debug monitor.
@@ -446,7 +431,7 @@ class GdbRemoteEntry(object):
         else:
             raise Exception("Don't know how to match a remote-sent packet when exact_payload isn't specified.")
 
-class MultiResponseGdbRemoteEntry(object):
+class MultiResponseGdbRemoteEntry(GdbRemoteEntryBase):
     """Represents a query/response style packet.
     
     Assumes the first item is sent to the gdb remote.
@@ -557,6 +542,99 @@ class MultiResponseGdbRemoteEntry(object
         self._is_send_to_remote = True
         return context
 
+class MatchRemoteOutputEntry(GdbRemoteEntryBase):
+    """Waits for output from the debug monitor to match a regex or time out.
+    
+    This entry type tries to match each time new gdb remote output is accumulated
+    using a provided regex.  If the output does not match the regex within the
+    given timeframe, the command fails the playback session.  If the regex does
+    match, any capture fields are recorded in the context.
+    
+    Settings accepted from params:
+
+        regex: required. Specifies a compiled regex object that must either succeed
+            with re.match or re.search (see regex_mode below) within the given timeout
+            (see timeout_seconds below) or cause the playback to fail.
+
+        regex_mode: optional. Available values: "match" or "search". If "match", the entire
+            stub output as collected so far must match the regex.  If search, then the regex
+            must match starting somewhere within the output text accumulated thus far.
+            Default: "match" (i.e. the regex must match the entirety of the accumulated output
+            buffer, so unexpected text will generally fail the match).
+        
+        capture: optional.  If specified, is a dictionary of regex match group indices (should start
+            with 1) to variable names that will store the capture group indicated by the
+            index. For example, {1:"thread_id"} will store capture group 1's content in the
+            context dictionary where "thread_id" is the key and the match group value is
+            the value. The value stored off can be used later in a expect_captures expression.
+            This arg only makes sense when regex is specified.
+    """
+    def __init__(self, regex=None, regex_mode="match", capture=None):
+        self._regex = regex
+        self._regex_mode = regex_mode
+        self._capture = capture
+        self._matched = False
+        
+        if not self._regex:
+            raise Exception("regex cannot be None")
+        
+        if not self._regex_mode in ["match", "search"]:
+            raise Exception("unsupported regex mode \"{}\": must be \"match\" or \"search\"".format(self._regex_mode))
+
+    def is_output_matcher(self):
+        return True
+
+    def is_send_to_remote(self):
+        # This is always a "wait for remote" command.
+        return False
+
+    def is_consumed(self):
+        return self._matched
+
+    def assert_match(self, asserter, accumulated_output, context):
+        # Validate args.
+        if not accumulated_output:
+            raise Exception("accumulated_output cannot be none")
+        if not context:
+            raise Exception("context cannot be none")
+
+        # Validate that we haven't already matched.
+        if self._matched:
+            raise Exception("invalid state - already matched, attempting to match again")
+
+        # If we don't have any content yet, we don't match.
+        if len(accumulated_output) < 1:
+            return context
+        
+        # Check if we match
+        if self._regex_mode == "match":
+            match = self._regex.match(accumulated_output)
+        elif self._regex_mode == "search":
+            match = self._regex.search(accumulated_output)
+        else:
+            raise Exception("Unexpected regex mode: {}".format(self._regex_mode))
+        
+        # If we don't match, wait to try again after next $O content, or time out.
+        if not match:
+            # print "re pattern \"{}\" did not match against \"{}\"".format(self._regex.pattern, accumulated_output)
+            return context
+        
+        # We do match.
+        self._matched = True
+        # print "re pattern \"{}\" matched against \"{}\"".format(self._regex.pattern, accumulated_output)
+        
+        # Collect up any captures into the context.
+        if self._capture:
+            # Handle captures.
+            for group_index, var_name in self._capture.items():
+                capture_text = match.group(group_index)
+                if not capture_text:
+                    raise Exception("No content for group index {}".format(group_index))
+                context[var_name] = capture_text
+
+        return context
+
+
 class GdbRemoteTestSequence(object):
 
     _LOG_LINE_REGEX = re.compile(r'^.*(read|send)\s+packet:\s+(.+)$')
@@ -569,21 +647,21 @@ class GdbRemoteTestSequence(object):
         for line in log_lines:
             if type(line) == str:
                 # Handle log line import
-                if self.logger:
-                    self.logger.debug("processing log line: {}".format(line))
+                # if self.logger:
+                #     self.logger.debug("processing log line: {}".format(line))
                 match = self._LOG_LINE_REGEX.match(line)
                 if match:
                     playback_packet = match.group(2)
                     direction = match.group(1)
                     if _is_packet_lldb_gdbserver_input(direction, remote_input_is_read):
                         # Handle as something to send to the remote debug monitor.
-                        if self.logger:
-                            self.logger.info("processed packet to send to remote: {}".format(playback_packet))
+                        # if self.logger:
+                        #     self.logger.info("processed packet to send to remote: {}".format(playback_packet))
                         self.entries.append(GdbRemoteEntry(is_send_to_remote=True, exact_payload=playback_packet))
                     else:
                         # Log line represents content to be expected from the remote debug monitor.
-                        if self.logger:
-                            self.logger.info("receiving packet from llgs, should match: {}".format(playback_packet))
+                        # if self.logger:
+                        #     self.logger.info("receiving packet from llgs, should match: {}".format(playback_packet))
                         self.entries.append(GdbRemoteEntry(is_send_to_remote=False,exact_payload=playback_packet))
                 else:
                     raise Exception("failed to interpret log line: {}".format(line))
@@ -602,16 +680,26 @@ class GdbRemoteTestSequence(object):
 
                     if _is_packet_lldb_gdbserver_input(direction, remote_input_is_read):
                         # Handle as something to send to the remote debug monitor.
-                        if self.logger:
-                            self.logger.info("processed dict sequence to send to remote")
+                        # if self.logger:
+                        #     self.logger.info("processed dict sequence to send to remote")
                         self.entries.append(GdbRemoteEntry(is_send_to_remote=True, regex=regex, capture=capture, expect_captures=expect_captures))
                     else:
                         # Log line represents content to be expected from the remote debug monitor.
-                        if self.logger:
-                            self.logger.info("processed dict sequence to match receiving from remote")
+                        # if self.logger:
+                        #     self.logger.info("processed dict sequence to match receiving from remote")
                         self.entries.append(GdbRemoteEntry(is_send_to_remote=False, regex=regex, capture=capture, expect_captures=expect_captures))
                 elif entry_type == "multi_response":
                     self.entries.append(MultiResponseGdbRemoteEntry(line))
+                elif entry_type == "output_match":
+
+                    regex = line.get("regex", None)
+                    # Compile the regex.
+                    if regex and (type(regex) == str):
+                        regex = re.compile(regex)
+
+                    regex_mode = line.get("regex_mode", "match")
+                    capture = line.get("capture", None)
+                    self.entries.append(MatchRemoteOutputEntry(regex=regex, regex_mode=regex_mode, capture=capture))
                 else:
                     raise Exception("unknown entry type \"%s\"" % entry_type)
 

Modified: lldb/trunk/test/tools/lldb-gdbserver/main.cpp
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/test/tools/lldb-gdbserver/main.cpp?rev=209845&r1=209844&r2=209845&view=diff
==============================================================================
--- lldb/trunk/test/tools/lldb-gdbserver/main.cpp (original)
+++ lldb/trunk/test/tools/lldb-gdbserver/main.cpp Thu May 29 15:44:45 2014
@@ -1,24 +1,88 @@
 #include <cstdlib>
 #include <cstring>
-#include <iostream>
+#include <errno.h>
+#include <inttypes.h>
 #include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
 #include <unistd.h>
 #include <vector>
 
+#if defined(__linux__)
+#include <sys/syscall.h>
+#endif
+
 static const char *const RETVAL_PREFIX = "retval:";
 static const char *const SLEEP_PREFIX  = "sleep:";
 static const char *const STDERR_PREFIX = "stderr:";
 
 static const char *const THREAD_PREFIX = "thread:";
 static const char *const THREAD_COMMAND_NEW = "new"; 
+static const char *const THREAD_COMMAND_PRINT_IDS = "print-ids"; 
+
+static bool g_print_thread_ids = false;
+static pthread_mutex_t g_print_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+static void
+print_thread_id ()
+{
+	// Put in the right magic here for your platform to spit out the thread id (tid) that debugserver/lldb-gdbserver would see as a TID.
+	// Otherwise, let the else clause print out the unsupported text so that the unit test knows to skip verifying thread ids.
+#if defined(__APPLE__)
+	printf ("%" PRIx64, static_cast<uint64_t> (pthread_mach_thread_np(pthread_self())));
+#elif defined (__linux__)
+	// This is a call to gettid() via syscall.
+	printf ("%" PRIx64, static_cast<uint64_t> (syscall (__NR_gettid)));
+#else
+	printf("{no-tid-support}");
+#endif
+}
+
+static void
+signal_handler (int signo)
+{
+	switch (signo)
+	{
+	case SIGUSR1:
+		// Print notice that we received the signal on a given thread.
+		pthread_mutex_lock (&g_print_mutex);
+		printf ("received SIGUSR1 on thread id: ");
+		print_thread_id ();
+		printf ("\n");
+		pthread_mutex_unlock (&g_print_mutex);
+		
+		// Reset the signal handler.
+		sig_t sig_result = signal (SIGUSR1, signal_handler);
+		if (sig_result == SIG_ERR)
+		{
+			fprintf(stderr, "failed to set signal handler: errno=%d\n", errno);
+			exit (1);
+		}
+
+		break;
+	}
+}
 
 static void*
 thread_func (void *arg)
 {
+	static int s_thread_index = 1;
 	// For now, just sleep for a few seconds.
 	// std::cout << "thread " << pthread_self() << ": created" << std::endl;
 
-	int sleep_seconds_remaining = 5;
+	const int this_thread_index = s_thread_index++;
+
+	if (g_print_thread_ids)
+	{
+		pthread_mutex_lock (&g_print_mutex);
+		printf ("thread %d id: ", this_thread_index);
+		print_thread_id ();
+		printf ("\n");
+		pthread_mutex_unlock (&g_print_mutex);
+	}
+
+	int sleep_seconds_remaining = 20;
 	while (sleep_seconds_remaining > 0)
 	{
 		sleep_seconds_remaining = sleep (sleep_seconds_remaining);
@@ -33,12 +97,21 @@ int main (int argc, char **argv)
 	std::vector<pthread_t> threads;
     int return_value = 0;
 
+	// Set the signal handler.
+	sig_t sig_result = signal (SIGUSR1, signal_handler);
+	if (sig_result == SIG_ERR)
+	{
+		fprintf(stderr, "failed to set signal handler: errno=%d\n", errno);
+		exit (1);
+	}
+	
+	// Process command line args.
     for (int i = 1; i < argc; ++i)
     {
         if (std::strstr (argv[i], STDERR_PREFIX))
         {
             // Treat remainder as text to go to stderr.
-            std::cerr << (argv[i] + strlen (STDERR_PREFIX)) << std::endl;
+            fprintf (stderr, "%s\n", (argv[i] + strlen (STDERR_PREFIX)));
         }
         else if (std::strstr (argv[i], RETVAL_PREFIX))
         {
@@ -68,11 +141,23 @@ int main (int argc, char **argv)
 				const int err = ::pthread_create (&new_thread, NULL, thread_func, NULL);
 			    if (err)
 				{
-					std::cerr << "pthread_create() failed with error code " << err << std::endl;
+					fprintf (stderr, "pthread_create() failed with error code %d\n", err);
 					exit (err);
 				}
 				threads.push_back (new_thread);
 			}
+			else if (std::strstr (argv[i] + strlen(THREAD_PREFIX), THREAD_COMMAND_PRINT_IDS))
+			{
+				// Turn on thread id announcing.
+				g_print_thread_ids = true;
+				
+				// And announce us.
+				pthread_mutex_lock (&g_print_mutex);
+				printf ("thread 0 id: ");
+				print_thread_id ();
+				printf ("\n");
+				pthread_mutex_unlock (&g_print_mutex);
+			}
 			else
 			{
 				// At this point we don't do anything else with threads.
@@ -82,7 +167,7 @@ int main (int argc, char **argv)
         else
         {
             // Treat the argument as text for stdout.
-            std::cout << argv[i] << std::endl;
+            printf("%s\n", argv[i]);
         }
     }
 
@@ -92,9 +177,7 @@ int main (int argc, char **argv)
 		void *thread_retval = NULL;
 		const int err = ::pthread_join (*it, &thread_retval);
 	    if (err != 0)
-		{
-			std::cerr << "pthread_join() failed with error code " << err << std::endl;
-		}
+			fprintf (stderr, "pthread_join() failed with error code %d\n", err);
 	}
 
     return return_value;

Added: lldb/trunk/test/tools/lldb-gdbserver/socket_packet_pump.py
URL: http://llvm.org/viewvc/llvm-project/lldb/trunk/test/tools/lldb-gdbserver/socket_packet_pump.py?rev=209845&view=auto
==============================================================================
--- lldb/trunk/test/tools/lldb-gdbserver/socket_packet_pump.py (added)
+++ lldb/trunk/test/tools/lldb-gdbserver/socket_packet_pump.py Thu May 29 15:44:45 2014
@@ -0,0 +1,156 @@
+import Queue
+import re
+import select
+import threading
+
+def _handle_output_packet_string(packet_contents):
+    if (not packet_contents) or (len(packet_contents) < 1):
+        return None
+    elif packet_contents[0] != "O":
+        return None
+    elif packet_contents == "OK":
+        return None
+    else:
+        return packet_contents[1:].decode("hex")
+
+class SocketPacketPump(object):
+    """A threaded packet reader that partitions packets into two streams.
+
+    All incoming $O packet content is accumulated with the current accumulation
+    state put into the OutputQueue.
+
+    All other incoming packets are placed in the packet queue.
+
+    A select thread can be started and stopped, and runs to place packet
+    content into the two queues.
+    """
+
+    _GDB_REMOTE_PACKET_REGEX = re.compile(r'^\$([^\#]*)#[0-9a-fA-F]{2}')
+
+    def __init__(self, pump_socket, logger=None):
+        if not pump_socket:
+            raise Exception("pump_socket cannot be None")
+
+        self._output_queue = Queue.Queue()
+        self._packet_queue = Queue.Queue()
+        self._thread = None
+        self._stop_thread = False
+        self._socket = pump_socket
+        self._logger = logger
+        self._receive_buffer = ""
+        self._accumulated_output = ""
+
+    def __enter__(self):
+        """Support the python 'with' statement.
+
+        Start the pump thread."""
+        self.start_pump_thread()
+        return self
+
+    def __exit__(self, exit_type, value, traceback):
+        """Support the python 'with' statement.
+
+        Shut down the pump thread."""
+        self.stop_pump_thread()
+
+    def start_pump_thread(self):
+        if self._thread:
+            raise Exception("pump thread is already running")
+        self._stop_thread = False
+        self._thread = threading.Thread(target=self._run_method)
+        self._thread.start()
+
+    def stop_pump_thread(self):
+        self._stop_thread = True
+        if self._thread:
+            self._thread.join()
+
+    def output_queue(self):
+        return self._output_queue
+
+    def packet_queue(self):
+        return self._packet_queue
+
+    def _process_new_bytes(self, new_bytes):
+        if not new_bytes:
+            return
+        if len(new_bytes) < 1:
+            return
+
+        # Add new bytes to our accumulated unprocessed packet bytes.
+        self._receive_buffer += new_bytes
+
+        # Parse fully-formed packets into individual packets.
+        has_more = len(self._receive_buffer) > 0
+        while has_more:
+            if len(self._receive_buffer) <= 0:
+                has_more = False
+            # handle '+' ack
+            elif self._receive_buffer[0] == "+":
+                self._packet_queue.put("+")
+                self._receive_buffer = self._receive_buffer[1:]
+                if self._logger:
+                    self._logger.debug(
+                        "parsed packet from stub: +\n" +
+                        "new receive_buffer: {}".format(
+                            self._receive_buffer))
+            else:
+                packet_match = self._GDB_REMOTE_PACKET_REGEX.match(
+                    self._receive_buffer)
+                if packet_match:
+                    # Our receive buffer matches a packet at the
+                    # start of the receive buffer.
+                    new_output_content = _handle_output_packet_string(
+                        packet_match.group(1))
+                    if new_output_content:
+                        # This was an $O packet with new content.
+                        self._accumulated_output += new_output_content
+                        self._output_queue.put(self._accumulated_output)
+                    else:
+                        # Any packet other than $O.
+                        self._packet_queue.put(packet_match.group(0))
+
+                    # Remove the parsed packet from the receive
+                    # buffer.
+                    self._receive_buffer = self._receive_buffer[
+                        len(packet_match.group(0)):]
+                    if self._logger:
+                        self._logger.debug("parsed packet from stub: " +
+                            packet_match.group(0))
+                        self._logger.debug("new receive_buffer: " +
+                            self._receive_buffer)
+                else:
+                    # We don't have enough in the receive bufferto make a full
+                    # packet. Stop trying until we read more.
+                    has_more = False
+
+    def _run_method(self):
+        self._receive_buffer = ""
+        self._accumulated_output = ""
+
+        if self._logger:
+            self._logger.info("socket pump starting")
+
+        # Keep looping around until we're asked to stop the thread.
+        while not self._stop_thread:
+            can_read, _, _ = select.select([self._socket], [], [], 0)
+            if can_read and self._socket in can_read:
+                try:
+                    new_bytes = self._socket.recv(4096)
+                    if self._logger and new_bytes and len(new_bytes) > 0:
+                        self._logger.debug("pump received bytes: {}".format(new_bytes))
+                except:
+                    # Likely a closed socket.  Done with the pump thread.
+                    if self._logger:
+                        self._logger.debug("socket read failed, stopping pump read thread")
+                    break
+                self._process_new_bytes(new_bytes)
+
+        if self._logger:
+            self._logger.info("socket pump exiting")
+
+    def get_accumulated_output(self):
+        return self._accumulated_output
+        
+    def get_receive_buffer(self):
+        return self._receive_buffer
\ No newline at end of file





More information about the lldb-commits mailing list