From 463a7a7fedba1fa8a2e3fbe93fe26388dadaedc6 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Tue, 22 Apr 2025 14:00:27 -0500 Subject: [PATCH 1/5] Reserve the 'play' content kind in _TestDiscovery (#1090) This defines the `'play'` test content kind in the `TestContentKind` enum and corresponding documentation. ### Motivation: This is intended to "reserve" this FourCC code for eventual use by the [swift-play-experimental](https://github.com/apple/swift-play-experimental) project, which has adopted the `_TestDiscovery` library from this package. ### 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. Fixes #1089 Fixes rdar://147585572 --- Documentation/ABI/TestContent.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Documentation/ABI/TestContent.md b/Documentation/ABI/TestContent.md index 2d0b9a4b8..c14eb9ec6 100644 --- a/Documentation/ABI/TestContent.md +++ b/Documentation/ABI/TestContent.md @@ -100,9 +100,10 @@ record's kind is a 32-bit unsigned value. The following kinds are defined: | `0x00000000` | – | Reserved (**do not use**) | | `0x74657374` | `'test'` | Test or suite declaration | | `0x65786974` | `'exit'` | Exit test | +| `0x706c6179` | `'play'` | [Playground](https://github.com/apple/swift-play-experimental) | - + If a test content record's `kind` field equals `0x00000000`, the values of all other fields in that record are undefined. From 534d7812d56c5a78ff833c9123d7427308beba24 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 23 Apr 2025 11:32:09 -0500 Subject: [PATCH 2/5] Update CODEOWNERS file (#1094) This PR updates the `CODEOWNERS` file to remove @SeanROlszewski as a code owner. We sincerely thank him for his valuable contributions and past collaboration! ### 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. --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 546238eb3..e2590577e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,4 +8,4 @@ # See https://swift.org/CONTRIBUTORS.txt for Swift project authors # -* @stmontgomery @grynspan @briancroom @SeanROlszewski @suzannaratcliff +* @stmontgomery @grynspan @briancroom @suzannaratcliff From 696d9b9d672461c4812a78446a5223df31a5ef41 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 23 Apr 2025 22:23:44 -0500 Subject: [PATCH 3/5] Fix crash in ConsoleOutputRecorder when an issue has a comment with an empty string (#1091) This fixes a crash which can occur when recording an issue with a comment whose string is empty. It also adds some test coverage of this scenario and other "uncommon" comment examples. ### 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. Fixes rdar://149482060 --- .../Event.ConsoleOutputRecorder.swift | 2 +- Tests/TestingTests/EventRecorderTests.swift | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift index ea48e7ad1..80e68c609 100644 --- a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift @@ -323,7 +323,7 @@ extension Event.ConsoleOutputRecorder { // text instead of just the symbol. Details may be multi-line messages, // so split the message on newlines and indent all lines to align them // to the indentation provided by the symbol. - var lines = message.stringValue.split(whereSeparator: \.isNewline) + var lines = message.stringValue.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) lines = CollectionOfOne(lines[0]) + lines.dropFirst().map { line in "\(padding) \(line)" } diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index 8ac7f6728..690fd416f 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -257,6 +257,30 @@ struct EventRecorderTests { } #endif + @Test( + "Uncommonly-formatted comments", + .bug("rdar://149482060"), + arguments: [ + "", // Empty string + "\n\n\n", // Only newlines + "\nFoo\n\nBar\n\n\nBaz\n", // Newlines interspersed with non-empty strings + ] + ) + func uncommonComments(text: String) async throws { + let stream = Stream() + + var configuration = Configuration() + configuration.eventHandlingOptions.isWarningIssueRecordedEventEnabled = true + let eventRecorder = Event.ConsoleOutputRecorder(writingUsing: stream.write) + configuration.eventHandler = { event, context in + eventRecorder.record(event, in: context) + } + + await Test { + Issue.record(Comment(rawValue: text) /* empty */) + }.run(configuration: configuration) + } + @available(_regexAPI, *) @Test("Issue counts are omitted on a successful test") func issueCountOmittedForPassingTest() async throws { From cacb295938a1794812ecdf771b78f0cde2177dd1 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 23 Apr 2025 22:29:23 -0500 Subject: [PATCH 4/5] Fix build failure when building with pre-6.2 toolchain due to unrecognized `unsafe` keyword (#1093) This fixes another bit of fallout from #1069 when building this project's test targets with a 6.1 (or any pre-6.2) toolchain. The `unsafe` keyword was introduced in 6.2 as part of [SE-0458: Opt-in Strict Memory Safety Checking](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md). Older toolchains are not aware of it, so the fix is to avoid emitting expressions involving that keyword when the macro plugin has been built using an older toolchain. ### 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. --- .../Support/EffectfulExpressionHandling.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index f67ca40ee..a70f4ae0f 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -111,7 +111,13 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp let needAwait = effectfulKeywords.contains(.await) && !expr.is(AwaitExprSyntax.self) let needTry = effectfulKeywords.contains(.try) && !expr.is(TryExprSyntax.self) + + // The 'unsafe' keyword was introduced in 6.2 as part of SE-0458. Older + // toolchains are not aware of it, so avoid emitting expressions involving + // that keyword when the macro has been built using an older toolchain. +#if compiler(>=6.2) let needUnsafe = effectfulKeywords.contains(.unsafe) && !expr.is(UnsafeExprSyntax.self) +#endif // First, add thunk function calls. if needAwait { @@ -120,9 +126,11 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp if needTry { expr = _makeCallToEffectfulThunk(.identifier("__requiringTry"), passing: expr) } +#if compiler(>=6.2) if needUnsafe { expr = _makeCallToEffectfulThunk(.identifier("__requiringUnsafe"), passing: expr) } +#endif // Then add keyword expressions. (We do this separately so we end up writing // `try await __r(__r(self))` instead of `try __r(await __r(self))` which is @@ -143,6 +151,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp ) ) } +#if compiler(>=6.2) if needUnsafe { expr = ExprSyntax( UnsafeExprSyntax( @@ -151,6 +160,7 @@ func applyEffectfulKeywords(_ effectfulKeywords: Set, to expr: some Exp ) ) } +#endif expr.leadingTrivia = originalExpr.leadingTrivia expr.trailingTrivia = originalExpr.trailingTrivia From b5fa554b5af01c87455c92d0e0db88b84667c20f Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 24 Apr 2025 10:46:11 -0500 Subject: [PATCH 5/5] Include code comments before expectations which are preceded by try/await in recorded issues (#1092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an issue where code comments placed before an expectation like `#expect()` which has effect introducer keywords like `try` or `await` are ignored, and ensures they are included in the recorded issue if the expectation fails. Consider this example of two failing expectations: ```swift // Uh oh! #expect(try x() == 2) // Uh oh! try #expect(x() == 2) ``` Prior to this PR, if `x()` returned a value other than `2`, there would be two issues recorded, but the second one would not have the comment `“Uh oh!”` because from the macro’s perspective, that code comment was on the `try` keyword and it could only see trivia associated with `#expect()`. Now, with the recent swift-syntax fix from https://github.com/swiftlang/swift-syntax/pull/3037, the `try` keyword and its associated trivia can be included and this bug can be fixed. We recently adopted a new-enough swift-syntax in #1069, so the only fix needed is to adopt `lexicalContext` for this new purpose in our macro. ### 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. --- Sources/TestingMacros/ConditionMacro.swift | 11 +++- .../Support/EffectfulExpressionHandling.swift | 17 +++++- .../ConditionMacroTests.swift | 53 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index 326522858..e21938041 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -157,8 +157,17 @@ extension ConditionMacro { expandedFunctionName = conditionArgument.expandedFunctionName } - // Capture any comments as well (either in source or as a macro argument.) + // Capture any comments as well -- either in source, preceding the + // expression macro or one of its lexical context nodes, or as an argument + // to the macro. let commentsArrayExpr = ArrayExprSyntax { + // Lexical context is ordered innermost-to-outermost, so reverse it to + // maintain the expected order. + for lexicalSyntaxNode in context.lexicalContext.trailingEffectExpressions.reversed() { + for commentTraitExpr in createCommentTraitExprs(for: lexicalSyntaxNode) { + ArrayElementSyntax(expression: commentTraitExpr) + } + } for commentTraitExpr in createCommentTraitExprs(for: macro) { ArrayElementSyntax(expression: commentTraitExpr) } diff --git a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift index a70f4ae0f..494d2fcfc 100644 --- a/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift +++ b/Sources/TestingMacros/Support/EffectfulExpressionHandling.swift @@ -12,7 +12,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -// MARK: - Finding effect keywords +// MARK: - Finding effect keywords and expressions /// A syntax visitor class that looks for effectful keywords in a given /// expression. @@ -69,6 +69,21 @@ func findEffectKeywords(in node: some SyntaxProtocol, context: some MacroExpansi return effectFinder.effectKeywords } +extension BidirectionalCollection { + /// The suffix of syntax nodes in this collection which are effectful + /// expressions, such as those for `try` or `await`. + var trailingEffectExpressions: some Collection { + reversed() + .prefix { node in + // This could be simplified if/when swift-syntax introduces a protocol + // which all effectful expression syntax node types conform to. + // See https://github.com/swiftlang/swift-syntax/issues/3040 + node.is(TryExprSyntax.self) || node.is(AwaitExprSyntax.self) || node.is(UnsafeExprSyntax.self) + } + .reversed() + } +} + // MARK: - Inserting effect keywords/thunks /// Make a function call expression to an effectful thunk function provided by diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index cd1333941..67531dabf 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -240,6 +240,59 @@ struct ConditionMacroTests { // Capture me Testing.__checkValue(try x(), expression: .__fromSyntaxNode("try x()"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() """, + + """ + // Capture me + try #expect(x) + """: + """ + // Capture me + try Testing.__checkValue(x, expression: .__fromSyntaxNode("x"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + """, + + """ + // Capture me + await #expect(x) + """: + """ + // Capture me + await Testing.__checkValue(x, expression: .__fromSyntaxNode("x"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + """, + + """ + // Ignore me + + // Comment for try + try + // Comment for await + await + // Comment for expect + #expect(x) + """: + """ + // Comment for try + try + // Comment for await + await + // Comment for expect + Testing.__checkValue(x, expression: .__fromSyntaxNode("x"), comments: [.__line("// Comment for try"), .__line("// Comment for await"), .__line("// Comment for expect")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + """, + + """ + // Ignore me + func example() { + // Capture me + #expect(x()) + } + """: + """ + func example() { + // Capture me + Testing.__checkFunctionCall((), calling: { _ in + x() + }, expression: .__fromFunctionCall(nil, "x"), comments: [.__line("// Capture me")], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected() + } + """, ] ) func commentCapture(input: String, expectedOutput: String) throws {