diff --git a/Package.swift b/Package.swift index d8ac4ec7..fa470c43 100644 --- a/Package.swift +++ b/Package.swift @@ -232,7 +232,14 @@ let package = Package( swiftSettings: swiftSettings(languageMode: .v6)), .target( name: "SWBUniversalPlatform", - dependencies: ["SWBCore", "SWBMacro", "SWBUtil"], + dependencies: [ + "SWBCore", + "SWBMacro", + "SWBUtil", + "SWBTaskConstruction", + "SWBTaskExecution", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], exclude: ["CMakeLists.txt"], resources: [.process("Specs")], swiftSettings: swiftSettings(languageMode: .v6)), diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index af043a83..b15e8b63 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -721,6 +721,8 @@ public final class BuiltinMacros { public static let GENERATE_MASTER_OBJECT_FILE = BuiltinMacros.declareBooleanMacro("GENERATE_MASTER_OBJECT_FILE") public static let GENERATE_PKGINFO_FILE = BuiltinMacros.declareBooleanMacro("GENERATE_PKGINFO_FILE") 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 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") @@ -1757,6 +1759,8 @@ public final class BuiltinMacros { GENERATE_MASTER_OBJECT_FILE, GENERATE_PKGINFO_FILE, GENERATE_RESOURCE_ACCESSORS, + GENERATE_TEST_ENTRY_POINT, + GENERATED_TEST_ENTRY_POINT_PATH, GENERATE_TEXT_BASED_STUBS, GENERATE_INTERMEDIATE_TEXT_BASED_STUBS, GID, diff --git a/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec b/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec index 484b04ea..eb72ba9c 100644 --- a/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec +++ b/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec @@ -53,6 +53,8 @@ SWIFT_FORCE_STATIC_LINK_STDLIB = NO; // Avoid warning for executable types ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + GENERATE_TEST_ENTRY_POINT = YES; + GENERATED_TEST_ENTRY_POINT_PATH = "$(DERIVED_SOURCES_DIR)/test_entry_point.swift"; }; PackageTypes = ( com.apple.package-type.mach-o-executable // default diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index f53e8aff..080061fd 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -838,6 +838,10 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase result.append((embedInCodeAccessorResult.fileToBuild, embedInCodeAccessorResult.fileToBuildFileType, /* shouldUsePrefixHeader */ false)) } + if scope.evaluate(BuiltinMacros.GENERATE_TEST_ENTRY_POINT) { + result.append((scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_PATH), context.lookupFileType(fileName: "sourcecode.swift")!, /* shouldUsePrefixHeader */ false)) + } + return result }()) diff --git a/Sources/SWBUniversalPlatform/CMakeLists.txt b/Sources/SWBUniversalPlatform/CMakeLists.txt index 0ca2f13c..f331ab96 100644 --- a/Sources/SWBUniversalPlatform/CMakeLists.txt +++ b/Sources/SWBUniversalPlatform/CMakeLists.txt @@ -34,11 +34,17 @@ add_library(SWBUniversalPlatform STATIC CppTool.swift DiffTool.swift LexCompiler.swift + TestEntryPointGenerationTaskAction.swift + TestEntryPointGenerationTool.swift + TestEntryPointTaskProducer.swift YaccCompiler.swift Plugin.swift) target_link_libraries(SWBUniversalPlatform PUBLIC SWBCore SWBMacro - SWBUtil) + SWBUtil + SWBTaskConstruction + SWBTaskExecution + ArgumentParser) target_sources(SWBUniversalPlatform PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/resource_bundle_accessor.swift") diff --git a/Sources/SWBUniversalPlatform/Plugin.swift b/Sources/SWBUniversalPlatform/Plugin.swift index bd278de3..b50a687c 100644 --- a/Sources/SWBUniversalPlatform/Plugin.swift +++ b/Sources/SWBUniversalPlatform/Plugin.swift @@ -13,9 +13,13 @@ public import SWBUtil import SWBCore import Foundation +import SWBTaskConstruction +import SWBTaskExecution @PluginExtensionSystemActor public func initializePlugin(_ manager: PluginManager) { manager.register(UniversalPlatformSpecsExtension(), type: SpecificationsExtensionPoint.self) + manager.register(UniversalPlatformTaskProducerExtension(), type: TaskProducerExtensionPoint.self) + manager.register(UniversalPlatformTaskActionExtension(), type: TaskActionExtensionPoint.self) } struct UniversalPlatformSpecsExtension: SpecificationsExtension { @@ -26,6 +30,7 @@ struct UniversalPlatformSpecsExtension: SpecificationsExtension { CppToolSpec.self, LexCompilerSpec.self, YaccCompilerSpec.self, + TestEntryPointGenerationToolSpec.self, ] } @@ -44,3 +49,35 @@ struct UniversalPlatformSpecsExtension: SpecificationsExtension { findResourceBundle(nameWhenInstalledInToolchain: "SwiftBuild_SWBUniversalPlatform", resourceSearchPaths: resourceSearchPaths, defaultBundle: Bundle.module)?.resourceURL.map { [$0] } ?? [] } } + +struct UniversalPlatformTaskProducerExtension: TaskProducerExtension { + func createPreSetupTaskProducers(_ context: SWBTaskConstruction.TaskProducerContext) -> [any SWBTaskConstruction.TaskProducer] { + [] + } + + struct TestEntryPointTaskProducerFactory: TaskProducerFactory { + var name: String { + "TestEntryPointTaskProducerFactory" + } + + func createTaskProducer(_ context: SWBTaskConstruction.TargetTaskProducerContext, startPhaseNodes: [SWBCore.PlannedVirtualNode], endPhaseNode: SWBCore.PlannedVirtualNode) -> any SWBTaskConstruction.TaskProducer { + TestEntryPointTaskProducer(context, phaseStartNodes: startPhaseNodes, phaseEndNode: endPhaseNode) + } + } + + var setupTaskProducers: [any SWBTaskConstruction.TaskProducerFactory] { + [TestEntryPointTaskProducerFactory()] + } + + var unorderedPostSetupTaskProducers: [any SWBTaskConstruction.TaskProducerFactory] { [] } + + var unorderedPostBuildPhasesTaskProducers: [any SWBTaskConstruction.TaskProducerFactory] { [] } + + var globalTaskProducers: [any SWBTaskConstruction.GlobalTaskProducerFactory] { [] } +} + +struct UniversalPlatformTaskActionExtension: TaskActionExtension { + var taskActionImplementations: [SWBUtil.SerializableTypeCode : any SWBUtil.PolymorphicSerializable.Type] { + [41: TestEntryPointGenerationTaskAction.self] + } +} diff --git a/Sources/SWBUniversalPlatform/Specs/TestEntryPointGenerator.xcspec b/Sources/SWBUniversalPlatform/Specs/TestEntryPointGenerator.xcspec new file mode 100644 index 00000000..103d4f00 --- /dev/null +++ b/Sources/SWBUniversalPlatform/Specs/TestEntryPointGenerator.xcspec @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +( + { Identifier = "org.swift.test-entry-point-generator"; + Type = Compiler; + Name = "Generate Test Entry Point"; + Description = "Generates the entry point for a test target."; + CommandLine = "builtin-generateTestEntryPoint [options] --output $(OutputPath)"; + RuleName = "GenerateTestEntryPoint $(OutputPath)"; + ExecDescription = "Generate entry point for $(PRODUCT_NAME)"; + Outputs = ( + "$(OutputPath)" + ); + Options = ( + ); + } +) diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift new file mode 100644 index 00000000..57dff473 --- /dev/null +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SWBUtil +import SWBCore +import SWBTaskExecution +import ArgumentParser + +class TestEntryPointGenerationTaskAction: TaskAction { + override class var toolIdentifier: String { + "TestEntryPointGenerationTaskAction" + } + + override func performTaskAction(_ task: any ExecutableTask, dynamicExecutionDelegate: any DynamicTaskExecutionDelegate, executionDelegate: any TaskExecutionDelegate, clientDelegate: any TaskExecutionClientDelegate, outputDelegate: any TaskOutputDelegate) async -> CommandResult { + do { + let options = try Options.parse(Array(task.commandLineAsStrings.dropFirst())) + try executionDelegate.fs.write(options.output, contents: #""" + #if canImport(Testing) + import Testing + #endif + + @main + @available(macOS 10.15, iOS 11, watchOS 4, tvOS 11, visionOS 1, *) + @available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings") + struct Runner { + private static func testingLibrary() -> String { + var iterator = CommandLine.arguments.makeIterator() + while let argument = iterator.next() { + if argument == "--testing-library", let libraryName = iterator.next() { + return libraryName.lowercased() + } + } + + // Fallback if not specified: run XCTest (legacy behavior) + return "xctest" + } + + #if os(Linux) + @_silgen_name("$ss13_runAsyncMainyyyyYaKcF") + private static func _runAsyncMain(_ asyncFun: @Sendable @escaping () async throws -> ()) + + static func main() { + let testingLibrary = Self.testingLibrary() + #if canImport(Testing) + if testingLibrary == "swift-testing" { + _runAsyncMain { + await Testing.__swiftPMEntryPoint() as Never + } + } + #endif + } + #else + static func main() async { + let testingLibrary = Self.testingLibrary() + #if canImport(Testing) + if testingLibrary == "swift-testing" { + await Testing.__swiftPMEntryPoint() as Never + } + #endif + } + #endif + } + """#) + return .succeeded + } catch { + outputDelegate.emitError("\(error)") + return .failed + } + } +} + +private struct Options: ParsableArguments { + @Option var output: Path +} diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift new file mode 100644 index 00000000..007611ad --- /dev/null +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SWBUtil +import SWBMacro +import SWBCore + +final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable { + static let identifier = "org.swift.test-entry-point-generator" + + override func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? { + TestEntryPointGenerationTaskAction() + } +} diff --git a/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift b/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift new file mode 100644 index 00000000..4d589270 --- /dev/null +++ b/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SWBCore +import SWBTaskConstruction + +class TestEntryPointTaskProducer: PhasedTaskProducer, TaskProducer { + func generateTasks() async -> [any PlannedTask] { + var tasks: [any PlannedTask] = [] + if context.settings.globalScope.evaluate(BuiltinMacros.GENERATE_TEST_ENTRY_POINT) { + await self.appendGeneratedTasks(&tasks) { delegate in + let scope = context.settings.globalScope + let outputPath = scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_PATH) + let cbc = CommandBuildContext(producer: context, scope: scope, inputs: [], outputs: [outputPath]) + await context.testEntryPointGenerationToolSpec.constructTasks(cbc, delegate) + } + } + return tasks + } +} + +extension TaskProducerContext { + var testEntryPointGenerationToolSpec: TestEntryPointGenerationToolSpec { + return workspaceContext.core.specRegistry.getSpec(TestEntryPointGenerationToolSpec.identifier, domain: domain) as! TestEntryPointGenerationToolSpec + } +} diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index e69141fb..046c9ef2 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -397,6 +397,103 @@ fileprivate struct BuildOperationTests: CoreBasedTests { } } + @Test(.skipHostOS(.macOS), .skipHostOS(.windows, "cannot find testing library")) + func unitTestWithGeneratedEntryPoint() async throws { + try await withTemporaryDirectory { (tmpDir: Path) in + let testProject = try await TestProject( + "TestProject", + sourceRoot: tmpDir, + groupTree: TestGroup( + "SomeFiles", + children: [ + TestFile("library.swift"), + 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, + ]) + ], + targets: [ + TestStandardTarget( + "test", + type: .unitTest, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "LD_RUNPATH_SEARCH_PATHS": "@loader_path/", + ]) + ], + buildPhases: [ + TestSourcesBuildPhase(["test.swift"]), + TestFrameworksBuildPhase([ + TestBuildFile(.target("library")), + ]) + ], + dependencies: [ + "library" + ] + ), + TestStandardTarget( + "library", + type: .dynamicLibrary, + buildConfigurations: [ + TestBuildConfiguration("Debug", buildSettings: [ + "DYLIB_INSTALL_NAME_BASE": "$ORIGIN", + + // FIXME: Find a way to make these default + "EXECUTABLE_PREFIX": "lib", + "EXECUTABLE_PREFIX[sdk=windows*]": "", + ]) + ], + buildPhases: [ + TestSourcesBuildPhase(["library.swift"]), + ], + ), + ]) + let core = try await getCore() + let tester = try await BuildOperationTester(core, testProject, simulated: false) + + let projectDir = tester.workspace.projects[0].sourceRoot + + try await tester.fs.writeFileContents(projectDir.join("library.swift")) { stream in + stream <<< "public func foo() -> Int { 42 }\n" + } + + try await tester.fs.writeFileContents(projectDir.join("test.swift")) { stream in + stream <<< """ + import Testing + import library + @Suite struct MySuite { + @Test func myTest() async throws { + #expect(foo() == 42) + } + } + """ + } + + let destination: RunDestinationInfo = .host + try await tester.checkBuild(runDestination: destination, persistent: true) { results in + results.checkNoErrors() + + let toolchain = try #require(try await getCore().toolchainRegistry.defaultToolchain) + let environment: Environment + if destination.platform == "linux" { + environment = ["LD_LIBRARY_PATH": toolchain.path.join("usr/lib/swift/linux").str] + } else { + environment = .init() + } + + let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "test.xctest")).str), arguments: ["--testing-library", "swift-testing"], environment: environment) + #expect(String(decoding: executionResult.stderr, as: UTF8.self).contains("Test run started")) + } + } + } + /// Check that environment variables are propagated from the user environment correctly. @Test(.requireSDKs(.host), .skipHostOS(.windows), .requireSystemPackages(apt: "yacc", yum: "byacc")) func userEnvironment() async throws { diff --git a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift index c42f9311..338abfc7 100644 --- a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift @@ -301,6 +301,67 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { } } + @Test(.requireSDKs(.linux)) + func unitTestTarget_linux() async throws { + let swiftCompilerPath = try await self.swiftCompilerPath + let swiftVersion = try await self.swiftVersion + let testProject = TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", + children: [ + TestFile("TestOne.swift"), + TestFile("TestTwo.swift"), + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "CODE_SIGN_IDENTITY": "", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SDKROOT": "linux", + "SWIFT_VERSION": swiftVersion, + ]), + ], + targets: [ + TestStandardTarget( + "UnitTestTarget", + type: .unitTest, + buildConfigurations: [ + TestBuildConfiguration("Debug", + buildSettings: [:]), + ], + buildPhases: [ + TestSourcesBuildPhase([ + "TestOne.swift", + "TestTwo.swift", + ]), + ], + dependencies: [] + ), + ]) + let core = try await getCore() + let tester = try TaskConstructionTester(core, testProject) + + let fs = PseudoFS() + + try await fs.writeFileContents(swiftCompilerPath) { $0 <<< "binary" } + + await tester.checkBuild(runDestination: .linux, fs: fs) { results in + results.checkTarget("UnitTestTarget") { target in + results.checkTask(.matchTarget(target), .matchRuleType("GenerateTestEntryPoint")) { task in + task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift")]) + task.checkOutputs([.pathPattern(.suffix("test_entry_point.swift"))]) + } + results.checkTask(.matchTarget(target), .matchRuleType("SwiftDriver Compilation")) { task in + task.checkInputs(contain: [.pathPattern(.suffix("test_entry_point.swift"))]) + } + } + + results.checkNoDiagnostics() + } + } + // MARK: Application test target using TEST_HOST