diff options
Diffstat (limited to 'packages/Python/lldbsuite/test/darwin_log.py')
-rw-r--r-- | packages/Python/lldbsuite/test/darwin_log.py | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/packages/Python/lldbsuite/test/darwin_log.py b/packages/Python/lldbsuite/test/darwin_log.py new file mode 100644 index 000000000000..3207ef0bccd8 --- /dev/null +++ b/packages/Python/lldbsuite/test/darwin_log.py @@ -0,0 +1,457 @@ +""" +Base class for DarwinLog tests. +""" + +# System imports +from __future__ import print_function + +import json +import os +import platform +import re +import sys +import threading + + +# lldb imports +import lldb +from lldb import SBProcess, SBTarget + +from lldbsuite.test import decorators +from lldbsuite.test import lldbtest +from lldbsuite.test import lldbtest_config +from lldbsuite.test import lldbutil + + +def expand_darwinlog_command(command): + return "plugin structured-data darwin-log " + command + + +def expand_darwinlog_settings_set_command(command): + return "settings set plugin.structured-data.darwin-log." + command + + +class DarwinLogTestBase(lldbtest.TestBase): + """Base class for DarwinLog test cases that are pexpect-based.""" + NO_DEBUG_INFO_TESTCASE = True + + CONTINUE_REGEX = re.compile(r"Process \d+ resuming") + + def setUp(self): + # Call super's setUp(). + super(DarwinLogTestBase, self).setUp() + + # Until other systems support this, exit + # early if we're not macOS version 10.12 + # or greater. + version = platform.mac_ver()[0].split('.') + if ((int(version[0]) == 10) and (int(version[1]) < 12) or + (int(version[0]) < 10)): + self.skipTest("DarwinLog tests currently require macOS 10.12+") + return + + self.child = None + self.child_prompt = '(lldb) ' + self.strict_sources = False + self.enable_process_monitor_logging = False + + def run_lldb_to_breakpoint(self, exe, source_file, line, + enable_command=None, settings_commands=None): + + import pexpect + # Set self.child_prompt, which is "(lldb) ". + prompt = self.child_prompt + + # So that the child gets torn down after the test. + self.child = pexpect.spawn('%s %s %s' % (lldbtest_config.lldbExec, + self.lldbOption, exe)) + child = self.child + + # Turn on logging for what the child sends back. + if self.TraceOn(): + child.logfile_read = sys.stdout + + if self.enable_process_monitor_logging: + if platform.system() == 'Darwin': + self.runCmd( + "settings set target.process.extra-startup-command " + "QSetLogging:bitmask=LOG_DARWIN_LOG;") + self.expect_prompt() + + # Run the enable command if we have one. + if enable_command is not None: + self.runCmd(enable_command) + self.expect_prompt() + + # Disable showing of source lines at our breakpoint. + # This is necessary for the logging tests, because the very + # text we want to match for output from the running inferior + # will show up in the source as well. We don't want the source + # output to erroneously make a match with our expected output. + self.runCmd("settings set stop-line-count-before 0") + self.expect_prompt() + self.runCmd("settings set stop-line-count-after 0") + self.expect_prompt() + + # While we're debugging, turn on packet logging. + self.runCmd("log enable -f /tmp/packets.log gdb-remote packets") + self.expect_prompt() + + # Prevent mirroring of NSLog/os_log content to stderr. We want log + # messages to come exclusively through our log channel. + self.runCmd( + "settings set target.env-vars IDE_DISABLED_OS_ACTIVITY_DT_MODE=1") + self.expect_prompt() + + # Run any darwin-log settings commands now, before we enable logging. + if settings_commands is not None: + for setting_command in settings_commands: + self.runCmd( + expand_darwinlog_settings_set_command(setting_command)) + self.expect_prompt() + + # Set breakpoint right before the os_log() macros. We don't + # set it on the os_log*() calls because these are a number of + # nested-scoped calls that will cause the debugger to stop + # multiple times on the same line. That is difficult to match + # os_log() content by since it is non-deterministic what the + # ordering between stops and log lines will be. This is why + # we stop before, and then have the process run in a sleep + # afterwards, so we get the log messages while the target + # process is "running" (sleeping). + child.sendline('breakpoint set -f %s -l %d' % (source_file, line)) + child.expect_exact(prompt) + + # Now run to the breakpoint that we just set. + child.sendline('run') + child.expect_exact(prompt) + + # Ensure we stopped at a breakpoint. + self.runCmd("thread list") + self.expect(re.compile(r"stop reason = breakpoint")) + + # Now we're ready to check if DarwinLog is available. + if not self.darwin_log_available(): + self.skipTest("DarwinLog not available") + + def runCmd(self, cmd): + self.child.sendline(cmd) + + def expect_prompt(self, exactly=True): + self.expect(self.child_prompt, exactly=exactly) + + def expect(self, pattern, exactly=False, *args, **kwargs): + if exactly: + return self.child.expect_exact(pattern, *args, **kwargs) + return self.child.expect(pattern, *args, **kwargs) + + def darwin_log_available(self): + self.runCmd("plugin structured-data darwin-log status") + self.expect(re.compile(r"Availability: ([\S]+)")) + return self.child.match is not None and ( + self.child.match.group(1) == "available") + + def do_test(self, enable_options, expect_regexes=None, + settings_commands=None): + """Test that a single fall-through reject rule rejects all logging.""" + self.build(dictionary=self.d) + self.setTearDownCleanup(dictionary=self.d) + + # Build the darwin-log enable command. + enable_cmd = expand_darwinlog_command('enable') + if enable_options is not None and len(enable_options) > 0: + enable_cmd += ' ' + ' '.join(enable_options) + + exe = os.path.join(os.getcwd(), self.exe_name) + self.run_lldb_to_breakpoint(exe, self.source, self.line, + enable_command=enable_cmd, + settings_commands=settings_commands) + self.expect_prompt() + + # Now go. + self.runCmd("process continue") + self.expect(self.CONTINUE_REGEX) + + if expect_regexes is None: + # Expect matching a log line or program exit. + # Test methods determine which ones are valid. + expect_regexes = ( + [re.compile(r"source-log-([^-]+)-(\S+)"), + re.compile(r"exited with status") + ]) + self.expect(expect_regexes) + + +def remove_add_mode_entry(log_entries): + """libtrace creates an "Add Mode:..." message when logging is enabled. + Strip this out of results since our test subjects don't create it.""" + return [entry for entry in log_entries + if "message" in entry and + not entry["message"].startswith("Add Mode:")] + + +class DarwinLogEventBasedTestBase(lldbtest.TestBase): + """Base class for event-based DarwinLog tests.""" + NO_DEBUG_INFO_TESTCASE = True + + class EventListenerThread(threading.Thread): + + def __init__(self, listener, process, trace_on, max_entry_count): + super( + DarwinLogEventBasedTestBase.EventListenerThread, + self).__init__() + self.process = process + self.listener = listener + self.trace_on = trace_on + self.max_entry_count = max_entry_count + self.exception = None + self.structured_data_event_count = 0 + self.wait_seconds = 2 + self.max_timeout_count = 4 + self.log_entries = [] + + def handle_structured_data_event(self, event): + structured_data = SBProcess.GetStructuredDataFromEvent(event) + if not structured_data.IsValid(): + if self.trace_on: + print("invalid structured data") + return + + # Track that we received a valid structured data event. + self.structured_data_event_count += 1 + + # Grab the individual log entries from the JSON. + stream = lldb.SBStream() + structured_data.GetAsJSON(stream) + dict = json.loads(stream.GetData()) + self.log_entries.extend(dict["events"]) + if self.trace_on: + print("Structured data (raw):", stream.GetData()) + + # Print the pretty-printed version. + if self.trace_on: + stream.Clear() + structured_data.PrettyPrint(stream) + print("Structured data (pretty print):", + stream.GetData()) + + def done(self, timeout_count): + """Returns True when we're done listening for events.""" + # See if we should consider the number of events retrieved. + if self.max_entry_count is not None: + if len(self.log_entries) >= self.max_entry_count: + # We've received the max threshold of events expected, + # we can exit here. + if self.trace_on: + print("Event listener thread exiting due to max " + "expected log entry count being reached.") + return True + + # If our event timeout count has exceeded our maximum timeout count, + # we're done. + if timeout_count >= self.max_timeout_count: + if self.trace_on: + print("Event listener thread exiting due to max number of " + "WaitForEvent() timeouts being reached.") + return True + + # If our process is dead, we're done. + if not self.process.is_alive: + if self.trace_on: + print("Event listener thread exiting due to test inferior " + "exiting.") + return True + + # We're not done. + return False + + def run(self): + event = lldb.SBEvent() + try: + timeout_count = 0 + + # Wait up to 4 times for the event to arrive. + while not self.done(timeout_count): + if self.trace_on: + print("Calling wait for event...") + if self.listener.WaitForEvent(self.wait_seconds, event): + while event.IsValid(): + # Check if it's a process event. + if SBProcess.EventIsStructuredDataEvent(event): + self.handle_structured_data_event(event) + else: + if self.trace_on: + print("ignoring unexpected event:", + lldbutil.get_description(event)) + # Grab the next event, if there is one. + event.Clear() + if not self.listener.GetNextEvent(event): + if self.trace_on: + print("listener has no more events " + "available at this time") + else: + if self.trace_on: + print("timeout occurred waiting for event...") + timeout_count += 1 + self.listener.Clear() + except Exception as e: + self.exception = e + + def setUp(self): + # Call super's setUp(). + super(DarwinLogEventBasedTestBase, self).setUp() + + # Until other systems support this, exit + # early if we're not macOS version 10.12 + # or greater. + version = platform.mac_ver()[0].split('.') + if ((int(version[0]) == 10) and (int(version[1]) < 12) or + (int(version[0]) < 10)): + self.skipTest("DarwinLog tests currently require macOS 10.12+") + return + + # Source filename. + self.source = 'main.c' + + # Output filename. + self.exe_name = 'a.out' + self.d = {'C_SOURCES': self.source, 'EXE': self.exe_name} + + # Locate breakpoint. + self.line = lldbtest.line_number(self.source, '// break here') + + # Enable debugserver logging of the darwin log collection + # mechanism. + self.runCmd("settings set target.process.extra-startup-command " + "QSetLogging:bitmask=LOG_DARWIN_LOG;") + + def darwin_log_available(self): + match = self.match("plugin structured-data darwin-log status", + patterns=[r"Availability: ([\S]+)"]) + return match is not None and (match.group(1) == "available") + + def do_test(self, enable_options, settings_commands=None, + run_enable_after_breakpoint=False, max_entry_count=None): + """Runs the test inferior, returning collected events. + + This method runs the test inferior to the first breakpoint hit. + It then adds a listener for structured data events, and collects + all events from that point forward until end of execution of the + test inferior. It then returns those events. + + @return + A list of structured data events received, in the order they + were received. + """ + self.build(dictionary=self.d) + self.setTearDownCleanup(dictionary=self.d) + + exe = os.path.join(os.getcwd(), self.exe_name) + + # Create a target by the debugger. + target = self.dbg.CreateTarget(exe) + self.assertTrue(target, lldbtest.VALID_TARGET) + + # Run the darwin-log settings commands. + if settings_commands is not None: + for setting_command in settings_commands: + self.runCmd( + expand_darwinlog_settings_set_command(setting_command)) + + # Build the darwin-log enable command. + enable_cmd = expand_darwinlog_command("enable") + if enable_options is not None and len(enable_options) > 0: + enable_cmd += ' ' + ' '.join(enable_options) + + # Run the darwin-log enable command now if we are not supposed + # to do it at the first breakpoint. This tests the start-up + # code, which has the benefit of being able to set os_log-related + # environment variables. + if not run_enable_after_breakpoint: + self.runCmd(enable_cmd) + + # Create the breakpoint. + breakpoint = target.BreakpointCreateByLocation(self.source, self.line) + self.assertIsNotNone(breakpoint) + self.assertTrue(breakpoint.IsValid()) + self.assertEqual(1, breakpoint.GetNumLocations(), + "Should have found one breakpoint") + + # Enable packet logging. + # self.runCmd("log enable -f /tmp/packets.log gdb-remote packets") + # self.runCmd("log enable lldb process") + + # Launch the process - doesn't stop at entry. + process = target.LaunchSimple(None, None, os.getcwd()) + self.assertIsNotNone(process, lldbtest.PROCESS_IS_VALID) + + # Keep track of whether we're tracing output. + trace_on = self.TraceOn() + + # Get the next thread that stops. + from lldbsuite.test.lldbutil import get_stopped_thread + thread = get_stopped_thread(process, lldb.eStopReasonBreakpoint) + + self.assertIsNotNone(thread, "There should be a thread stopped " + "due to breakpoint") + + # The process should be stopped at this point. + self.expect("process status", lldbtest.PROCESS_STOPPED, + patterns=['Process .* stopped']) + + # The stop reason of the thread should be breakpoint. + self.expect("thread list", lldbtest.STOPPED_DUE_TO_BREAKPOINT, + substrs=['stopped', 'stop reason = breakpoint']) + + # And our one and only breakpoint should have been hit. + self.assertEquals(breakpoint.GetHitCount(), 1) + + # Check if DarwinLog is available. This check cannot be done + # until after the process has started, as the feature availability + # comes through the stub. The stub isn't running until + # the target process is running. So this is really the earliest + # we can check. + if not self.darwin_log_available(): + self.skipTest("DarwinLog not available") + + # Now setup the structured data listener. + # + # Grab the broadcaster for the process. We'll be attaching our + # listener to it. + broadcaster = process.GetBroadcaster() + self.assertIsNotNone(broadcaster) + + listener = lldb.SBListener("SBStructuredData listener") + self.assertIsNotNone(listener) + + rc = broadcaster.AddListener( + listener, lldb.SBProcess.eBroadcastBitStructuredData) + self.assertTrue(rc, "Successfully add listener to process broadcaster") + + # Start the listening thread to retrieve the events. + # Bump up max entry count for the potentially included Add Mode: + # entry. + if max_entry_count is not None: + max_entry_count += 1 + event_thread = self.EventListenerThread(listener, process, trace_on, + max_entry_count) + event_thread.start() + + # Continue the test inferior. We should get any events after this. + process.Continue() + + # Wait until the event thread terminates. + # print("main thread now waiting for event thread to receive events.") + event_thread.join() + + # If the process is still alive, we kill it here. + if process.is_alive: + process.Kill() + + # Fail on any exceptions that occurred during event execution. + if event_thread.exception is not None: + # Re-raise it here so it shows up as a test error. + raise event_thread + + # Return the collected logging events. + return remove_add_mode_entry(event_thread.log_entries) |