Skip to content

Commit 02bedf7

Browse files
committed
Optionally symbolicate backtraces.
This PR adds the ability to symbolicate backtraces on Darwin and Windows. A few different modes are provided: mangled, demangled, and "precise demangled" (which includes symbol addresses and instruction pointer offsets.) Tools such as the Swift VS Code plugin will be able to adopt this new feature along with VS Code's new call stacks API (microsoft/vscode#222126). Note that on Linux, it is not currently possible to symbolicate backtraces meaningfully. The standard library's `Backtrace` type has the ability to do this for us, but we'll need some tweaks to its interface before we can adopt it. Note also that Apple's internal Core Symbolication framework is not used; we may be able to add support for it in a future PR (or Apple may opt to use it in their internal fork of Swift Testing.) There is no way to emit backtraces to the command line right now. I considered having `--very-verbose` imply backtraces, but it's something I'm going to reserve for a future PR after discussion with the community.
1 parent cca6de2 commit 02bedf7

File tree

12 files changed

+440
-4
lines changed

12 files changed

+440
-4
lines changed

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ public struct __CommandLineArguments_v0: Sendable {
191191
/// The value of the `--parallel` or `--no-parallel` argument.
192192
public var parallel: Bool?
193193

194+
/// The value of the `--symbolicate-backtraces` argument.
195+
public var symbolicateBacktraces: String?
196+
194197
/// The value of the `--verbose` argument.
195198
public var verbose: Bool?
196199

@@ -280,6 +283,7 @@ extension __CommandLineArguments_v0: Codable {
280283
enum CodingKeys: String, CodingKey {
281284
case listTests
282285
case parallel
286+
case symbolicateBacktraces
283287
case verbose
284288
case veryVerbose
285289
case quiet
@@ -366,6 +370,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
366370
result.parallel = false
367371
}
368372

373+
// Whether or not to symbolicate backtraces in the event stream.
374+
if let symbolicateBacktracesIndex = args.firstIndex(of: "--symbolicate-backtraces"), !isLastArgument(at: symbolicateBacktracesIndex) {
375+
result.symbolicateBacktraces = args[args.index(after: symbolicateBacktracesIndex)]
376+
}
377+
369378
// Verbosity
370379
if let verbosityIndex = args.firstIndex(of: "--verbosity"), !isLastArgument(at: verbosityIndex),
371380
let verbosity = Int(args[args.index(after: verbosityIndex)]) {
@@ -425,6 +434,21 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr
425434
// Parallelization (on by default)
426435
configuration.isParallelizationEnabled = args.parallel ?? true
427436

437+
// Whether or not to symbolicate backtraces in the event stream.
438+
if let symbolicateBacktraces = args.symbolicateBacktraces {
439+
switch symbolicateBacktraces.lowercased() {
440+
case "mangled", "on", "true":
441+
configuration.backtraceSymbolicationMode = .mangled
442+
case "demangled":
443+
configuration.backtraceSymbolicationMode = .demangled
444+
case "precise-demangled":
445+
configuration.backtraceSymbolicationMode = .preciseDemangled
446+
default:
447+
throw _EntryPointError.invalidArgument("--symbolicate-backtraces", value: symbolicateBacktraces)
448+
449+
}
450+
}
451+
428452
#if !SWT_NO_FILE_IO
429453
// XML output
430454
if let xunitOutputPath = args.xunitOutput {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
extension ABIv0 {
12+
/// A type implementing the JSON encoding of ``Backtrace`` for the ABI entry
13+
/// point and event stream output.
14+
///
15+
/// This type is not part of the public interface of the testing library. It
16+
/// assists in converting values to JSON; clients that consume this JSON are
17+
/// expected to write their own decoders.
18+
struct EncodedBacktrace: Sendable {
19+
/// A type describing a frame in the backtrace.
20+
struct Frame: Sendable {
21+
/// The address of the frame.
22+
var address: Backtrace.Address
23+
24+
/// The name of the frame, possibly demangled, if available.
25+
var symbolName: String?
26+
}
27+
28+
/// The frames in the backtrace.
29+
var frames: [Frame]
30+
31+
init(encoding backtrace: borrowing Backtrace, in eventContext: borrowing Event.Context) {
32+
if let symbolicationMode = eventContext.configuration?.backtraceSymbolicationMode {
33+
frames = zip(backtrace.addresses, backtrace.symbolicate(symbolicationMode)).map(Frame.init)
34+
} else {
35+
frames = backtrace.addresses.map { Frame(address: $0) }
36+
}
37+
}
38+
}
39+
}
40+
41+
// MARK: - Codable
42+
43+
extension ABIv0.EncodedBacktrace: Codable {
44+
func encode(to encoder: any Encoder) throws {
45+
try frames.encode(to: encoder)
46+
}
47+
48+
init(from decoder: any Decoder) throws {
49+
self.frames = try [Frame](from: decoder)
50+
}
51+
}
52+
53+
extension ABIv0.EncodedBacktrace.Frame: Codable {}

Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension ABIv0 {
7070
kind = .testCaseStarted
7171
case let .issueRecorded(recordedIssue):
7272
kind = .issueRecorded
73-
issue = EncodedIssue(encoding: recordedIssue)
73+
issue = EncodedIssue(encoding: recordedIssue, in: eventContext)
7474
case .testCaseEnded:
7575
if eventContext.test?.isParameterized == false {
7676
return nil

Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedIssue.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ extension ABIv0 {
2323
var sourceLocation: SourceLocation?
2424

2525
/// The backtrace where this issue occurred, if available.
26-
var _backtrace: [Backtrace.Address]?
26+
var _backtrace: EncodedBacktrace?
2727

28-
init(encoding issue: borrowing Issue) {
28+
init(encoding issue: borrowing Issue, in eventContext: borrowing Event.Context) {
2929
isKnown = issue.isKnown
3030
sourceLocation = issue.sourceLocation
31-
_backtrace = issue.sourceContext.backtrace.map(\.addresses)
31+
if let backtrace = issue.sourceContext.backtrace {
32+
_backtrace = EncodedBacktrace(encoding: backtrace, in: eventContext)
33+
}
3234
}
3335
}
3436
}

Sources/Testing/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_library(Testing
1313
ABI/v0/ABIv0.Record.swift
1414
ABI/v0/ABIv0.Record+Streaming.swift
1515
ABI/v0/ABIv0.swift
16+
ABI/v0/Encoded/ABIv0.EncodedBacktrace.swift
1617
ABI/v0/Encoded/ABIv0.EncodedEvent.swift
1718
ABI/v0/Encoded/ABIv0.EncodedInstant.swift
1819
ABI/v0/Encoded/ABIv0.EncodedIssue.swift
@@ -50,6 +51,7 @@ add_library(Testing
5051
Running/Runner.swift
5152
Running/SkipInfo.swift
5253
SourceAttribution/Backtrace.swift
54+
SourceAttribution/Backtrace+Symbolication.swift
5355
SourceAttribution/CustomTestStringConvertible.swift
5456
SourceAttribution/Expression.swift
5557
SourceAttribution/Expression+Macro.swift

Sources/Testing/Running/Configuration.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ public struct Configuration: Sendable {
2020
/// Whether or not to parallelize the execution of tests and test cases.
2121
public var isParallelizationEnabled = true
2222

23+
/// How to symbolicate backtraces captured during a test run.
24+
///
25+
/// If the value of this property is not `nil`, symbolication will be
26+
/// performed automatically when a backtrace is encoded into an event stream.
27+
///
28+
/// The value of this property does not affect event handlers implemented in
29+
/// Swift in-process. When handling a backtrace in Swift, use its
30+
/// ``Backtrace/symbolicate(_:)`` function to symbolicate it.
31+
public var backtraceSymbolicationMode: Backtrace.SymbolicationMode?
32+
2333
/// A type describing whether or not, and how, to iterate a test plan
2434
/// repeatedly.
2535
///
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
private import _TestingInternals
12+
13+
/// A type representing a backtrace or stack trace.
14+
@_spi(ForToolsIntegrationOnly)
15+
extension Backtrace {
16+
/// Demangle a symbol name.
17+
///
18+
/// - Parameters:
19+
/// - mangledSymbolName: The symbol name to demangle.
20+
///
21+
/// - Returns: The demangled form of `mangledSymbolName` according to the
22+
/// Swift standard library or the platform's C++ standard library, or `nil`
23+
/// if the symbol name could not be demangled.
24+
private static func _demangle(_ mangledSymbolName: String) -> String? {
25+
guard let demangledSymbolName = swt_copyDemangledSymbolName(mangledSymbolName) else {
26+
return nil
27+
}
28+
defer {
29+
free(demangledSymbolName)
30+
}
31+
return String(validatingCString: demangledSymbolName)
32+
}
33+
34+
/// Symbolicate a sequence of addresses.
35+
///
36+
/// - Parameters:
37+
/// - addresses: The sequence of addresses. These addresses must have
38+
/// originated in the current process.
39+
/// - mode: How to symbolicate the addresses in the backtrace.
40+
///
41+
/// - Returns: An array of strings representing the names of symbols in
42+
/// `addresses`.
43+
///
44+
/// If an address in `addresses` cannot be symbolicated, it is converted to a
45+
/// string using ``Swift/String/init(describingForTest:)``.
46+
private static func _symbolicate(addresses: UnsafeBufferPointer<UnsafeRawPointer?>, mode: SymbolicationMode) -> [String] {
47+
let count = addresses.count
48+
var symbolNames = [(String, displacement: UInt)?](repeating: nil, count: count)
49+
50+
#if SWT_TARGET_OS_APPLE
51+
for (i, address) in addresses.enumerated() {
52+
guard let address else {
53+
continue
54+
}
55+
var info = Dl_info()
56+
if 0 != dladdr(address, &info) {
57+
let displacement = UInt(bitPattern: address) - UInt(bitPattern: info.dli_saddr)
58+
if var symbolName = info.dli_sname.flatMap(String.init(validatingCString:)) {
59+
if mode != .mangled {
60+
symbolName = _demangle(symbolName) ?? symbolName
61+
}
62+
symbolNames[i] = (symbolName, displacement)
63+
}
64+
}
65+
}
66+
#elseif os(Linux)
67+
// Although Linux has dladdr(), it does not have symbol names from ELF
68+
// binaries by default. The standard library's backtracing functionality has
69+
// implemented sufficient ELF/DWARF parsing to be able to symbolicate Linux
70+
// backtraces. TODO: adopt the standard library's Backtrace on Linux
71+
// Note that this means on Linux we don't have demangling capability (since
72+
// we don't have the mangled symbol names in the first place) so this code
73+
// does not check the mode argument.
74+
#elseif os(Windows)
75+
_withDbgHelpLibrary { hProcess in
76+
guard let hProcess else {
77+
return
78+
}
79+
for (i, address) in addresses.enumerated() {
80+
guard let address else {
81+
continue
82+
}
83+
84+
withUnsafeTemporaryAllocation(of: SYMBOL_INFO_PACKAGEW.self, capacity: 1) { symbolInfo in
85+
let symbolInfo = symbolInfo.baseAddress!
86+
symbolInfo.pointee.si.SizeOfStruct = ULONG(MemoryLayout<SYMBOL_INFOW>.stride)
87+
symbolInfo.pointee.si.MaxNameLen = ULONG(MAX_SYM_NAME)
88+
var displacement = DWORD64(0)
89+
if SymFromAddrW(hProcess, DWORD64(clamping: UInt(bitPattern: address)), &displacement, symbolInfo.pointer(to: \.si)!),
90+
var symbolName = String.decodeCString(symbolInfo.pointer(to: \.si.Name)!, as: UTF16.self)?.result {
91+
if mode != .mangled {
92+
symbolName = _demangle(symbolName) ?? symbolName
93+
}
94+
symbolNames[i] = (symbolName, UInt(clamping: displacement))
95+
}
96+
}
97+
}
98+
}
99+
#elseif os(WASI)
100+
// WASI does not currently support backtracing let alone symbolication.
101+
#else
102+
#warning("Platform-specific implementation missing: backtrace symbolication unavailable")
103+
#endif
104+
105+
var result = [String]()
106+
result.reserveCapacity(count)
107+
for (i, address) in addresses.enumerated() {
108+
let formatted = if let (symbolName, displacement) = symbolNames[i] {
109+
if mode == .preciseDemangled {
110+
"\(i) \(symbolName) (\(String(describingForTest: address))+\(displacement))"
111+
} else {
112+
symbolName
113+
}
114+
} else {
115+
String(describingForTest: address)
116+
}
117+
result.append(formatted)
118+
}
119+
return result
120+
}
121+
122+
/// An enumeration describing the symbolication mode to use when handling
123+
/// events containing backtraces.
124+
public enum SymbolicationMode: Sendable {
125+
/// The backtrace should be symbolicated, but no demangling should be
126+
/// performed.
127+
case mangled
128+
129+
/// The backtrace should be symbolicated and Swift and C++ symbols should be
130+
/// demangled if possible.
131+
case demangled
132+
133+
/// The backtrace should be symbolicated, Swift and C++ symbols should be
134+
/// demangled if possible, and precise symbol addresses and offsets should
135+
/// be provided if available.
136+
case preciseDemangled
137+
}
138+
139+
/// Symbolicate the addresses in this backtrace.
140+
///
141+
/// - Parameters:
142+
/// - mode: How to symbolicate the addresses in the backtrace.
143+
///
144+
/// - Returns: An array of strings representing the names of symbols in
145+
/// `addresses`.
146+
///
147+
/// If an address in `addresses` cannot be symbolicated, it is converted to a
148+
/// string using ``Swift/String/init(describingForTest:)``.
149+
public func symbolicate(_ mode: SymbolicationMode) -> [String] {
150+
#if _pointerBitWidth(_64)
151+
// The width of a pointer equals the width of an `Address`, so we can just
152+
// bitcast the memory rather than mapping through UInt first.
153+
addresses.withUnsafeBufferPointer { addresses in
154+
addresses.withMemoryRebound(to: UnsafeRawPointer?.self) { addresses in
155+
Self._symbolicate(addresses: addresses, mode: mode)
156+
}
157+
}
158+
#else
159+
let addresses = addresses.map { UnsafeRawPointer(bitPattern: UInt($0)) }
160+
return addresses.withUnsafeBufferPointer { addresses in
161+
Self._symbolicate(addresses: addresses, mode: mode)
162+
}
163+
#endif
164+
}
165+
}
166+
167+
#if os(Windows)
168+
// MARK: -
169+
170+
/// Configure the environment to allow calling into the Debug Help library.
171+
///
172+
/// - Parameters:
173+
/// - body: A function to invoke. A process handle valid for use with Debug
174+
/// Help functions is passed in, or `nullptr` if the Debug Help library
175+
/// could not be initialized.
176+
/// - context: An arbitrary pointer to pass to `body`.
177+
///
178+
/// On Windows, the Debug Help library (DbgHelp.lib) is not thread-safe. All
179+
/// calls into it from the Swift runtime and stdlib should route through this
180+
/// function.
181+
private func _withDbgHelpLibrary(_ body: (HANDLE?) -> Void) {
182+
withoutActuallyEscaping(body) { body in
183+
withUnsafePointer(to: body) { context in
184+
_swift_win32_withDbgHelpLibrary({ hProcess, context in
185+
let body = context!.load(as: ((HANDLE?) -> Void).self)
186+
body(hProcess)
187+
}, .init(mutating: context))
188+
}
189+
}
190+
}
191+
#endif

Sources/_TestingInternals/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ set(CMAKE_CXX_SCAN_FOR_MODULES 0)
1111
include(LibraryVersion)
1212
include(TargetTriple)
1313
add_library(_TestingInternals STATIC
14+
Demangle.cpp
1415
Discovery.cpp
1516
Versions.cpp
1617
WillThrow.cpp)

0 commit comments

Comments
 (0)