forked from OSchip/llvm-project
458 lines
18 KiB
Python
458 lines
18 KiB
Python
"""
|
|
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)
|