Skip to content

Commit dab00ae

Browse files
authored
Optionally symbolicate backtraces. (#676)
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. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent cca6de2 commit dab00ae

File tree

12 files changed

+339
-6
lines changed

12 files changed

+339
-6
lines changed

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 22 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,19 @@ 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+
default:
445+
throw _EntryPointError.invalidArgument("--symbolicate-backtraces", value: symbolicateBacktraces)
446+
447+
}
448+
}
449+
428450
#if !SWT_NO_FILE_IO
429451
// XML output
430452
if let xunitOutputPath = args.xunitOutput {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
/// The frames in the backtrace.
20+
var symbolicatedAddresses: [Backtrace.SymbolicatedAddress]
21+
22+
init(encoding backtrace: borrowing Backtrace, in eventContext: borrowing Event.Context) {
23+
if let symbolicationMode = eventContext.configuration?.backtraceSymbolicationMode {
24+
symbolicatedAddresses = backtrace.symbolicate(symbolicationMode)
25+
} else {
26+
symbolicatedAddresses = backtrace.addresses.map { Backtrace.SymbolicatedAddress(address: $0) }
27+
}
28+
}
29+
}
30+
}
31+
32+
// MARK: - Codable
33+
34+
extension ABIv0.EncodedBacktrace: Codable {
35+
func encode(to encoder: any Encoder) throws {
36+
try symbolicatedAddresses.encode(to: encoder)
37+
}
38+
39+
init(from decoder: any Decoder) throws {
40+
self.symbolicatedAddresses = try [Backtrace.SymbolicatedAddress](from: decoder)
41+
}
42+
}

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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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(Experimental) @_spi(ForToolsIntegrationOnly)
15+
extension Backtrace {
16+
/// An enumeration describing the symbolication mode to use when handling
17+
/// events containing backtraces.
18+
public enum SymbolicationMode: Sendable {
19+
/// The backtrace should be symbolicated, but no demangling should be
20+
/// performed.
21+
case mangled
22+
23+
/// The backtrace should be symbolicated and Swift symbols should be
24+
/// demangled if possible.
25+
///
26+
/// "Foreign" symbol names such as those produced by C++ are not demangled.
27+
case demangled
28+
}
29+
30+
/// A type representing an instance of ``Backtrace/Address`` that has been
31+
/// symbolicated by a call to ``Backtrace/symbolicate(_:)``.
32+
public struct SymbolicatedAddress: Sendable {
33+
/// The (unsymbolicated) address from the backtrace.
34+
public var address: Address
35+
36+
/// The offset of ``address`` from the start of the corresponding function,
37+
/// if available.
38+
///
39+
/// If ``address`` could not be resolved to a symbol, the value of this
40+
/// property is `nil`.
41+
public var offset: UInt64?
42+
43+
/// The name of the symbol at ``address``, if available.
44+
///
45+
/// If ``address`` could not be resolved to a symbol, the value of this
46+
/// property is `nil`.
47+
public var symbolName: String?
48+
}
49+
50+
/// Symbolicate the addresses in this backtrace.
51+
///
52+
/// - Parameters:
53+
/// - mode: How to symbolicate the addresses in the backtrace.
54+
///
55+
/// - Returns: An array of strings representing the names of symbols in
56+
/// `addresses`.
57+
///
58+
/// If an address in `addresses` cannot be symbolicated, the corresponding
59+
/// instance of ``SymbolicatedAddress`` in the resulting array has a `nil`
60+
/// value for its ``Backtrace/SymbolicatedAddress/symbolName`` property.
61+
public func symbolicate(_ mode: SymbolicationMode) -> [SymbolicatedAddress] {
62+
var result = addresses.map { SymbolicatedAddress(address: $0) }
63+
64+
#if SWT_TARGET_OS_APPLE
65+
for (i, address) in addresses.enumerated() {
66+
var info = Dl_info()
67+
if 0 != dladdr(UnsafePointer(bitPattern: UInt(clamping: address)), &info) {
68+
let offset = address - Address(clamping: UInt(bitPattern: info.dli_saddr))
69+
let symbolName = info.dli_sname.flatMap(String.init(validatingCString:))
70+
result[i] = SymbolicatedAddress(address: address, offset: offset, symbolName: symbolName)
71+
}
72+
}
73+
#elseif os(Linux)
74+
// Although Linux has dladdr(), it does not have symbol names from ELF
75+
// binaries by default. The standard library's backtracing functionality has
76+
// implemented sufficient ELF/DWARF parsing to be able to symbolicate Linux
77+
// backtraces. TODO: adopt the standard library's Backtrace on Linux
78+
// Note that this means on Linux we don't have demangling capability (since
79+
// we don't have the mangled symbol names in the first place) so this code
80+
// does not check the mode argument.
81+
#elseif os(Windows)
82+
_withDbgHelpLibrary { hProcess in
83+
guard let hProcess else {
84+
return
85+
}
86+
for (i, address) in addresses.enumerated() {
87+
withUnsafeTemporaryAllocation(of: SYMBOL_INFO_PACKAGEW.self, capacity: 1) { symbolInfo in
88+
let symbolInfo = symbolInfo.baseAddress!
89+
symbolInfo.pointee.si.SizeOfStruct = ULONG(MemoryLayout<SYMBOL_INFOW>.stride)
90+
symbolInfo.pointee.si.MaxNameLen = ULONG(MAX_SYM_NAME)
91+
var displacement = DWORD64(0)
92+
if SymFromAddrW(hProcess, DWORD64(clamping: address), &displacement, symbolInfo.pointer(to: \.si)!) {
93+
let symbolName = String.decodeCString(symbolInfo.pointer(to: \.si.Name)!, as: UTF16.self)?.result
94+
result[i] = SymbolicatedAddress(address: address, offset: displacement, symbolName: symbolName)
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+
if mode != .mangled {
106+
result = result.map { symbolicatedAddress in
107+
var symbolicatedAddress = symbolicatedAddress
108+
if let demangledName = symbolicatedAddress.symbolName.flatMap(_demangle) {
109+
symbolicatedAddress.symbolName = demangledName
110+
}
111+
return symbolicatedAddress
112+
}
113+
}
114+
115+
return result
116+
}
117+
}
118+
119+
// MARK: - Codable
120+
121+
extension Backtrace.SymbolicatedAddress: Codable {}
122+
123+
// MARK: - Swift runtime wrappers
124+
125+
/// Demangle a symbol name.
126+
///
127+
/// - Parameters:
128+
/// - mangledSymbolName: The symbol name to demangle.
129+
///
130+
/// - Returns: The demangled form of `mangledSymbolName` according to the
131+
/// Swift standard library or the platform's C++ standard library, or `nil`
132+
/// if the symbol name could not be demangled.
133+
private func _demangle(_ mangledSymbolName: String) -> String? {
134+
mangledSymbolName.withCString { mangledSymbolName in
135+
guard let demangledSymbolName = swift_demangle(mangledSymbolName, strlen(mangledSymbolName), nil, nil, 0) else {
136+
return nil
137+
}
138+
defer {
139+
free(demangledSymbolName)
140+
}
141+
return String(validatingCString: demangledSymbolName)
142+
}
143+
}
144+
145+
#if os(Windows)
146+
/// Configure the environment to allow calling into the Debug Help library.
147+
///
148+
/// - Parameters:
149+
/// - body: A function to invoke. A process handle valid for use with Debug
150+
/// Help functions is passed in, or `nullptr` if the Debug Help library
151+
/// could not be initialized.
152+
/// - context: An arbitrary pointer to pass to `body`.
153+
///
154+
/// On Windows, the Debug Help library (DbgHelp.lib) is not thread-safe. All
155+
/// calls into it from the Swift runtime and stdlib should route through this
156+
/// function.
157+
private func _withDbgHelpLibrary(_ body: (HANDLE?) -> Void) {
158+
withoutActuallyEscaping(body) { body in
159+
withUnsafePointer(to: body) { context in
160+
_swift_win32_withDbgHelpLibrary({ hProcess, context in
161+
let body = context!.load(as: ((HANDLE?) -> Void).self)
162+
body(hProcess)
163+
}, .init(mutating: context))
164+
}
165+
}
166+
}
167+
#endif
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
#if !defined(SWT_DEMANGLE_H)
12+
#define SWT_DEMANGLE_H
13+
14+
#include "Defines.h"
15+
#include "Includes.h"
16+
17+
SWT_ASSUME_NONNULL_BEGIN
18+
19+
/// Demangle a Swift symbol name.
20+
///
21+
/// - Parameters:
22+
/// - mangledName: A pointer to the mangled symbol name to demangle.
23+
/// - mangledNameLength: The length of `mangledName` in bytes, not including
24+
/// any trailing null byte.
25+
/// - outputBuffer: Unused by the testing library. Pass `nullptr`.
26+
/// - outputBufferSize: Unused by the testing library. Pass `nullptr`.
27+
/// - flags: Unused by the testing library. Pass `0`.
28+
///
29+
/// - Returns: The demangled form of `mangledName`, or `nullptr` if demangling
30+
/// failed. The caller is responsible for freeing this string with `free()`
31+
/// when done.
32+
SWT_IMPORT_FROM_STDLIB char *_Nullable swift_demangle(
33+
const char *mangledName,
34+
size_t mangledNameLength,
35+
char *_Nullable outputBuffer,
36+
size_t *_Nullable outputBufferSize,
37+
uint32_t flags
38+
);
39+
40+
#if defined(_WIN32)
41+
/// Configure the environment to allow calling into the Debug Help library.
42+
///
43+
/// - Parameters:
44+
/// - body: A function to invoke. A process handle valid for use with Debug
45+
/// Help functions is passed in, or `nullptr` if the Debug Help library
46+
/// could not be initialized.
47+
/// - context: An arbitrary pointer to pass to `body`.
48+
///
49+
/// On Windows, the Debug Help library (DbgHelp.lib) is not thread-safe. All
50+
/// calls into it from the Swift runtime and stdlib should route through this
51+
/// function.
52+
SWT_IMPORT_FROM_STDLIB void _swift_win32_withDbgHelpLibrary(
53+
void (* body)(HANDLE _Nullable hProcess, void *_Null_unspecified context),
54+
void *_Null_unspecified context
55+
);
56+
#endif
57+
58+
SWT_ASSUME_NONNULL_END
59+
60+
#endif

Sources/_TestingInternals/include/Discovery.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
#define SWT_DISCOVERY_H
1313

1414
#include "Defines.h"
15-
16-
#include <stdbool.h>
15+
#include "Includes.h"
1716

1817
SWT_ASSUME_NONNULL_BEGIN
1918

0 commit comments

Comments
 (0)