Skip to content

Commit 3c2f4e3

Browse files
authored
Shard GeneratedTests into parallel targets and refactor code generation (#6102)
Split the monolithic GeneratedTests target (242 test classes) into 10 sharded targets with ~25 tests each to enable parallel test execution. Reduces test time from 85.4s to 36.7s (57% improvement) by running shards concurrently. Most shards finish in 2-8s with 2 outliers at 30-37s. The implementation automatically scales with new rules and provides parallel test execution with improved code maintainability.
1 parent b6ebbcf commit 3c2f4e3

15 files changed

+1724
-1503
lines changed

Source/swiftlint-dev/Rules+Register.swift

Lines changed: 132 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ extension SwiftLintDev.Rules {
4747
.sorted()
4848
try registerInRulesList(rules)
4949
try registerInTests(rules)
50+
try registerInTestsBzl(rules)
5051
try registerInTestReference(adding: newRule)
5152
print("(Re-)Registered \(rules.count) rules.")
5253
}
@@ -74,53 +75,158 @@ struct NewRuleDetails: Equatable {
7475
}
7576
}
7677

78+
/// Struct to hold processed rule information and shard calculations
79+
private struct ProcessedRulesContext {
80+
let baseRuleNames: [String]
81+
let totalShards: Int
82+
let shardSize: Int
83+
84+
init(ruleFiles: [String], shardSize: Int) {
85+
self.baseRuleNames = ruleFiles.map { $0.replacingOccurrences(of: ".swift", with: "") }
86+
self.shardSize = shardSize
87+
guard shardSize > 0, !baseRuleNames.isEmpty else {
88+
self.totalShards = baseRuleNames.isEmpty ? 0 : 1
89+
return
90+
}
91+
self.totalShards = (baseRuleNames.count + shardSize - 1) / shardSize // Ceiling division
92+
}
93+
94+
/// Returns the rule names for a specific shard index
95+
func shardRules(forIndex shardIndex: Int) -> ArraySlice<String> {
96+
let startIndex = shardIndex * shardSize
97+
let endIndex = min(startIndex + shardSize, baseRuleNames.count)
98+
return baseRuleNames[startIndex..<endIndex]
99+
}
100+
101+
/// Returns zero-padded shard numbers for all shards
102+
var shardNumbers: [String] {
103+
(1...totalShards).map { String(format: "%02d", $0) }
104+
}
105+
}
106+
77107
private extension SwiftLintDev.Rules.Register {
108+
/// Number of test classes per shard for optimal parallelization
109+
private static let shardSize = 25
110+
111+
/// Common parent directory of testsDirectory
112+
private var testsParentDirectory: URL {
113+
testsDirectory.deletingLastPathComponent()
114+
}
115+
116+
/// Generate content for BuiltInRules.swift file
117+
private func generateBuiltInRulesFileContent(rulesImportList: String) -> String {
118+
"""
119+
// GENERATED FILE. DO NOT EDIT!
120+
121+
/// The rule list containing all available rules built into SwiftLint.
122+
public let builtInRules: [any Rule.Type] = [
123+
\(rulesImportList.indent(by: 4)),
124+
]
125+
126+
"""
127+
}
128+
129+
/// Generate content for Swift test files
130+
private func generateSwiftTestFileContent(forTestClasses testClassesString: String) -> String {
131+
"""
132+
// GENERATED FILE. DO NOT EDIT!
133+
// swiftlint:disable:previous file_name
134+
// swiftlint:disable:previous blanket_disable_command superfluous_disable_command
135+
// swiftlint:disable:next blanket_disable_command superfluous_disable_command
136+
// swiftlint:disable file_length single_test_class type_name
137+
138+
@testable import SwiftLintBuiltInRules
139+
@testable import SwiftLintCore
140+
import TestHelpers
141+
142+
\(testClassesString)
143+
144+
"""
145+
}
146+
147+
/// Generate content for Bazel .bzl files
148+
private func generateBzlFileContent(macroInvocations: String) -> String {
149+
#"""
150+
# GENERATED FILE. DO NOT EDIT!
151+
152+
load(":test_macros.bzl", "generated_test_shard")
153+
154+
def generated_tests(copts, strict_concurrency_copts):
155+
"""Creates all generated test targets for SwiftLint rules.
156+
157+
Args:
158+
copts: Common compiler options
159+
strict_concurrency_copts: Strict concurrency compiler options
160+
"""
161+
\#(macroInvocations)
162+
163+
"""#
164+
}
165+
78166
func registerInRulesList(_ ruleFiles: [String]) throws {
79-
let rules = ruleFiles
167+
let rulesImportString = ruleFiles
80168
.map { $0.replacingOccurrences(of: ".swift", with: ".self") }
81169
.joined(separator: ",\n")
82170
let builtInRulesFile = rulesDirectory.deletingLastPathComponent()
83171
.appendingPathComponent("Models", isDirectory: true)
84172
.appendingPathComponent("BuiltInRules.swift", isDirectory: false)
85-
try """
86-
// GENERATED FILE. DO NOT EDIT!
87173

88-
/// The rule list containing all available rules built into SwiftLint.
89-
public let builtInRules: [any Rule.Type] = [
90-
\(rules.indent(by: 4)),
91-
]
92-
93-
""".write(to: builtInRulesFile, atomically: true, encoding: .utf8)
174+
let fileContent = generateBuiltInRulesFileContent(rulesImportList: rulesImportString)
175+
try fileContent.write(to: builtInRulesFile, atomically: true, encoding: .utf8)
94176
}
95177

96178
func registerInTests(_ ruleFiles: [String]) throws {
97-
let testFile = testsDirectory
98-
.appendingPathComponent("GeneratedTests.swift", isDirectory: false)
99-
let rules = ruleFiles
100-
.map { $0.replacingOccurrences(of: ".swift", with: "") }
101-
.map { testName in """
179+
let rulesContext = ProcessedRulesContext(ruleFiles: ruleFiles, shardSize: Self.shardSize)
180+
181+
// Remove old generated files
182+
let existingFiles = try FileManager.default.contentsOfDirectory(
183+
at: testsDirectory,
184+
includingPropertiesForKeys: nil
185+
)
186+
for file in existingFiles where file.lastPathComponent.hasPrefix("GeneratedTests") &&
187+
file.pathExtension == "swift" {
188+
try FileManager.default.removeItem(at: file)
189+
}
190+
191+
// Create sharded test files
192+
for shardIndex in 0..<rulesContext.totalShards {
193+
let shardRules = rulesContext.shardRules(forIndex: shardIndex)
194+
195+
let testClasses = shardRules.map { testName in """
102196
final class \(testName)GeneratedTests: SwiftLintTestCase {
103197
func testWithDefaultConfiguration() {
104198
verifyRule(\(testName).description)
105199
}
106200
}
107201
"""
108-
}
109-
.joined(separator: "\n\n")
202+
}.joined(separator: "\n\n")
110203

111-
try """
112-
// GENERATED FILE. DO NOT EDIT!
204+
let shardNumber = rulesContext.shardNumbers[shardIndex]
205+
let testFile = testsDirectory.appendingPathComponent(
206+
"GeneratedTests_\(shardNumber).swift",
207+
isDirectory: false
208+
)
113209

114-
@testable import SwiftLintBuiltInRules
115-
@testable import SwiftLintCore
116-
import TestHelpers
210+
let fileContent = generateSwiftTestFileContent(forTestClasses: testClasses)
211+
try fileContent.write(to: testFile, atomically: true, encoding: .utf8)
212+
}
213+
}
117214

118-
// swiftlint:disable:next blanket_disable_command
119-
// swiftlint:disable file_length single_test_class type_name
215+
func registerInTestsBzl(_ ruleFiles: [String]) throws {
216+
let rulesContext = ProcessedRulesContext(ruleFiles: ruleFiles, shardSize: Self.shardSize)
120217

121-
\(rules)
218+
// Generate macro calls for each shard
219+
let macroInvocationsString = rulesContext.shardNumbers.map {
220+
#" generated_test_shard("\#($0)", copts, strict_concurrency_copts)"#
221+
}.joined(separator: "\n")
222+
223+
let bzlFile = testsParentDirectory.appendingPathComponent(
224+
"generated_tests.bzl",
225+
isDirectory: false
226+
)
122227

123-
""".write(to: testFile, atomically: true, encoding: .utf8)
228+
let fileContent = generateBzlFileContent(macroInvocations: macroInvocationsString)
229+
try fileContent.write(to: bzlFile, atomically: true, encoding: .utf8)
124230
}
125231

126232
func registerInTestReference(adding newRule: NewRuleDetails?) throws {
@@ -147,7 +253,7 @@ private extension SwiftLintDev.Rules.Register {
147253
.joined(separator: "\n")
148254
.appending("\n")
149255
.write(
150-
to: testsDirectory.deletingLastPathComponent()
256+
to: testsParentDirectory
151257
.appendingPathComponent("IntegrationTests", isDirectory: true)
152258
.appendingPathComponent("default_rule_configurations.yml", isDirectory: false),
153259
atomically: true,

Tests/BUILD

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library", "swift_test")
2+
load(":generated_tests.bzl", "generated_tests")
23

34
exports_files(["BUILD"])
45

@@ -143,23 +144,7 @@ swift_test(
143144

144145
# GeneratedTests
145146

146-
swift_library(
147-
name = "GeneratedTests.library",
148-
testonly = True,
149-
srcs = ["GeneratedTests/GeneratedTests.swift"],
150-
module_name = "GeneratedTests",
151-
package_name = "SwiftLint",
152-
deps = [
153-
":TestHelpers",
154-
],
155-
copts = copts + strict_concurrency_copts,
156-
)
157-
158-
swift_test(
159-
name = "GeneratedTests",
160-
visibility = ["//visibility:public"],
161-
deps = [":GeneratedTests.library"],
162-
)
147+
generated_tests(copts, strict_concurrency_copts)
163148

164149
# IntegrationTests
165150

0 commit comments

Comments
 (0)