From 133a61087e07611720420f75b552e12be57cfbd3 Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Wed, 13 Aug 2025 13:29:29 -0700 Subject: [PATCH] Reapply "Allow opting into Swift Testing entrypoints for macOS test bundles when building with the SwiftPM CLI" (#727) This reverts commit d91213d0c345fbb272f0199f0a590b8992af534a. --- Sources/SWBCore/Settings/BuiltinMacros.swift | 2 + .../Specs/ProductTypes.xcspec | 4 + .../TestEntryPointGenerationTaskAction.swift | 100 ++++++++++-------- .../TestEntryPointGenerationTool.swift | 24 +++-- .../BuildOperationTests.swift | 70 ++++++++++++ .../UnitTestTaskConstructionTests.swift | 2 +- 6 files changed, 147 insertions(+), 55 deletions(-) diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index d0f5dbd5..2c5448b6 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -726,6 +726,7 @@ public final class BuiltinMacros { public static let GENERATE_RESOURCE_ACCESSORS = BuiltinMacros.declareBooleanMacro("GENERATE_RESOURCE_ACCESSORS") public static let GENERATE_TEST_ENTRY_POINT = BuiltinMacros.declareBooleanMacro("GENERATE_TEST_ENTRY_POINT") public static let GENERATED_TEST_ENTRY_POINT_PATH = BuiltinMacros.declarePathMacro("GENERATED_TEST_ENTRY_POINT_PATH") + public static let GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS = BuiltinMacros.declareBooleanMacro("GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS") public static let GENERATE_TEXT_BASED_STUBS = BuiltinMacros.declareBooleanMacro("GENERATE_TEXT_BASED_STUBS") public static let GENERATE_INTERMEDIATE_TEXT_BASED_STUBS = BuiltinMacros.declareBooleanMacro("GENERATE_INTERMEDIATE_TEXT_BASED_STUBS") public static let GLOBAL_API_NOTES_PATH = BuiltinMacros.declareStringMacro("GLOBAL_API_NOTES_PATH") @@ -1792,6 +1793,7 @@ public final class BuiltinMacros { GENERATE_RESOURCE_ACCESSORS, GENERATE_TEST_ENTRY_POINT, GENERATED_TEST_ENTRY_POINT_PATH, + GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS, GENERATE_TEXT_BASED_STUBS, GENERATE_INTERMEDIATE_TEXT_BASED_STUBS, GID, diff --git a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec index e8211ed1..647cdaea 100644 --- a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec @@ -335,6 +335,9 @@ PROVISIONING_PROFILE_SUPPORTED = YES; PROVISIONING_PROFILE_REQUIRED = NO; + + GENERATE_TEST_ENTRY_POINT = "$(GENERATE_TEST_ENTRYPOINTS_FOR_BUNDLES)"; + GENERATED_TEST_ENTRY_POINT_PATH = "$(DERIVED_SOURCES_DIR)/test_entry_point.swift"; }; PackageTypes = ( com.apple.package-type.bundle.unit-test @@ -353,6 +356,7 @@ ENABLE_TESTING_SEARCH_PATHS = YES; GENERATE_TEST_ENTRY_POINT = YES; GENERATED_TEST_ENTRY_POINT_PATH = "$(DERIVED_SOURCES_DIR)/test_entry_point.swift"; + GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS = YES; }; PackageTypes = ( com.apple.package-type.mach-o-executable diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift index 9765719a..d853b2c7 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift @@ -25,24 +25,30 @@ class TestEntryPointGenerationTaskAction: TaskAction { let options = try Options.parse(Array(task.commandLineAsStrings.dropFirst())) var tests: [IndexStore.TestCaseClass] = [] - var objects: [Path] = [] - for linkerFilelist in options.linkerFilelist { - let filelistContents = String(String(decoding: try executionDelegate.fs.read(linkerFilelist), as: UTF8.self)) - let entries = filelistContents.split(separator: "\n", omittingEmptySubsequences: true).map { Path($0) }.map { - for indexUnitBasePath in options.indexUnitBasePath { - if let remappedPath = generateIndexOutputPath(from: $0, basePath: indexUnitBasePath) { - return remappedPath + if options.discoverTests { + var objects: [Path] = [] + for linkerFilelist in options.linkerFilelist { + let filelistContents = String(String(decoding: try executionDelegate.fs.read(linkerFilelist), as: UTF8.self)) + let entries = filelistContents.split(separator: "\n", omittingEmptySubsequences: true).map { Path($0) }.map { + for indexUnitBasePath in options.indexUnitBasePath { + if let remappedPath = generateIndexOutputPath(from: $0, basePath: indexUnitBasePath) { + return remappedPath + } } + return $0 } - return $0 + objects.append(contentsOf: entries) + } + guard let indexStoreLibraryPath = options.indexStoreLibraryPath else { + outputDelegate.emitError("Test discovery was requested, but failed to lookup index store library in toolchain") + return .failed + } + let indexStoreAPI = try IndexStoreAPI(dylib: indexStoreLibraryPath) + for indexStore in options.indexStore { + let store = try IndexStore.open(store: indexStore, api: indexStoreAPI) + let testInfo = try store.listTests(in: objects) + tests.append(contentsOf: testInfo) } - objects.append(contentsOf: entries) - } - let indexStoreAPI = try IndexStoreAPI(dylib: options.indexStoreLibraryPath) - for indexStore in options.indexStore { - let store = try IndexStore.open(store: indexStore, api: indexStoreAPI) - let testInfo = try store.listTests(in: objects) - tests.append(contentsOf: testInfo) } try executionDelegate.fs.write(options.output, contents: ByteString(encodingAsUTF8: """ @@ -52,8 +58,8 @@ class TestEntryPointGenerationTaskAction: TaskAction { \(testObservationFragment) - import XCTest - \(discoveredTestsFragment(tests: tests)) + public import XCTest + \(discoveredTestsFragment(tests: tests, options: options)) @main @available(macOS 10.15, iOS 11, watchOS 4, tvOS 11, visionOS 1, *) @@ -94,16 +100,7 @@ class TestEntryPointGenerationTaskAction: TaskAction { } } #endif - if testingLibrary == "xctest" { - #if !os(Windows) && \(options.enableExperimentalTestOutput) - _ = Self.testOutputPath().map { SwiftPMXCTestObserver(testOutputPath: testOutputPath) } - #endif - #if os(WASI) - await XCTMain(__allDiscoveredTests()) as Never - #else - XCTMain(__allDiscoveredTests()) as Never - #endif - } + \(xctestFragment(enableExperimentalTestOutput: options.enableExperimentalTestOutput, disable: !options.discoverTests)) } #else static func main() async { @@ -113,16 +110,7 @@ class TestEntryPointGenerationTaskAction: TaskAction { await Testing.__swiftPMEntryPoint() as Never } #endif - if testingLibrary == "xctest" { - #if !os(Windows) && \(options.enableExperimentalTestOutput) - _ = Self.testOutputPath().map { SwiftPMXCTestObserver(testOutputPath: testOutputPath) } - #endif - #if os(WASI) - await XCTMain(__allDiscoveredTests()) as Never - #else - XCTMain(__allDiscoveredTests()) as Never - #endif - } + \(xctestFragment(enableExperimentalTestOutput: options.enableExperimentalTestOutput, disable: !options.discoverTests)) } #endif } @@ -137,14 +125,18 @@ class TestEntryPointGenerationTaskAction: TaskAction { private struct Options: ParsableArguments { @Option var output: Path - @Option var indexStoreLibraryPath: Path - @Option var linkerFilelist: [Path] - @Option var indexStore: [Path] - @Option var indexUnitBasePath: [Path] + @Option var indexStoreLibraryPath: Path? = nil + @Option() var linkerFilelist: [Path] = [] + @Option var indexStore: [Path] = [] + @Option var indexUnitBasePath: [Path] = [] @Flag var enableExperimentalTestOutput: Bool = false + @Flag var discoverTests: Bool = false } - private func discoveredTestsFragment(tests: [IndexStore.TestCaseClass]) -> String { + private func discoveredTestsFragment(tests: [IndexStore.TestCaseClass], options: Options) -> String { + guard options.discoverTests else { + return "" + } var fragment = "" for moduleName in Set(tests.map { $0.module }).sorted() { fragment += "@testable import \(moduleName)\n" @@ -174,11 +166,29 @@ class TestEntryPointGenerationTaskAction: TaskAction { return fragment } + private func xctestFragment(enableExperimentalTestOutput: Bool, disable: Bool) -> String { + guard !disable else { + return "" + } + return """ + if testingLibrary == "xctest" { + #if !os(Windows) && \(enableExperimentalTestOutput) + _ = Self.testOutputPath().map { SwiftPMXCTestObserver(testOutputPath: testOutputPath) } + #endif + #if os(WASI) + await XCTMain(__allDiscoveredTests()) as Never + #else + XCTMain(__allDiscoveredTests()) as Never + #endif + } + """ + } + private var testObservationFragment: String = """ #if !os(Windows) // Test observation is not supported on Windows - import Foundation - import XCTest + public import Foundation + public import XCTest public final class SwiftPMXCTestObserver: NSObject { let testOutputPath: String @@ -562,7 +572,7 @@ class TestEntryPointGenerationTaskAction: TaskAction { } } - import XCTest + public import XCTest #if canImport(Darwin) // XCTAttachment is unavailable in swift-corelibs-xctest. extension TestAttachment { diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift index e8e97f25..ec9dbfb7 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift @@ -19,9 +19,13 @@ final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecId override func commandLineFromTemplate(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, optionContext: (any DiscoveredCommandLineToolSpecInfo)?, specialArgs: [String] = [], lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) async -> [CommandLineArgument] { var args = await super.commandLineFromTemplate(cbc, delegate, optionContext: optionContext, specialArgs: specialArgs, lookup: lookup) - for (toolchainPath, toolchainLibrarySearchPath) in cbc.producer.toolchains.map({ ($0.path, $0.librarySearchPaths) }) { - if let path = toolchainLibrarySearchPath.findLibrary(operatingSystem: cbc.producer.hostOperatingSystem, basename: "IndexStore") { - args.append(contentsOf: ["--index-store-library-path", .path(path)]) + if cbc.scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS) { + args.append("--discover-tests") + for toolchainLibrarySearchPath in cbc.producer.toolchains.map({ $0.librarySearchPaths }) { + if let path = toolchainLibrarySearchPath.findLibrary(operatingSystem: cbc.producer.hostOperatingSystem, basename: "IndexStore") { + args.append(contentsOf: ["--index-store-library-path", .path(path)]) + break + } } for input in cbc.inputs { if input.fileType.conformsTo(identifier: "text") { @@ -43,12 +47,14 @@ final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecId public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, indexStorePaths: [Path], indexUnitBasePaths: [Path]) async { var commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: nil) - for indexStorePath in indexStorePaths { - commandLine.append(contentsOf: ["--index-store", .path(indexStorePath)]) - } - - for basePath in indexUnitBasePaths { - commandLine.append(contentsOf: ["--index-unit-base-path", .path(basePath)]) + if cbc.scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS) { + for indexStorePath in indexStorePaths { + commandLine.append(contentsOf: ["--index-store", .path(indexStorePath)]) + } + + for basePath in indexUnitBasePaths { + commandLine.append(contentsOf: ["--index-unit-base-path", .path(basePath)]) + } } delegate.createTask( diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index cf9ebad8..8bac3097 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -382,6 +382,76 @@ fileprivate struct BuildOperationTests: CoreBasedTests { } } + @Test(.requireSDKs(.macOS)) + func unitTestWithGeneratedEntryPointViaMacOSOverride() async throws { + try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in + let testProject = try await TestProject( + "TestProject", + sourceRoot: tmpDir, + groupTree: TestGroup( + "SomeFiles", + children: [ + TestFile("test.swift"), + ]), + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "ARCHS": "$(ARCHS_STANDARD)", + "CODE_SIGNING_ALLOWED": "NO", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SDKROOT": "$(HOST_PLATFORM)", + "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)", + "SWIFT_VERSION": swiftVersion, + "INDEX_DATA_STORE_DIR": "\(tmpDir.join("index").str)", + "LINKER_DRIVER": "swiftc" + ]) + ], + targets: [ + TestStandardTarget( + "MyTests", + type: .unitTest, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "GENERATE_TEST_ENTRYPOINTS_FOR_BUNDLES": "YES" + ]) + ], + buildPhases: [ + TestSourcesBuildPhase(["test.swift"]), + ], + ), + ]) + let core = try await getCore() + let tester = try await BuildOperationTester(core, testProject, simulated: false) + try localFS.createDirectory(tmpDir.join("index")) + let projectDir = tester.workspace.projects[0].sourceRoot + + try await tester.fs.writeFileContents(projectDir.join("test.swift")) { stream in + stream <<< """ + import Testing + import XCTest + @Suite struct MySuite { + @Test func myTest() { + #expect(42 == 42) + } + } + + final class MYXCTests: XCTestCase { + func testFoo() { + XCTAssertTrue(true) + } + } + """ + } + + let destination: RunDestinationInfo = .host + try await tester.checkBuild(runDestination: destination, persistent: true) { results in + results.checkNoErrors() + results.checkTask(.matchRuleType("GenerateTestEntryPoint")) { task in + task.checkCommandLineMatches(["builtin-generateTestEntryPoint", "--output", .suffix("test_entry_point.swift")]) + } + } + } + } + @Test(.requireSDKs(.host), .skipHostOS(.macOS), .skipHostOS(.windows, "cannot find testing library")) func unitTestWithGeneratedEntryPoint() async throws { try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in diff --git a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift index 36c60d39..cd27640e 100644 --- a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift @@ -366,7 +366,7 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { await tester.checkBuild(runDestination: .linux, fs: fs) { results in results.checkTarget("UnitTestRunner") { target in results.checkTask(.matchTarget(target), .matchRuleType("GenerateTestEntryPoint")) { task in - task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift"), "--index-store-library-path", .suffix("libIndexStore.so"), "--linker-filelist", .suffix("UnitTestTarget.LinkFileList"), "--index-store", "/index", "--index-unit-base-path", "/tmp/Test/aProject/build"]) + task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift"), "--discover-tests", "--index-store-library-path", .suffix("libIndexStore.so"), "--linker-filelist", .suffix("UnitTestTarget.LinkFileList"), "--index-store", "/index", "--index-unit-base-path", "/tmp/Test/aProject/build"]) task.checkInputs([ .pathPattern(.suffix("UnitTestTarget.LinkFileList")), .pathPattern(.suffix("UnitTestTarget.so")),