Skip to content

Commit d35579a

Browse files
authored
Add experimental flag which allows providing a custom test entry point file while still performing test discovery (#5709)
Add experimental flag which allows providing a custom test entry point file while still performing test discovery There are situations where a user may wish to customize the entry point file used to run tests on non-Apple platforms via `swift test`. For example, to run custom code before invoking the `XCTMain(...)` function. But including a custom `XCTMain.swift` file (or its former name, `LinuxMain.swift`) causes `swift test` to solely rely on that file to run tests, and more importantly, it causes SwiftPM to skip automatic test discovery, so you end up needing to list all the tests to run in that file. Passing `--enable-test-discovery` explicitly does the opposite: it performs test discovery, synthesizes its own test entry point file, and ignores any custom entry point file present in the package. This PR adds a way to both provide a custom test entry point file and still perform test discovery, allowing the entry point to reference the automatically-discovered tests and pass them to `XCTMain(...)`. It enables this behavior when passing a new, experimental flag `--experimental-test-entry-point-path <file>`. rdar://97940043
1 parent 0de0bc8 commit d35579a

20 files changed

+497
-162
lines changed

Sources/Build/BuildOperation.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ extension BuildDescription {
611611
let swiftCommands = llbuild.manifest.getCmdToolMap(kind: SwiftCompilerTool.self)
612612
let swiftFrontendCommands = llbuild.manifest.getCmdToolMap(kind: SwiftFrontendTool.self)
613613
let testDiscoveryCommands = llbuild.manifest.getCmdToolMap(kind: TestDiscoveryTool.self)
614+
let testEntryPointCommands = llbuild.manifest.getCmdToolMap(kind: TestEntryPointTool.self)
614615
let copyCommands = llbuild.manifest.getCmdToolMap(kind: CopyTool.self)
615616

616617
// Create the build description.
@@ -619,6 +620,7 @@ extension BuildDescription {
619620
swiftCommands: swiftCommands,
620621
swiftFrontendCommands: swiftFrontendCommands,
621622
testDiscoveryCommands: testDiscoveryCommands,
623+
testEntryPointCommands: testEntryPointCommands,
622624
copyCommands: copyCommands,
623625
pluginDescriptions: plan.pluginDescriptions
624626
)

Sources/Build/BuildOperationBuildSystemDelegateHandler.swift

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private extension IndexStore.TestCaseClass.TestMethod {
5959
}
6060
}
6161

62-
final class TestDiscoveryCommand: CustomLLBuildCommand {
62+
final class TestDiscoveryCommand: CustomLLBuildCommand, TestBuildCommand {
6363

6464
private func write(
6565
tests: [IndexStore.TestCaseClass],
@@ -155,24 +155,76 @@ final class TestDiscoveryCommand: CustomLLBuildCommand {
155155

156156
stream <<< "import XCTest" <<< "\n\n"
157157

158-
stream <<< "@main" <<< "\n"
159158
stream <<< "@available(*, deprecated, message: \"Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings\")" <<< "\n"
160-
stream <<< "struct Runner" <<< " {" <<< "\n"
161-
stream <<< indent(4) <<< "static func main()" <<< " {" <<< "\n"
162-
stream <<< indent(8) <<< "\(testsKeyword) tests = [XCTestCaseEntry]()" <<< "\n"
159+
stream <<< "public func __allDiscoveredTests() -> [XCTestCaseEntry] {" <<< "\n"
160+
stream <<< indent(4) <<< "\(testsKeyword) tests = [XCTestCaseEntry]()" <<< "\n\n"
161+
163162
for module in testsByModule.keys {
164-
stream <<< indent(8) <<< "tests += __\(module)__allTests()" <<< "\n"
163+
stream <<< indent(4) <<< "tests += __\(module)__allTests()" <<< "\n"
165164
}
166-
stream <<< indent(8) <<< "\n"
167-
stream <<< indent(8) <<< "XCTMain(tests)" <<< "\n"
168-
stream <<< indent(4) <<< "}" <<< "\n"
165+
166+
stream <<< "\n"
167+
stream <<< indent(4) <<< "return tests" <<< "\n"
169168
stream <<< "}" <<< "\n"
170169

171170
stream.flush()
172171
}
173172

174-
private func indent(_ spaces: Int) -> ByteStreamable {
175-
return Format.asRepeating(string: " ", count: spaces)
173+
override func execute(
174+
_ command: SPMLLBuild.Command,
175+
_ buildSystemCommandInterface: SPMLLBuild.BuildSystemCommandInterface
176+
) -> Bool {
177+
do {
178+
// This tool will never run without the build description.
179+
guard let buildDescription = self.context.buildDescription else {
180+
throw InternalError("unknown build description")
181+
}
182+
guard let tool = buildDescription.testDiscoveryCommands[command.name] else {
183+
throw InternalError("command \(command.name) not registered")
184+
}
185+
try execute(fileSystem: self.context.fileSystem, tool: tool)
186+
return true
187+
} catch {
188+
self.context.observabilityScope.emit(error)
189+
return false
190+
}
191+
}
192+
}
193+
194+
final class TestEntryPointCommand: CustomLLBuildCommand, TestBuildCommand {
195+
196+
private func execute(fileSystem: TSCBasic.FileSystem, tool: LLBuildManifest.TestEntryPointTool) throws {
197+
// Find the inputs, which are the names of the test discovery module(s)
198+
let inputs = tool.inputs.compactMap { try? AbsolutePath(validating: $0.name) }
199+
let discoveryModuleNames = inputs.map { $0.basenameWithoutExt }
200+
201+
let outputs = tool.outputs.compactMap { try? AbsolutePath(validating: $0.name) }
202+
203+
// Find the main output file
204+
guard let mainFile = outputs.first(where: { path in
205+
path.basename == LLBuildManifest.TestEntryPointTool.mainFileName
206+
}) else {
207+
throw InternalError("main file output (\(LLBuildManifest.TestEntryPointTool.mainFileName)) not found")
208+
}
209+
210+
// Write the main file.
211+
let stream = try LocalFileOutputByteStream(mainFile)
212+
213+
stream <<< "import XCTest" <<< "\n"
214+
for discoveryModuleName in discoveryModuleNames {
215+
stream <<< "import \(discoveryModuleName)" <<< "\n"
216+
}
217+
stream <<< "\n"
218+
219+
stream <<< "@main" <<< "\n"
220+
stream <<< "@available(*, deprecated, message: \"Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings\")" <<< "\n"
221+
stream <<< "struct Runner" <<< " {" <<< "\n"
222+
stream <<< indent(4) <<< "static func main()" <<< " {" <<< "\n"
223+
stream <<< indent(8) <<< "XCTMain(__allDiscoveredTests())" <<< "\n"
224+
stream <<< indent(4) <<< "}" <<< "\n"
225+
stream <<< "}" <<< "\n"
226+
227+
stream.flush()
176228
}
177229

178230
override func execute(
@@ -184,8 +236,8 @@ final class TestDiscoveryCommand: CustomLLBuildCommand {
184236
guard let buildDescription = self.context.buildDescription else {
185237
throw InternalError("unknown build description")
186238
}
187-
guard let tool = buildDescription.testDiscoveryCommands[command.name] else {
188-
throw StringError("command \(command.name) not registered")
239+
guard let tool = buildDescription.testEntryPointCommands[command.name] else {
240+
throw InternalError("command \(command.name) not registered")
189241
}
190242
try execute(fileSystem: self.context.fileSystem, tool: tool)
191243
return true
@@ -196,6 +248,19 @@ final class TestDiscoveryCommand: CustomLLBuildCommand {
196248
}
197249
}
198250

251+
private protocol TestBuildCommand {}
252+
253+
/// Functionality common to all build commands related to test targets.
254+
extension TestBuildCommand {
255+
256+
/// Returns a value containing `spaces` number of space characters.
257+
/// Intended to facilitate indenting generated code a specified number of levels.
258+
fileprivate func indent(_ spaces: Int) -> ByteStreamable {
259+
return Format.asRepeating(string: " ", count: spaces)
260+
}
261+
262+
}
263+
199264
private final class InProcessTool: Tool {
200265
let context: BuildExecutionContext
201266
let type: CustomLLBuildCommand.Type
@@ -230,6 +295,9 @@ public struct BuildDescription: Codable {
230295
/// The map of test discovery commands.
231296
let testDiscoveryCommands: [BuildManifest.CmdName: LLBuildManifest.TestDiscoveryTool]
232297

298+
/// The map of test entry point commands.
299+
let testEntryPointCommands: [BuildManifest.CmdName: LLBuildManifest.TestEntryPointTool]
300+
233301
/// The map of copy commands.
234302
let copyCommands: [BuildManifest.CmdName: LLBuildManifest.CopyTool]
235303

@@ -257,12 +325,14 @@ public struct BuildDescription: Codable {
257325
swiftCommands: [BuildManifest.CmdName : SwiftCompilerTool],
258326
swiftFrontendCommands: [BuildManifest.CmdName : SwiftFrontendTool],
259327
testDiscoveryCommands: [BuildManifest.CmdName: LLBuildManifest.TestDiscoveryTool],
328+
testEntryPointCommands: [BuildManifest.CmdName: LLBuildManifest.TestEntryPointTool],
260329
copyCommands: [BuildManifest.CmdName: LLBuildManifest.CopyTool],
261330
pluginDescriptions: [PluginDescription]
262331
) throws {
263332
self.swiftCommands = swiftCommands
264333
self.swiftFrontendCommands = swiftFrontendCommands
265334
self.testDiscoveryCommands = testDiscoveryCommands
335+
self.testEntryPointCommands = testEntryPointCommands
266336
self.copyCommands = copyCommands
267337
self.explicitTargetDependencyImportCheckingMode = plan.buildParameters.explicitTargetDependencyImportCheckingMode
268338
self.targetDependencyMap = try plan.targets.reduce(into: [TargetName: [TargetName]]()) {
@@ -278,7 +348,7 @@ public struct BuildDescription: Codable {
278348
targetCommandLines[target.c99name] =
279349
try desc.emitCommandLine(scanInvocation: true) + ["-driver-use-frontend-path",
280350
plan.buildParameters.toolchain.swiftCompilerPath.pathString]
281-
if desc.isTestDiscoveryTarget {
351+
if case .discovery = desc.testTargetRole {
282352
generatedSourceTargets.append(target.c99name)
283353
}
284354
}
@@ -500,6 +570,8 @@ final class BuildOperationBuildSystemDelegateHandler: LLBuildBuildSystemDelegate
500570
switch name {
501571
case TestDiscoveryTool.name:
502572
return InProcessTool(buildExecutionContext, type: TestDiscoveryCommand.self)
573+
case TestEntryPointTool.name:
574+
return InProcessTool(buildExecutionContext, type: TestEntryPointCommand.self)
503575
case PackageStructureTool.name:
504576
return InProcessTool(buildExecutionContext, type: PackageStructureCommand.self)
505577
case CopyTool.name:

0 commit comments

Comments
 (0)