Skip to content

Commit 46d74fd

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 a0c8fc6 commit 46d74fd

17 files changed

+470
-14
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/Events/Event.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public struct Event: Sendable {
203203
// Create both the event and its associated context here at same point, to
204204
// ensure their task local-derived values are the same.
205205
let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant)
206-
let context = Event.Context(test: test, testCase: testCase)
206+
let context = Event.Context(test: test, testCase: testCase, configuration: nil)
207207
event._post(in: context, configuration: configuration)
208208
}
209209
}
@@ -239,16 +239,24 @@ extension Event {
239239
/// functions), the value of this property is `nil`.
240240
public var testCase: Test.Case?
241241

242+
/// The configuration handling the corresponding event, if any.
243+
///
244+
/// The value of this property is a copy of the configuration that owns the
245+
/// currently-running event handler; to avoid reference cycles, the
246+
/// ``Configuration/eventHandler`` property of this instance is cleared.
247+
public var configuration: Configuration?
248+
242249
/// Initialize a new instance of this type.
243250
///
244251
/// - Parameters:
245252
/// - test: The test for which this instance's associated event occurred,
246253
/// if any.
247254
/// - testCase: The test case for which this instance's associated event
248255
/// occurred, if any.
249-
init(test: Test? = .current, testCase: Test.Case? = .current) {
256+
init(test: Test?, testCase: Test.Case?, configuration: Configuration?) {
250257
self.test = test
251258
self.testCase = testCase
259+
self.configuration = configuration
252260
}
253261
}
254262

Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ extension Event.HumanReadableOutputRecorder {
230230
"«unknown»"
231231
}
232232
let instant = event.instant
233-
let iterationCount = Configuration.current?.repetitionPolicy.maximumIterationCount
233+
let iterationCount = eventContext.configuration?.repetitionPolicy.maximumIterationCount
234234

235235
// First, make any updates to the context/state associated with this
236236
// recorder.

Sources/Testing/Running/Configuration+EventHandling.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ extension Configuration {
2020
/// `eventHandler` but this method may also be used as a customization point
2121
/// to change how the event is passed to the event handler.
2222
func handleEvent(_ event: borrowing Event, in context: borrowing Event.Context) {
23-
eventHandler(event, context)
23+
var contextCopy = copy context
24+
contextCopy.configuration = self
25+
contextCopy.configuration?.eventHandler = { _, _ in }
26+
eventHandler(event, contextCopy)
2427
}
2528
}

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

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,12 @@ extension Test.Case {
222222
return try await Runner.RuntimeState.$current.withValue(runtimeState, operation: body)
223223
}
224224
}
225+
226+
// MARK: - Event.Context convenience initializer
227+
228+
extension Event.Context {
229+
init() {
230+
let state = Runner.RuntimeState.current
231+
self.init(test: state?.test, testCase: state?.testCase, configuration: state?.configuration)
232+
}
233+
}

0 commit comments

Comments
 (0)