Skip to content

Commit 62d1c28

Browse files
Extend existing Snapshot types and introduce new ones for Event.Context (#321)
This PR extends some of the existing Snapshot types (mostly introduces new computed properties from their "original" types) and introduces a new one for `Event.Context` ### Motivation: Components outside the test process itself sometimes need access to (a serialized form) of the event context (ie. the test and test case information). ### Modifications: I added a new Snapshot type for `Event.Context`, extended various other existing snapshot types to have more properties (to be more in line with their "original" types), and changed the initializers of existing Snapshot types to `borrow` their argument (where possible). ### Result: `Event.Context` has a serializable variant, and snapshot types are generally a bit more similar to the types they represent. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [ ] If public symbols are renamed or modified, DocC references should be updated.
1 parent c7d3dda commit 62d1c28

File tree

10 files changed

+220
-17
lines changed

10 files changed

+220
-17
lines changed

Sources/Testing/Events/Event.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ extension Event {
278278

279279
extension Event {
280280
/// A serializable event that occurred during testing.
281+
@_spi(ForToolsIntegrationOnly)
281282
public struct Snapshot: Sendable, Codable {
282283

283284
/// The kind of event.
@@ -447,3 +448,36 @@ extension Event.Kind {
447448
}
448449
}
449450
}
451+
452+
extension Event.Context {
453+
454+
/// A serializable type which provides context about a posted ``Event``.
455+
///
456+
@_spi(ForToolsIntegrationOnly)
457+
public struct Snapshot: Sendable, Codable {
458+
/// A snapshot of the test for which this instance's associated ``Event``
459+
/// occurred, if any.
460+
///
461+
/// If an event occurred independently of any test, or if the running test
462+
/// cannot be determined, the value of this property is `nil`.
463+
public var test: Test.Snapshot?
464+
465+
/// A snapshot of the test case for which this instance's associated
466+
/// ``Event`` occurred, if any.
467+
///
468+
/// The test case indicates which element in the iterated sequence is
469+
/// associated with this event. For non-parameterized tests, a single test
470+
/// case is synthesized. For test suite types (as opposed to test
471+
/// functions), the value of this property is `nil`.
472+
public var testCase: Test.Case.Snapshot?
473+
474+
/// Initialize a new instance of this type.
475+
///
476+
/// - Parameters:
477+
/// - context: The context to snapshot.
478+
public init(snapshotting context: borrowing Event.Context) {
479+
test = context.test.map { Test.Snapshot(snapshotting: $0) }
480+
testCase = context.testCase.map { Test.Case.Snapshot(snapshotting: $0) }
481+
}
482+
}
483+
}

Sources/Testing/Expectations/Expectation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ extension Expectation {
9191

9292
/// Creates a snapshot expectation from a real ``Expectation``.
9393
/// - Parameter expectation: The real expectation.
94-
public init(snapshotting expectation: Expectation) {
94+
public init(snapshotting expectation: borrowing Expectation) {
9595
self.evaluatedExpression = expectation.evaluatedExpression
9696
self.mismatchedErrorDescription = expectation.mismatchedErrorDescription
9797
self.differenceDescription = expectation.differenceDescription

Sources/Testing/Issues/Issue.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ extension Issue {
200200
/// Initialize an issue instance with the specified details.
201201
///
202202
/// - Parameter issue: The original issue that gets snapshotted.
203-
public init(snapshotting issue: Issue) {
203+
public init(snapshotting issue: borrowing Issue) {
204204
self.kind = Issue.Kind.Snapshot(snapshotting: issue.kind)
205205
self.comments = issue.comments
206206
self.sourceContext = issue.sourceContext
@@ -217,6 +217,16 @@ extension Issue {
217217
}
218218
return nil
219219
}
220+
221+
/// The location in source where this issue occurred, if available.
222+
public var sourceLocation: SourceLocation? {
223+
get {
224+
sourceContext.sourceLocation
225+
}
226+
set {
227+
sourceContext.sourceLocation = newValue
228+
}
229+
}
220230
}
221231
}
222232

Sources/Testing/Parameterization/Test.Case.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,17 @@ extension Test.Case {
172172
/// The arguments passed to this test case.
173173
public var arguments: [Argument.Snapshot]
174174

175+
/// Whether or not this test case is from a parameterized test.
176+
public var isParameterized: Bool {
177+
!arguments.isEmpty
178+
}
179+
175180
/// Initialize an instance of this type by snapshotting the specified test
176181
/// case.
177182
///
178183
/// - Parameters:
179184
/// - testCase: The original test case to snapshot.
180-
init(snapshotting testCase: Test.Case) {
185+
public init(snapshotting testCase: borrowing Test.Case) {
181186
id = testCase.id
182187
arguments = testCase.arguments.map(Test.Case.Argument.Snapshot.init)
183188
}
@@ -204,7 +209,7 @@ extension Test.Case.Argument {
204209
///
205210
/// - Parameters:
206211
/// - argument: The original test case argument to snapshot.
207-
init(snapshotting argument: Test.Case.Argument) {
212+
public init(snapshotting argument: Test.Case.Argument) {
208213
id = argument.id
209214
value = Expression.Value(reflecting: argument.value)
210215
parameter = argument.parameter

Sources/Testing/Running/EntryPoint.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,8 @@ struct EventAndContextSnapshot {
282282
/// A snapshot of the event.
283283
var event: Event.Snapshot
284284

285-
/// A snapshot of the test associated with the event, if any.
286-
var test: Test.Snapshot?
287-
288-
/// A snapshot of the test case associated with the event, if any.
289-
var testCase: Test.Case.Snapshot?
285+
/// A snapshot of the event context.
286+
var eventContext: Event.Context.Snapshot
290287
}
291288

292289
extension EventAndContextSnapshot: Codable {}
@@ -323,8 +320,7 @@ private func _eventHandlerForStreamingEvents(toFileAtPath path: String) throws -
323320
return { event, context in
324321
let snapshot = EventAndContextSnapshot(
325322
event: Event.Snapshot(snapshotting: event),
326-
test: context.test.map { Test.Snapshot(snapshotting: $0) },
327-
testCase: context.testCase.map { Test.Case.Snapshot(snapshotting: $0) }
323+
eventContext: Event.Context.Snapshot(snapshotting: context)
328324
)
329325
if var snapshotJSON = try? JSONEncoder().encode(snapshot) {
330326
func isASCIINewline(_ byte: UInt8) -> Bool {

Sources/Testing/Running/Runner.Plan.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ extension Runner.Plan {
329329
///
330330
/// - Parameters:
331331
/// - plan: The original plan to snapshot.
332-
public init(snapshotting plan: Runner.Plan) {
332+
public init(snapshotting plan: borrowing Runner.Plan) {
333333
plan.stepGraph.forEach { keyPath, step in
334334
let step = step.map(Step.Snapshot.init(snapshotting:))
335335
_stepGraph.insertValue(step, at: keyPath)
@@ -394,7 +394,7 @@ extension Runner.Plan.Step {
394394
///
395395
/// - Parameters:
396396
/// - step: The original step to snapshot.
397-
init(snapshotting step: Runner.Plan.Step) {
397+
public init(snapshotting step: borrowing Runner.Plan.Step) {
398398
test = Test.Snapshot(snapshotting: step.test)
399399
action = Runner.Plan.Action.Snapshot(snapshotting: step.action)
400400
}
@@ -431,7 +431,7 @@ extension Runner.Plan.Action {
431431
///
432432
/// - Parameters:
433433
/// - action: The original action to snapshot.
434-
init(snapshotting action: Runner.Plan.Action) {
434+
public init(snapshotting action: Runner.Plan.Action) {
435435
self = switch action {
436436
case let .run(options):
437437
.run(options: options)

Sources/Testing/Test.swift

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,20 @@ extension Test {
177177
/// A serializable snapshot of a ``Test`` instance.
178178
@_spi(ForToolsIntegrationOnly)
179179
public struct Snapshot: Sendable, Codable, Identifiable {
180+
181+
private enum CodingKeys: String, CodingKey {
182+
case id
183+
case name
184+
case displayName
185+
case sourceLocation
186+
case testCases
187+
case parameters
188+
case comments
189+
case tags
190+
case associatedBugs
191+
case _timeLimit = "timeLimit"
192+
}
193+
180194
/// The ID of this test.
181195
public var id: Test.ID
182196

@@ -190,8 +204,6 @@ extension Test {
190204
/// The customized display name of this test, if any.
191205
public var displayName: String?
192206

193-
// FIXME: Include traits as well.
194-
195207
/// The source location of this test.
196208
public var sourceLocation: SourceLocation
197209

@@ -210,17 +222,44 @@ extension Test {
210222
/// - ``Test/parameters``
211223
public var parameters: [Parameter]?
212224

225+
/// The complete set of comments about this test from all of its traits.
226+
public var comments: [Comment]
227+
228+
/// The complete, unique set of tags associated with this test.
229+
public var tags: Set<Tag>
230+
231+
/// The set of bugs associated with this test.
232+
///
233+
/// For information on how to associate a bug with a test, see the
234+
/// documentation for ``Bug``.
235+
public var associatedBugs: [Bug]
236+
237+
// Backing storage for ``Test/Snapshot/timeLimit``.
238+
private var _timeLimit: TimeValue?
239+
240+
/// The maximum amount of time a test may run for before timing out.
241+
@available(_clockAPI, *)
242+
public var timeLimit: Duration? {
243+
_timeLimit.map(Duration.init)
244+
}
245+
213246
/// Initialize an instance of this type by snapshotting the specified test.
214247
///
215248
/// - Parameters:
216249
/// - test: The original test to snapshot.
217-
public init(snapshotting test: Test) {
250+
public init(snapshotting test: borrowing Test) {
218251
id = test.id
219252
name = test.name
220253
displayName = test.displayName
221254
sourceLocation = test.sourceLocation
222255
testCases = test.testCases?.map(Test.Case.Snapshot.init)
223256
parameters = test.parameters
257+
comments = test.comments
258+
tags = test.tags
259+
associatedBugs = test.associatedBugs
260+
if #available(_clockAPI, *) {
261+
_timeLimit = test.timeLimit.map(TimeValue.init)
262+
}
224263
}
225264

226265
/// Whether or not this test is parameterized.

Tests/TestingTests/EventTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,17 @@ struct EventTests {
6767

6868
#expect(String(describing: decoded) == String(describing: eventSnapshot))
6969
}
70+
71+
@Test("Event.Contexts's Codable Conformances")
72+
func codable() async throws {
73+
let eventContext = Event.Context()
74+
let snapshot = Event.Context.Snapshot(snapshotting: eventContext)
75+
76+
let encoded = try JSONEncoder().encode(snapshot)
77+
let decoded = try JSONDecoder().decode(Event.Context.Snapshot.self, from: encoded)
78+
79+
#expect(String(describing: decoded.test) == String(describing: eventContext.test.map(Test.Snapshot.init(snapshotting:))))
80+
#expect(String(describing: decoded.testCase) == String(describing: eventContext.testCase.map(Test.Case.Snapshot.init(snapshotting:))))
81+
}
7082
#endif
7183
}

Tests/TestingTests/IssueTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,4 +1422,63 @@ struct IssueCodingTests {
14221422
let errorSnapshot = try #require(issueSnapshot.error)
14231423
#expect(String(describing: errorSnapshot) == String(describing: underlyingError))
14241424
}
1425+
1426+
@Test func sourceLocationPropertyGetter() throws {
1427+
let sourceLocation = SourceLocation(
1428+
fileID: "fileID",
1429+
filePath: "filePath",
1430+
line: 13,
1431+
column: 42
1432+
)
1433+
1434+
let sourceContext = SourceContext(
1435+
backtrace: Backtrace(addresses: [13, 42]),
1436+
sourceLocation: sourceLocation
1437+
)
1438+
1439+
let issue = Issue(
1440+
kind: .apiMisused,
1441+
comments: [],
1442+
sourceContext: sourceContext
1443+
)
1444+
1445+
let issueSnapshot = Issue.Snapshot(snapshotting: issue)
1446+
#expect(issueSnapshot.sourceContext == sourceContext)
1447+
#expect(issueSnapshot.sourceLocation == sourceLocation)
1448+
}
1449+
1450+
@Test func sourceLocationPropertySetter() throws {
1451+
let initialSourceLocation = SourceLocation(
1452+
fileID: "fileID",
1453+
filePath: "filePath",
1454+
line: 13,
1455+
column: 42
1456+
)
1457+
1458+
let sourceContext = SourceContext(
1459+
backtrace: Backtrace(addresses: [13, 42]),
1460+
sourceLocation: initialSourceLocation
1461+
)
1462+
1463+
let issue = Issue(
1464+
kind: .apiMisused,
1465+
comments: [],
1466+
sourceContext: sourceContext
1467+
)
1468+
1469+
let updatedSourceLocation = SourceLocation(
1470+
fileID: "fileID2",
1471+
filePath: "filePath2",
1472+
line: 14,
1473+
column: 43
1474+
)
1475+
1476+
var issueSnapshot = Issue.Snapshot(snapshotting: issue)
1477+
issueSnapshot.sourceLocation = updatedSourceLocation
1478+
1479+
#expect(issueSnapshot.sourceContext != sourceContext)
1480+
#expect(issueSnapshot.sourceLocation != initialSourceLocation)
1481+
#expect(issueSnapshot.sourceLocation == updatedSourceLocation)
1482+
#expect(issueSnapshot.sourceContext.sourceLocation == updatedSourceLocation)
1483+
}
14251484
}

Tests/TestingTests/Test.SnapshotTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,52 @@ struct Test_SnapshotTests {
6969
#expect(snapshot.isSuite)
7070
}
7171
}
72+
73+
/// This is a comment that should show up in the test's `comments` property.
74+
@Test("comments property")
75+
func comments() async throws {
76+
let test = try #require(Test.current)
77+
let snapshot = Test.Snapshot(snapshotting: test)
78+
79+
#expect(!snapshot.comments.isEmpty)
80+
#expect(snapshot.comments == test.comments)
81+
}
82+
83+
@Test("tags property", .tags(Tag.testTag))
84+
func tags() async throws {
85+
let test = try #require(Test.current)
86+
let snapshot = Test.Snapshot(snapshotting: test)
87+
88+
#expect(snapshot.tags.count == 1)
89+
#expect(snapshot.tags.first == Tag.testTag)
90+
}
91+
92+
@Test("associatedBugs property", bug)
93+
func associatedBugs() async throws {
94+
let test = try #require(Test.current)
95+
let snapshot = Test.Snapshot(snapshotting: test)
96+
97+
#expect(snapshot.associatedBugs.count == 1)
98+
#expect(snapshot.associatedBugs.first == Self.bug)
99+
}
100+
101+
private static let bug: Bug = Bug.bug(12345, relationship: .failingBecauseOfBug, "Lorem ipsum")
102+
103+
@available(_clockAPI, *)
104+
@Test("timeLimit property", .timeLimit(duration))
105+
func timeLimit() async throws {
106+
let test = try #require(Test.current)
107+
let snapshot = Test.Snapshot(snapshotting: test)
108+
109+
#expect(snapshot.timeLimit == Self.duration)
110+
}
111+
112+
@available(_clockAPI, *)
113+
private static var duration: Duration {
114+
.seconds(999_999_999)
115+
}
116+
}
117+
118+
extension Tag {
119+
@Tag fileprivate static let testTag: Self
72120
}

0 commit comments

Comments
 (0)