Skip to content

Commit b9df952

Browse files
authored
Add --default-test-suite-attributes option to preferSwiftTesting rule (#2192)
1 parent baf1093 commit b9df952

File tree

5 files changed

+85
-36
lines changed

5 files changed

+85
-36
lines changed

Rules.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,6 +1984,7 @@ Prefer the Swift Testing library over XCTest.
19841984
Option | Description
19851985
--- | ---
19861986
`--xctest-symbols` | Comma-delimited list of symbols that depend on XCTest
1987+
`--default-test-suite-attributes` | Comma-delimited list of attributes to add when converting from XCTest. e.g. "@MainActor,@Suite(.serialized)"
19871988

19881989
<details>
19891990
<summary>Examples</summary>
@@ -2003,7 +2004,6 @@ Option | Description
20032004
- XCTAssertNil(myFeature.crashReport)
20042005
- }
20052006
- }
2006-
+ @MainActor @Suite(.serialized)
20072007
+ final class MyFeatureTests {
20082008
+ @Test func myFeatureHasNoBugs() {
20092009
+ let myFeature = MyFeature()
@@ -2031,7 +2031,6 @@ Option | Description
20312031
- XCTAssertEqual(myFeature.screens.count, 8)
20322032
- }
20332033
- }
2034-
+ @MainActor
20352034
+ final class MyFeatureTests {
20362035
+ var myFeature: MyFeature!
20372036
+

Sources/OptionDescriptor.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,19 @@ struct _Descriptors {
13741374
help: "Comma-delimited list of symbols that depend on XCTest",
13751375
keyPath: \.additionalXCTestSymbols
13761376
)
1377+
let defaultTestSuiteAttributes = OptionDescriptor(
1378+
argumentName: "default-test-suite-attributes",
1379+
displayName: "Default test suite attributes",
1380+
help: "Comma-delimited list of attributes to add when converting from XCTest. e.g. \"@MainActor,@Suite(.serialized)\"",
1381+
keyPath: \.defaultTestSuiteAttributes,
1382+
validateArray: { array in
1383+
for attribute in array {
1384+
guard attribute.starts(with: "@"), !tokenize(attribute).contains(where: \.isError) else {
1385+
throw FormatError.options("Invalid attribute: \"\(attribute)\"")
1386+
}
1387+
}
1388+
}
1389+
)
13771390
let swiftUIPropertiesSortMode = OptionDescriptor(
13781391
argumentName: "sort-swiftui-properties",
13791392
displayName: "Sort SwiftUI Dynamic Properties",

Sources/Options.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ public struct FormatOptions: CustomStringConvertible {
823823
public var nilInit: NilInitType
824824
public var preservedPrivateDeclarations: Set<String>
825825
public var additionalXCTestSymbols: Set<String>
826+
public var defaultTestSuiteAttributes: [String]
826827
public var equatableMacro: EquatableMacro
827828
public var urlMacro: URLMacro
828829
public var preferFileMacro: Bool
@@ -964,6 +965,7 @@ public struct FormatOptions: CustomStringConvertible {
964965
nilInit: NilInitType = .remove,
965966
preservedPrivateDeclarations: Set<String> = [],
966967
additionalXCTestSymbols: Set<String> = [],
968+
defaultTestSuiteAttributes: [String] = [],
967969
equatableMacro: EquatableMacro = .none,
968970
urlMacro: URLMacro = .none,
969971
preferFileMacro: Bool = true,
@@ -1094,6 +1096,7 @@ public struct FormatOptions: CustomStringConvertible {
10941096
self.nilInit = nilInit
10951097
self.preservedPrivateDeclarations = preservedPrivateDeclarations
10961098
self.additionalXCTestSymbols = additionalXCTestSymbols
1099+
self.defaultTestSuiteAttributes = defaultTestSuiteAttributes
10971100
self.equatableMacro = equatableMacro
10981101
self.urlMacro = urlMacro
10991102
self.preferFileMacro = preferFileMacro

Sources/Rules/PreferSwiftTesting.swift

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public extension FormatRule {
1212
static let preferSwiftTesting = FormatRule(
1313
help: "Prefer the Swift Testing library over XCTest.",
1414
disabledByDefault: true,
15-
options: ["xctest-symbols"]
15+
options: ["xctest-symbols", "default-test-suite-attributes"]
1616
) { formatter in
1717
// Swift Testing was introduced in Xcode 16.0 with Swift 6.0
1818
guard formatter.options.swiftVersion >= "6.0" else { return }
@@ -68,7 +68,6 @@ public extension FormatRule {
6868
- XCTAssertNil(myFeature.crashReport)
6969
- }
7070
- }
71-
+ @MainActor @Suite(.serialized)
7271
+ final class MyFeatureTests {
7372
+ @Test func myFeatureHasNoBugs() {
7473
+ let myFeature = MyFeature()
@@ -96,7 +95,6 @@ public extension FormatRule {
9695
- XCTAssertEqual(myFeature.screens.count, 8)
9796
- }
9897
- }
99-
+ @MainActor
10098
+ final class MyFeatureTests {
10199
+ var myFeature: MyFeature!
102100
+
@@ -119,7 +117,7 @@ public extension FormatRule {
119117
}
120118
}
121119

122-
// MARK: XCTestCase test suite convesaion
120+
// MARK: XCTestCase test suite conversion
123121

124122
extension TypeDeclaration {
125123
/// Whether or not this declaration uses XCTest functionality that is
@@ -158,25 +156,13 @@ extension TypeDeclaration {
158156
formatter.removeConformance(at: xcTestCaseConformance.index)
159157
}
160158

161-
// XCTest runs test serially, but Swift Testing defaults to running tests concurrently.
162-
// For compatibility, have the generate Swift Testing suite default to running tests serially.
163-
//
164-
// Also from the XCTest to Swift Testing migration guide:
165-
// https://developer.apple.com/documentation/testing/migratingfromxctest
166-
// > XCTest runs synchronous test methods on the main actor by default,
167-
// > while the testing library runs all test functions on an arbitrary task.
168-
// > If a test function must run on the main thread, isolate it to the main actor
169-
// > with @MainActor, or run the thread-sensitive code inside a call to
170-
// > MainActor.run(resultType:body:).
171-
//
172-
// Moving test case to a background thread may cause failures, e.g. if
173-
// the test case accesses any UIKit APIs, so we mark the test suite
174-
// as @MainActor for maximum compatibility.
175-
let startOfModifiers = formatter.startOfModifiers(at: keywordIndex, includingAttributes: true)
176-
if !modifiers.contains("@MainActor") {
177-
formatter.insert(tokenize("@MainActor @Suite(.serialized)\n"), at: startOfModifiers)
178-
} else {
179-
formatter.insert(tokenize("@Suite(.serialized)\n"), at: startOfModifiers)
159+
// Allow the user to specify additional attributes to add to the new test suite,
160+
// like `@MainActor`, `@Suite(.serialized)`, etc.
161+
let attributesToAdd = formatter.options.defaultTestSuiteAttributes.joined(separator: " ")
162+
if !attributesToAdd.isEmpty {
163+
let startOfModifiers = formatter.startOfModifiers(at: keywordIndex, includingAttributes: true)
164+
let attributesWithNewline = attributesToAdd + "\n"
165+
formatter.insert(tokenize(attributesWithNewline), at: startOfModifiers)
180166
}
181167

182168
let instanceMethods = body.filter { $0.keyword == "func" && !$0.modifiers.contains("static") }

Tests/Rules/PreferSwiftTestingTests.swift

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ final class PreferSwiftTestingTests: XCTestCase {
3838
@testable import MyFeatureLib
3939
import Testing
4040
41-
@MainActor @Suite(.serialized)
4241
final class MyFeatureTests {
4342
@Test func myFeatureWorks() {
4443
let myFeature = MyFeature()
@@ -93,7 +92,6 @@ final class PreferSwiftTestingTests: XCTestCase {
9392
@testable import MyFeatureLib
9493
import Testing
9594
96-
@MainActor @Suite(.serialized)
9795
final class MyFeatureTests {
9896
var myFeature: MyFeature!
9997
@@ -178,7 +176,6 @@ final class PreferSwiftTestingTests: XCTestCase {
178176
import Foundation
179177
import Testing
180178
181-
@MainActor @Suite(.serialized)
182179
class HelperConversionTests {
183180
@Test func convertsSimpleXCTestHelpers() throws {
184181
#expect(foo)
@@ -285,7 +282,6 @@ final class PreferSwiftTestingTests: XCTestCase {
285282
import Foundation
286283
import Testing
287284
288-
@MainActor @Suite(.serialized)
289285
class HelperConversionTests {
290286
@Test func converts_multiline_XCTest_helpers() {
291287
#expect(foo.bar(
@@ -463,7 +459,6 @@ final class PreferSwiftTestingTests: XCTestCase {
463459
@testable import MyFeatureLib
464460
import Testing
465461
466-
@MainActor @Suite(.serialized)
467462
final class MyFeatureTests {
468463
@Test func myFeatureWorks() {
469464
let myFeature = MyFeature()
@@ -528,7 +523,6 @@ final class PreferSwiftTestingTests: XCTestCase {
528523
import Foundation
529524
import Testing
530525
531-
@MainActor @Suite(.serialized)
532526
final class MyFeatureTests {
533527
@Test func test123() {
534528
#expect((1 + 2) == 3)
@@ -552,7 +546,7 @@ final class PreferSwiftTestingTests: XCTestCase {
552546
testFormatting(for: input, [output], rules: [.preferSwiftTesting, .sortImports], options: options)
553547
}
554548

555-
func testDoesntUpTestNameToExistingFunctionName() {
549+
func testDoesntUpdateTestNameToExistingFunctionName() {
556550
let input = """
557551
import XCTest
558552
@@ -571,7 +565,6 @@ final class PreferSwiftTestingTests: XCTestCase {
571565
import Foundation
572566
import Testing
573567
574-
@MainActor @Suite(.serialized)
575568
final class MyFeatureTests {
576569
@Test func testOnePlusTwo() {
577570
#expect(onePlusTwo() == 3)
@@ -607,7 +600,6 @@ final class PreferSwiftTestingTests: XCTestCase {
607600
import Foundation
608601
import Testing
609602
610-
@MainActor @Suite(.serialized)
611603
final class MyFeatureTests {
612604
@Test func myFeatureWorks() {
613605
testMyFeatureWorks(MyFeature())
@@ -648,7 +640,6 @@ final class PreferSwiftTestingTests: XCTestCase {
648640
import Foundation
649641
import Testing
650642
651-
@MainActor @Suite(.serialized)
652643
final class MyFeatureTests {
653644
@Test func myFeatureWorks() throws {
654645
let myFeature = MyFeature()
@@ -725,7 +716,6 @@ final class PreferSwiftTestingTests: XCTestCase {
725716
import Testing
726717
import UIKit
727718
728-
@MainActor @Suite(.serialized)
729719
final class MyFeatureTests {
730720
@Test func myFeatureWorks() {
731721
let viewController = UIViewController()
@@ -737,4 +727,62 @@ final class PreferSwiftTestingTests: XCTestCase {
737727
let options = FormatOptions(swiftVersion: "6.0")
738728
testFormatting(for: input, [output], rules: [.preferSwiftTesting, .sortImports], options: options)
739729
}
730+
731+
func testAppliesCustomTestSuiteAttribute() {
732+
let input = """
733+
import XCTest
734+
735+
final class MyFeatureTests: XCTestCase {
736+
func testMyFeatureWorks() {
737+
let myFeature = MyFeature()
738+
XCTAssertTrue(myFeature.worksProperly)
739+
}
740+
}
741+
"""
742+
743+
let output = """
744+
import Foundation
745+
import Testing
746+
747+
@MainActor
748+
final class MyFeatureTests {
749+
@Test func myFeatureWorks() {
750+
let myFeature = MyFeature()
751+
#expect(myFeature.worksProperly)
752+
}
753+
}
754+
"""
755+
756+
let options = FormatOptions(defaultTestSuiteAttributes: ["@MainActor"], swiftVersion: "6.0")
757+
testFormatting(for: input, [output], rules: [.preferSwiftTesting, .sortImports], options: options)
758+
}
759+
760+
func testAppliesMultipleTestSuiteAttributes() {
761+
let input = """
762+
import XCTest
763+
764+
final class MyFeatureTests: XCTestCase {
765+
func testMyFeatureWorks() {
766+
let myFeature = MyFeature()
767+
XCTAssertTrue(myFeature.worksProperly)
768+
}
769+
}
770+
"""
771+
772+
let output = """
773+
import Foundation
774+
import Testing
775+
776+
@MainActor @Suite(.serialized)
777+
final class MyFeatureTests {
778+
@Test func myFeatureWorks() {
779+
let myFeature = MyFeature()
780+
#expect(myFeature.worksProperly)
781+
}
782+
}
783+
"""
784+
785+
let options = FormatOptions(defaultTestSuiteAttributes: ["@MainActor", "@Suite(.serialized)"], swiftVersion: "6.0")
786+
testFormatting(for: input, [output], rules: [.preferSwiftTesting, .sortImports], options: options)
787+
}
740788
}

0 commit comments

Comments
 (0)