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) | 
