diff --git a/Sources/SWBApplePlatform/Specs/DarwinProductTypes.xcspec b/Sources/SWBApplePlatform/Specs/DarwinProductTypes.xcspec index e1bc5a17..d6e43e3f 100644 --- a/Sources/SWBApplePlatform/Specs/DarwinProductTypes.xcspec +++ b/Sources/SWBApplePlatform/Specs/DarwinProductTypes.xcspec @@ -461,4 +461,17 @@ ); Platforms = (driverkit); }, + { + _Domain = darwin; + Type = ProductType; + Identifier = com.apple.product-type.tool.swiftpm-test-runner; + Name = "SwiftPM Unit Test Runner"; + Description = "SwiftPM Unit Test Runner"; + DefaultBuildProperties = { + __SKIP_BUILD = YES; + }; + PackageTypes = ( + com.apple.package-type.mach-o-executable + ); + }, ) diff --git a/Sources/SWBCSupport/IndexStore.h b/Sources/SWBCSupport/IndexStore.h new file mode 100644 index 00000000..c1546932 --- /dev/null +++ b/Sources/SWBCSupport/IndexStore.h @@ -0,0 +1,190 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef INDEXSTORE_H +#define INDEXSTORE_H + +#include +#include +#include +#include + +typedef void *indexstore_error_t; + +typedef struct { + const char *data; + size_t length; +} indexstore_string_ref_t; + +typedef void *indexstore_t; +typedef void *indexstore_symbol_t; + +typedef enum { + INDEXSTORE_SYMBOL_KIND_UNKNOWN = 0, + INDEXSTORE_SYMBOL_KIND_MODULE = 1, + INDEXSTORE_SYMBOL_KIND_NAMESPACE = 2, + INDEXSTORE_SYMBOL_KIND_NAMESPACEALIAS = 3, + INDEXSTORE_SYMBOL_KIND_MACRO = 4, + INDEXSTORE_SYMBOL_KIND_ENUM = 5, + INDEXSTORE_SYMBOL_KIND_STRUCT = 6, + INDEXSTORE_SYMBOL_KIND_CLASS = 7, + INDEXSTORE_SYMBOL_KIND_PROTOCOL = 8, + INDEXSTORE_SYMBOL_KIND_EXTENSION = 9, + INDEXSTORE_SYMBOL_KIND_UNION = 10, + INDEXSTORE_SYMBOL_KIND_TYPEALIAS = 11, + INDEXSTORE_SYMBOL_KIND_FUNCTION = 12, + INDEXSTORE_SYMBOL_KIND_VARIABLE = 13, + INDEXSTORE_SYMBOL_KIND_FIELD = 14, + INDEXSTORE_SYMBOL_KIND_ENUMCONSTANT = 15, + INDEXSTORE_SYMBOL_KIND_INSTANCEMETHOD = 16, + INDEXSTORE_SYMBOL_KIND_CLASSMETHOD = 17, + INDEXSTORE_SYMBOL_KIND_STATICMETHOD = 18, + INDEXSTORE_SYMBOL_KIND_INSTANCEPROPERTY = 19, + INDEXSTORE_SYMBOL_KIND_CLASSPROPERTY = 20, + INDEXSTORE_SYMBOL_KIND_STATICPROPERTY = 21, + INDEXSTORE_SYMBOL_KIND_CONSTRUCTOR = 22, + INDEXSTORE_SYMBOL_KIND_DESTRUCTOR = 23, + INDEXSTORE_SYMBOL_KIND_CONVERSIONFUNCTION = 24, + INDEXSTORE_SYMBOL_KIND_PARAMETER = 25, + INDEXSTORE_SYMBOL_KIND_USING = 26, + + INDEXSTORE_SYMBOL_KIND_COMMENTTAG = 1000, +} indexstore_symbol_kind_t; + +typedef enum { + INDEXSTORE_SYMBOL_PROPERTY_GENERIC = 1 << 0, + INDEXSTORE_SYMBOL_PROPERTY_TEMPLATE_PARTIAL_SPECIALIZATION = 1 << 1, + INDEXSTORE_SYMBOL_PROPERTY_TEMPLATE_SPECIALIZATION = 1 << 2, + INDEXSTORE_SYMBOL_PROPERTY_UNITTEST = 1 << 3, + INDEXSTORE_SYMBOL_PROPERTY_IBANNOTATED = 1 << 4, + INDEXSTORE_SYMBOL_PROPERTY_IBOUTLETCOLLECTION = 1 << 5, + INDEXSTORE_SYMBOL_PROPERTY_GKINSPECTABLE = 1 << 6, + INDEXSTORE_SYMBOL_PROPERTY_LOCAL = 1 << 7, + INDEXSTORE_SYMBOL_PROPERTY_PROTOCOL_INTERFACE = 1 << 8, + INDEXSTORE_SYMBOL_PROPERTY_SWIFT_ASYNC = 1 << 16, +} indexstore_symbol_property_t; + +typedef enum { + INDEXSTORE_SYMBOL_ROLE_DECLARATION = 1 << 0, + INDEXSTORE_SYMBOL_ROLE_DEFINITION = 1 << 1, + INDEXSTORE_SYMBOL_ROLE_REFERENCE = 1 << 2, + INDEXSTORE_SYMBOL_ROLE_READ = 1 << 3, + INDEXSTORE_SYMBOL_ROLE_WRITE = 1 << 4, + INDEXSTORE_SYMBOL_ROLE_CALL = 1 << 5, + INDEXSTORE_SYMBOL_ROLE_DYNAMIC = 1 << 6, + INDEXSTORE_SYMBOL_ROLE_ADDRESSOF = 1 << 7, + INDEXSTORE_SYMBOL_ROLE_IMPLICIT = 1 << 8, + INDEXSTORE_SYMBOL_ROLE_UNDEFINITION = 1 << 19, + + // Relation roles. + INDEXSTORE_SYMBOL_ROLE_REL_CHILDOF = 1 << 9, + INDEXSTORE_SYMBOL_ROLE_REL_BASEOF = 1 << 10, + INDEXSTORE_SYMBOL_ROLE_REL_OVERRIDEOF = 1 << 11, + INDEXSTORE_SYMBOL_ROLE_REL_RECEIVEDBY = 1 << 12, + INDEXSTORE_SYMBOL_ROLE_REL_CALLEDBY = 1 << 13, + INDEXSTORE_SYMBOL_ROLE_REL_EXTENDEDBY = 1 << 14, + INDEXSTORE_SYMBOL_ROLE_REL_ACCESSOROF = 1 << 15, + INDEXSTORE_SYMBOL_ROLE_REL_CONTAINEDBY = 1 << 16, + INDEXSTORE_SYMBOL_ROLE_REL_IBTYPEOF = 1 << 17, + INDEXSTORE_SYMBOL_ROLE_REL_SPECIALIZATIONOF = 1 << 18, +} indexstore_symbol_role_t; + +typedef void *indexstore_unit_dependency_t; + +typedef enum { + INDEXSTORE_UNIT_DEPENDENCY_UNIT = 1, + INDEXSTORE_UNIT_DEPENDENCY_RECORD = 2, + INDEXSTORE_UNIT_DEPENDENCY_FILE = 3, +} indexstore_unit_dependency_kind_t; + +typedef void *indexstore_symbol_relation_t; +typedef void *indexstore_occurrence_t; +typedef void *indexstore_record_reader_t; +typedef void *indexstore_unit_reader_t; + +typedef struct { + const char * + (*error_get_description)(indexstore_error_t); + + void + (*error_dispose)(indexstore_error_t); + + indexstore_t + (*store_create)(const char *store_path, indexstore_error_t *error); + + void + (*store_dispose)(indexstore_t); + + size_t + (*store_get_unit_name_from_output_path)(indexstore_t store, + const char *output_path, + char *name_buf, + size_t buf_size); + + indexstore_symbol_kind_t + (*symbol_get_kind)(indexstore_symbol_t); + + uint64_t + (*symbol_get_properties)(indexstore_symbol_t); + + indexstore_string_ref_t + (*symbol_get_name)(indexstore_symbol_t); + + uint64_t + (*symbol_relation_get_roles)(indexstore_symbol_relation_t); + + indexstore_symbol_t + (*symbol_relation_get_symbol)(indexstore_symbol_relation_t); + + indexstore_symbol_t + (*occurrence_get_symbol)(indexstore_occurrence_t); + + bool + (*occurrence_relations_apply_f)(indexstore_occurrence_t, + void *context, + bool(*applier)(void *context, indexstore_symbol_relation_t symbol_rel)); + + indexstore_record_reader_t + (*record_reader_create)(indexstore_t store, const char *record_name, + indexstore_error_t *error); + + void + (*record_reader_dispose)(indexstore_record_reader_t); + + bool + (*record_reader_occurrences_apply_f)(indexstore_record_reader_t, + void *context, + bool(*applier)(void *context, indexstore_occurrence_t occur)); + + indexstore_unit_reader_t + (*unit_reader_create)(indexstore_t store, const char *unit_name, + indexstore_error_t *error); + + void + (*unit_reader_dispose)(indexstore_unit_reader_t); + + indexstore_string_ref_t + (*unit_reader_get_module_name)(indexstore_unit_reader_t); + + indexstore_unit_dependency_kind_t + (*unit_dependency_get_kind)(indexstore_unit_dependency_t); + + indexstore_string_ref_t + (*unit_dependency_get_name)(indexstore_unit_dependency_t); + + bool + (*unit_reader_dependencies_apply_f)(indexstore_unit_reader_t, + void *context, + bool(*applier)(void *context, indexstore_unit_dependency_t)); +} swiftbuild_indexstore_functions_t; + +#endif diff --git a/Sources/SWBCSupport/SWBCSupport.h b/Sources/SWBCSupport/SWBCSupport.h index c020472c..18591c94 100644 --- a/Sources/SWBCSupport/SWBCSupport.h +++ b/Sources/SWBCSupport/SWBCSupport.h @@ -21,6 +21,7 @@ #include "CLibclang.h" #include "CLibRemarksHelper.h" +#include "IndexStore.h" #include "PluginAPI.h" #include "PluginAPI_functions.h" #include "PluginAPI_types.h" diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 7fbab73d..21be4140 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -750,6 +750,7 @@ public final class BuiltinMacros { public static let INDEX_PREPARED_TARGET_MARKER_PATH = BuiltinMacros.declareStringMacro("INDEX_PREPARED_TARGET_MARKER_PATH") public static let INDEX_REGULAR_BUILD_PRODUCTS_DIR = BuiltinMacros.declareStringMacro("INDEX_REGULAR_BUILD_PRODUCTS_DIR") public static let INDEX_REGULAR_BUILD_INTERMEDIATES_DIR = BuiltinMacros.declareStringMacro("INDEX_REGULAR_BUILD_INTERMEDIATES_DIR") + public static let INDEX_STORE_LIBRARY_PATH = BuiltinMacros.declarePathMacro("INDEX_STORE_LIBRARY_PATH") public static let INFOPLIST_ENFORCE_MINIMUM_OS = BuiltinMacros.declareBooleanMacro("INFOPLIST_ENFORCE_MINIMUM_OS") public static let INFOPLIST_EXPAND_BUILD_SETTINGS = BuiltinMacros.declareBooleanMacro("INFOPLIST_EXPAND_BUILD_SETTINGS") public static let INFOPLIST_FILE = BuiltinMacros.declarePathMacro("INFOPLIST_FILE") @@ -1797,6 +1798,7 @@ public final class BuiltinMacros { INDEX_PREPARED_TARGET_MARKER_PATH, INDEX_REGULAR_BUILD_PRODUCTS_DIR, INDEX_REGULAR_BUILD_INTERMEDIATES_DIR, + INDEX_STORE_LIBRARY_PATH, INDEX_ENABLE_DATA_STORE, INDEX_PRECOMPS_DIR, INFOPLIST_ENFORCE_MINIMUM_OS, diff --git a/Sources/SWBCore/SpecImplementations/ProductTypes.swift b/Sources/SWBCore/SpecImplementations/ProductTypes.swift index 7cb6a1d5..28415e4b 100644 --- a/Sources/SWBCore/SpecImplementations/ProductTypes.swift +++ b/Sources/SWBCore/SpecImplementations/ProductTypes.swift @@ -321,7 +321,7 @@ public class ProductTypeSpec : Spec, SpecType, @unchecked Sendable { } /// Returns whether the product type supports embedding Swift standard libraries inside it. - public var supportsEmbeddingSwiftStandardLibraries: Bool { + public func supportsEmbeddingSwiftStandardLibraries(producer: CommandProducer) -> Bool { // Most product types don't support having the Swift libraries embedded in them. return false } @@ -381,7 +381,7 @@ public final class ApplicationProductTypeSpec : BundleProductTypeSpec, @unchecke return "PBXApplicationProductType" } - public override var supportsEmbeddingSwiftStandardLibraries: Bool { + public override func supportsEmbeddingSwiftStandardLibraries(producer: CommandProducer) -> Bool { return true } @@ -602,8 +602,8 @@ public final class XCTestBundleProductTypeSpec : BundleProductTypeSpec, @uncheck super.init(parser, basedOnSpec) } - public override var supportsEmbeddingSwiftStandardLibraries: Bool { - return true + public override func supportsEmbeddingSwiftStandardLibraries(producer: CommandProducer) -> Bool { + return producer.isApplePlatform } public class func usesXCTRunner(_ scope: MacroEvaluationScope) -> Bool { @@ -649,7 +649,7 @@ public final class XCTestBundleProductTypeSpec : BundleProductTypeSpec, @uncheck var (tableOpt, warnings, errors) = super.overridingBuildSettings(scope, platform: platform) var table = tableOpt ?? MacroValueAssignmentTable(namespace: scope.namespace) - let isDeviceBuild = platform?.isDeploymentPlatform == true && platform?.identifier != "com.apple.platform.macosx" + let isDeviceBuild = platform?.isDeploymentPlatform == true && platform?.name != scope.evaluate(BuiltinMacros.HOST_PLATFORM) if isDeviceBuild { // For tests running on devices (not simulators) we always want to generate dSYMs so that symbolication can give file and line information about test failures. table.push(BuiltinMacros.DEBUG_INFORMATION_FORMAT, literal: "dwarf-with-dsym") diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 70e9ad3a..687820ec 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -3763,6 +3763,9 @@ public extension BuildPhaseWithBuildFiles { /// - Returns: If the build phase contains any Swift source files that are not filtered out via the platform filter or excluded source file name patterns. func containsSwiftSources(_ referenceLookupContext: any ReferenceLookupContext, _ specLookupContext: any SpecLookupContext, _ scope: MacroEvaluationScope, _ filePathResolver: FilePathResolver) -> Bool { guard let swiftFileType = specLookupContext.lookupFileType(identifier: "sourcecode.swift") else { return false } + if scope.evaluate(BuiltinMacros.GENERATE_TEST_ENTRY_POINT) { + return true + } return containsFiles(ofType: swiftFileType, referenceLookupContext, specLookupContext, scope, filePathResolver) } } diff --git a/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec b/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec index eb72ba9c..00bc75d5 100644 --- a/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec +++ b/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec @@ -24,41 +24,18 @@ SortNumber = 0; }, - // Test type bundle (bodged to be a tool) { Domain = generic-unix; Type = ProductType; Identifier = com.apple.product-type.bundle.unit-test; - Class = PBXToolProductType; - Name = "Command-line Tool"; - Description = "Standalone command-line tool"; - DefaultTargetName = "Command-line Tool"; + BasedOn = com.apple.product-type.library.dynamic; DefaultBuildProperties = { - FULL_PRODUCT_NAME = "$(EXECUTABLE_NAME)"; - EXECUTABLE_PREFIX = ""; - EXECUTABLE_SUFFIX = ".xctest"; - REZ_EXECUTABLE = YES; - INSTALL_PATH = "/usr/local/bin"; - FRAMEWORK_FLAG_PREFIX = "-framework"; - LIBRARY_FLAG_PREFIX = "-l"; - LIBRARY_FLAG_NOSPACE = YES; - GCC_DYNAMIC_NO_PIC = NO; - LD_NO_PIE = NO; - GCC_SYMBOLS_PRIVATE_EXTERN = YES; - GCC_INLINES_ARE_PRIVATE_EXTERN = YES; - STRIP_STYLE = "all"; - CODE_SIGNING_ALLOWED = NO; - IsUnitTest = YES; - SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; - 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"; + // Index store data is required to discover XCTest tests + COMPILER_INDEX_STORE_ENABLE = YES; + SWIFT_INDEX_STORE_ENABLE = YES; + // Testability is needed to generate code to invoke discovered XCTest tests + SWIFT_ENABLE_TESTABILITY = YES; }; - PackageTypes = ( - com.apple.package-type.mach-o-executable // default - ); }, // Dynamic library (masquerading as a framework to placate Swift's project structure) diff --git a/Sources/SWBProjectModel/PIFGenerationModel.swift b/Sources/SWBProjectModel/PIFGenerationModel.swift index 698a7343..f3db2bce 100644 --- a/Sources/SWBProjectModel/PIFGenerationModel.swift +++ b/Sources/SWBProjectModel/PIFGenerationModel.swift @@ -295,6 +295,7 @@ public enum PIF { case executable = "com.apple.product-type.tool" case hostBuildTool = "com.apple.product-type.tool.host-build" case unitTest = "com.apple.product-type.bundle.unit-test" + case swiftpmTestRunner = "com.apple.product-type.tool.swiftpm-test-runner" case bundle = "com.apple.product-type.bundle" case packageProduct = "packageProduct" public var asString: String { return rawValue } @@ -1022,6 +1023,7 @@ public enum PIF { public var SWIFT_ADD_TOOLCHAIN_SWIFTSYNTAX_SEARCH_PATHS: String? public var SWIFT_FORCE_STATIC_LINK_STDLIB: String? public var SWIFT_FORCE_DYNAMIC_LINK_STDLIB: String? + public var SWIFT_INDEX_STORE_ENABLE: String? public var SWIFT_INSTALL_OBJC_HEADER: String? public var SWIFT_LOAD_BINARY_MACROS: [String]? public var SWIFT_MODULE_ALIASES: [String]? diff --git a/Sources/SWBQNXPlatform/Specs/QNX.xcspec b/Sources/SWBQNXPlatform/Specs/QNX.xcspec index 3c72c620..aea12e3b 100644 --- a/Sources/SWBQNXPlatform/Specs/QNX.xcspec +++ b/Sources/SWBQNXPlatform/Specs/QNX.xcspec @@ -24,39 +24,18 @@ SortNumber = 0; }, - // Test type bundle (bodged to be a tool) { Domain = qnx; Type = ProductType; Identifier = com.apple.product-type.bundle.unit-test; - Class = PBXToolProductType; - Name = "Command-line Tool"; - Description = "Standalone command-line tool"; - DefaultTargetName = "Command-line Tool"; + BasedOn = com.apple.product-type.library.dynamic; DefaultBuildProperties = { - FULL_PRODUCT_NAME = "$(EXECUTABLE_NAME)"; - EXECUTABLE_PREFIX = ""; - EXECUTABLE_SUFFIX = ".xctest"; - REZ_EXECUTABLE = YES; - INSTALL_PATH = "/usr/local/bin"; - FRAMEWORK_FLAG_PREFIX = "-framework"; - LIBRARY_FLAG_PREFIX = "-l"; - LIBRARY_FLAG_NOSPACE = YES; - GCC_DYNAMIC_NO_PIC = NO; - LD_NO_PIE = NO; - GCC_SYMBOLS_PRIVATE_EXTERN = YES; - GCC_INLINES_ARE_PRIVATE_EXTERN = YES; - STRIP_STYLE = "all"; - CODE_SIGNING_ALLOWED = NO; - IsUnitTest = YES; - SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; - SWIFT_FORCE_STATIC_LINK_STDLIB = NO; - // Avoid warning for executable types - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + // Index store data is required to discover XCTest tests + COMPILER_INDEX_STORE_ENABLE = YES; + SWIFT_INDEX_STORE_ENABLE = YES; + // Testability is needed to generate code to invoke discovered XCTest tests + SWIFT_ENABLE_TESTABILITY = YES; }; - PackageTypes = ( - com.apple.package-type.mach-o-executable // default - ); }, // Dynamic library (masquerading as a framework to placate Swift's project structure) diff --git a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift index 88d2edfe..883073b0 100644 --- a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift +++ b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlan.swift @@ -31,7 +31,7 @@ package protocol GlobalProductPlanDelegate: CoreClientTargetDiagnosticProducingD package final class GlobalProductPlan: GlobalTargetInfoProvider { /// The build plan request. - let planRequest: BuildPlanRequest + package let planRequest: BuildPlanRequest /// The target task info for each configured target. private(set) var targetTaskInfos: [ConfiguredTarget: TargetTaskInfo] diff --git a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift index 1f740497..820a55f5 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/InfoPlistTaskProducer.swift @@ -57,7 +57,7 @@ private extension ProductTypeSpec break } - fatalError("unknown product type") + fatalError("unknown product type \(type(of: self))") } } diff --git a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift index bba8b051..c74e1843 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/SwiftStandardLibrariesTaskProducer.swift @@ -41,7 +41,7 @@ final class SwiftStandardLibrariesTaskProducer: PhasedTaskProducer, TaskProducer let buildingAnySwiftSourceFiles = (context.configuredTarget?.target as? BuildPhaseTarget)?.sourcesBuildPhase?.containsSwiftSources(context.workspaceContext.workspace, context, scope, context.filePathResolver) ?? false // Determine whether we want to embed swift libraries. - var shouldEmbedSwiftLibraries = (buildingAnySwiftSourceFiles && productType.supportsEmbeddingSwiftStandardLibraries) + var shouldEmbedSwiftLibraries = (buildingAnySwiftSourceFiles && productType.supportsEmbeddingSwiftStandardLibraries(producer: context)) // If ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES then we will override our earlier reasoning if the product is a wrapper. if !shouldEmbedSwiftLibraries && scope.evaluate(BuiltinMacros.ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES) { diff --git a/Sources/SWBTestSupport/TestWorkspaces.swift b/Sources/SWBTestSupport/TestWorkspaces.swift index c225eb2c..6837bcf7 100644 --- a/Sources/SWBTestSupport/TestWorkspaces.swift +++ b/Sources/SWBTestSupport/TestWorkspaces.swift @@ -924,6 +924,7 @@ package final class TestStandardTarget: TestInternalTarget, Sendable { case extensionKitExtension case xcodeExtension case unitTest + case swiftpmTestRunner case uiTest case multiDeviceUITest case systemExtension @@ -972,6 +973,8 @@ package final class TestStandardTarget: TestInternalTarget, Sendable { return "com.apple.product-type.xcode-extension" case .unitTest: return "com.apple.product-type.bundle.unit-test" + case .swiftpmTestRunner: + return "com.apple.product-type.tool.swiftpm-test-runner" case .uiTest: return "com.apple.product-type.bundle.ui-testing" case .multiDeviceUITest: @@ -1015,7 +1018,8 @@ package final class TestStandardTarget: TestInternalTarget, Sendable { .appClip: return "\(name).app" case .commandLineTool, - .hostBuildTool: + .hostBuildTool, + .swiftpmTestRunner: return "\(name)" case .framework, .staticFramework: diff --git a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec index 0067050f..bd6cf9c8 100644 --- a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec @@ -312,4 +312,19 @@ IsUnitTest = YES; WantsBundleIdentifierEditing = NO; }, + // SwiftPM test runner + { Type = ProductType; + Identifier = com.apple.product-type.tool.swiftpm-test-runner; + BasedOn = com.apple.product-type.tool; + Name = "SwiftPM Unit Test Runner"; + Description = "SwiftPM Unit Test Runner"; + DefaultBuildProperties = { + ENABLE_TESTING_SEARCH_PATHS = YES; + 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 + ); + }, ) diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift index 57dff473..9765719a 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift @@ -23,10 +23,37 @@ class TestEntryPointGenerationTaskAction: TaskAction { 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: #""" + + 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 + } + } + return $0 + } + 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: """ #if canImport(Testing) import Testing #endif + + \(testObservationFragment) + + import XCTest + \(discoveredTestsFragment(tests: tests)) @main @available(macOS 10.15, iOS 11, watchOS 4, tvOS 11, visionOS 1, *) @@ -44,6 +71,16 @@ class TestEntryPointGenerationTaskAction: TaskAction { return "xctest" } + private static func testOutputPath() -> String? { + var iterator = CommandLine.arguments.makeIterator() + while let argument = iterator.next() { + if argument == "--testing-output-path", let outputPath = iterator.next() { + return outputPath + } + } + return nil + } + #if os(Linux) @_silgen_name("$ss13_runAsyncMainyyyyYaKcF") private static func _runAsyncMain(_ asyncFun: @Sendable @escaping () async throws -> ()) @@ -57,6 +94,16 @@ 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 + } } #else static func main() async { @@ -66,18 +113,564 @@ 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 + } } #endif } - """#) + """)) + return .succeeded } catch { outputDelegate.emitError("\(error)") return .failed } } -} -private struct Options: ParsableArguments { - @Option var output: Path + private struct Options: ParsableArguments { + @Option var output: Path + @Option var indexStoreLibraryPath: Path + @Option var linkerFilelist: [Path] + @Option var indexStore: [Path] + @Option var indexUnitBasePath: [Path] + @Flag var enableExperimentalTestOutput: Bool = false + } + + private func discoveredTestsFragment(tests: [IndexStore.TestCaseClass]) -> String { + var fragment = "" + for moduleName in Set(tests.map { $0.module }).sorted() { + fragment += "@testable import \(moduleName)\n" + } + fragment += """ + @available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings") + public func __allDiscoveredTests() -> [XCTestCaseEntry] { + return [ + + """ + for testClass in tests { + + let testTuples = testClass.testMethods.map { method in + let basename = method.name.hasSuffix("()") ? String(method.name.dropLast(2)) : method.name + if method.isAsync { + return " (\"\(basename)\", asyncTest(\(testClass.name).\(basename)))" + } else { + return " (\"\(basename)\", \(testClass.name).\(basename))" + } + } + fragment += " testCase([\(testTuples.joined(separator: ",\n"))]),\n" + } + fragment += """ + ] + } + """ + return fragment + } + + private var testObservationFragment: String = + """ + #if !os(Windows) // Test observation is not supported on Windows + import Foundation + import XCTest + + public final class SwiftPMXCTestObserver: NSObject { + let testOutputPath: String + + public init(testOutputPath: String) { + self.testOutputPath = testOutputPath + super.init() + XCTestObservationCenter.shared.addTestObserver(self) + } + } + + extension SwiftPMXCTestObserver: XCTestObservation { + private func write(record: any Encodable) { + let lock = FileLock(at: URL(fileURLWithPath: self.testOutputPath + ".lock")) + _ = try? lock.withLock { + self._write(record: record) + } + } + + private func _write(record: any Encodable) { + if let data = try? JSONEncoder().encode(record) { + if let fileHandle = FileHandle(forWritingAtPath: self.testOutputPath) { + defer { fileHandle.closeFile() } + fileHandle.seekToEndOfFile() + fileHandle.write("\\n".data(using: .utf8)!) + fileHandle.write(data) + } else { + _ = try? data.write(to: URL(fileURLWithPath: self.testOutputPath)) + } + } + } + + public func testBundleWillStart(_ testBundle: Bundle) { + let record = TestBundleEventRecord(bundle: .init(testBundle), event: .start) + write(record: TestEventRecord(bundleEvent: record)) + } + + public func testSuiteWillStart(_ testSuite: XCTestSuite) { + let record = TestSuiteEventRecord(suite: .init(testSuite), event: .start) + write(record: TestEventRecord(suiteEvent: record)) + } + + public func testCaseWillStart(_ testCase: XCTestCase) { + let record = TestCaseEventRecord(testCase: .init(testCase), event: .start) + write(record: TestEventRecord(caseEvent: record)) + } + + #if canImport(Darwin) + public func testCase(_ testCase: XCTestCase, didRecord issue: XCTIssue) { + let record = TestCaseFailureRecord(testCase: .init(testCase), issue: .init(issue), failureKind: .unexpected) + write(record: TestEventRecord(caseFailure: record)) + } + + public func testCase(_ testCase: XCTestCase, didRecord expectedFailure: XCTExpectedFailure) { + let record = TestCaseFailureRecord(testCase: .init(testCase), issue: .init(expectedFailure.issue), failureKind: .expected(failureReason: expectedFailure.failureReason)) + write(record: TestEventRecord(caseFailure: record)) + } + #else + public func testCase(_ testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) { + let issue = TestIssue(description: description, inFile: filePath, atLine: lineNumber) + let record = TestCaseFailureRecord(testCase: .init(testCase), issue: issue, failureKind: .unexpected) + write(record: TestEventRecord(caseFailure: record)) + } + #endif + + public func testCaseDidFinish(_ testCase: XCTestCase) { + let record = TestCaseEventRecord(testCase: .init(testCase), event: .finish) + write(record: TestEventRecord(caseEvent: record)) + } + + #if canImport(Darwin) + public func testSuite(_ testSuite: XCTestSuite, didRecord issue: XCTIssue) { + let record = TestSuiteFailureRecord(suite: .init(testSuite), issue: .init(issue), failureKind: .unexpected) + write(record: TestEventRecord(suiteFailure: record)) + } + + public func testSuite(_ testSuite: XCTestSuite, didRecord expectedFailure: XCTExpectedFailure) { + let record = TestSuiteFailureRecord(suite: .init(testSuite), issue: .init(expectedFailure.issue), failureKind: .expected(failureReason: expectedFailure.failureReason)) + write(record: TestEventRecord(suiteFailure: record)) + } + #else + public func testSuite(_ testSuite: XCTestSuite, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) { + let issue = TestIssue(description: description, inFile: filePath, atLine: lineNumber) + let record = TestSuiteFailureRecord(suite: .init(testSuite), issue: issue, failureKind: .unexpected) + write(record: TestEventRecord(suiteFailure: record)) + } + #endif + + public func testSuiteDidFinish(_ testSuite: XCTestSuite) { + let record = TestSuiteEventRecord(suite: .init(testSuite), event: .finish) + write(record: TestEventRecord(suiteEvent: record)) + } + + public func testBundleDidFinish(_ testBundle: Bundle) { + let record = TestBundleEventRecord(bundle: .init(testBundle), event: .finish) + write(record: TestEventRecord(bundleEvent: record)) + } + } + + // FIXME: Copied from `Lock.swift` in TSCBasic, would be nice if we had a better way + + #if canImport(Glibc) + @_exported import Glibc + #elseif canImport(Musl) + @_exported import Musl + #elseif os(Windows) + @_exported import CRT + @_exported import WinSDK + #elseif os(WASI) + @_exported import WASILibc + #elseif canImport(Android) + @_exported import Android + #else + @_exported import Darwin.C + #endif + + import Foundation + + public final class FileLock { + #if os(Windows) + private var handle: HANDLE? + #else + private var fileDescriptor: CInt? + #endif + + private let lockFile: URL + + public init(at lockFile: URL) { + self.lockFile = lockFile + } + + public func lock() throws { + #if os(Windows) + if handle == nil { + let h: HANDLE = lockFile.path.withCString(encodedAs: UTF16.self, { + CreateFileW( + $0, + UInt32(GENERIC_READ) | UInt32(GENERIC_WRITE), + UInt32(FILE_SHARE_READ) | UInt32(FILE_SHARE_WRITE), + nil, + DWORD(OPEN_ALWAYS), + DWORD(FILE_ATTRIBUTE_NORMAL), + nil + ) + }) + if h == INVALID_HANDLE_VALUE { + throw FileSystemError(errno: Int32(GetLastError()), lockFile) + } + self.handle = h + } + var overlapped = OVERLAPPED() + overlapped.Offset = 0 + overlapped.OffsetHigh = 0 + overlapped.hEvent = nil + if !LockFileEx(handle, DWORD(LOCKFILE_EXCLUSIVE_LOCK), 0, + UInt32.max, UInt32.max, &overlapped) { + throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError())) + } + #elseif os(WASI) + // WASI doesn't support flock + #else + if fileDescriptor == nil { + let fd = open(lockFile.path, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666) + if fd == -1 { + fatalError("errno: \\(errno), lockFile: \\(lockFile)") + } + self.fileDescriptor = fd + } + while true { + if flock(fileDescriptor!, LOCK_EX) == 0 { + break + } + if errno == EINTR { continue } + fatalError("unable to acquire lock, errno: \\(errno)") + } + #endif + } + + public func unlock() { + #if os(Windows) + var overlapped = OVERLAPPED() + overlapped.Offset = 0 + overlapped.OffsetHigh = 0 + overlapped.hEvent = nil + UnlockFileEx(handle, 0, UInt32.max, UInt32.max, &overlapped) + #elseif os(WASI) + // WASI doesn't support flock + #else + guard let fd = fileDescriptor else { return } + flock(fd, LOCK_UN) + #endif + } + + deinit { + #if os(Windows) + guard let handle = handle else { return } + CloseHandle(handle) + #elseif os(WASI) + // WASI doesn't support flock + #else + guard let fd = fileDescriptor else { return } + close(fd) + #endif + } + + public func withLock(_ body: () throws -> T) throws -> T { + try lock() + defer { unlock() } + return try body() + } + + public func withLock(_ body: () async throws -> T) async throws -> T { + try lock() + defer { unlock() } + return try await body() + } + } + + // FIXME: Copied from `XCTEvents.swift`, would be nice if we had a better way + + struct TestEventRecord: Codable { + let caseFailure: TestCaseFailureRecord? + let suiteFailure: TestSuiteFailureRecord? + + let bundleEvent: TestBundleEventRecord? + let suiteEvent: TestSuiteEventRecord? + let caseEvent: TestCaseEventRecord? + + init( + caseFailure: TestCaseFailureRecord? = nil, + suiteFailure: TestSuiteFailureRecord? = nil, + bundleEvent: TestBundleEventRecord? = nil, + suiteEvent: TestSuiteEventRecord? = nil, + caseEvent: TestCaseEventRecord? = nil + ) { + self.caseFailure = caseFailure + self.suiteFailure = suiteFailure + self.bundleEvent = bundleEvent + self.suiteEvent = suiteEvent + self.caseEvent = caseEvent + } + } + + // MARK: - Records + + struct TestAttachment: Codable { + let name: String? + // TODO: Handle `userInfo: [AnyHashable : Any]?` + let uniformTypeIdentifier: String + let payload: Data? + } + + struct TestBundleEventRecord: Codable { + let bundle: TestBundle + let event: TestEvent + } + + struct TestCaseEventRecord: Codable { + let testCase: TestCase + let event: TestEvent + } + + struct TestCaseFailureRecord: Codable, CustomStringConvertible { + let testCase: TestCase + let issue: TestIssue + let failureKind: TestFailureKind + + var description: String { + return "\\(issue.sourceCodeContext.description)\\(testCase) \\(issue.compactDescription)" + } + } + + struct TestSuiteEventRecord: Codable { + let suite: TestSuiteRecord + let event: TestEvent + } + + struct TestSuiteFailureRecord: Codable { + let suite: TestSuiteRecord + let issue: TestIssue + let failureKind: TestFailureKind + } + + // MARK: Primitives + + struct TestBundle: Codable { + let bundleIdentifier: String? + let bundlePath: String + } + + struct TestCase: Codable { + let name: String + } + + struct TestErrorInfo: Codable { + let description: String + let type: String + } + + enum TestEvent: Codable { + case start + case finish + } + + enum TestFailureKind: Codable, Equatable { + case unexpected + case expected(failureReason: String?) + + var isExpected: Bool { + switch self { + case .expected: return true + case .unexpected: return false + } + } + } + + struct TestIssue: Codable { + let type: TestIssueType + let compactDescription: String + let detailedDescription: String? + let associatedError: TestErrorInfo? + let sourceCodeContext: TestSourceCodeContext + let attachments: [TestAttachment] + } + + enum TestIssueType: Codable { + case assertionFailure + case performanceRegression + case system + case thrownError + case uncaughtException + case unmatchedExpectedFailure + case unknown + } + + struct TestLocation: Codable, CustomStringConvertible { + let file: String + let line: Int + + var description: String { + return "\\(file):\\(line) " + } + } + + struct TestSourceCodeContext: Codable, CustomStringConvertible { + let callStack: [TestSourceCodeFrame] + let location: TestLocation? + + var description: String { + return location?.description ?? "" + } + } + + struct TestSourceCodeFrame: Codable { + let address: UInt64 + let symbolInfo: TestSourceCodeSymbolInfo? + let symbolicationError: TestErrorInfo? + } + + struct TestSourceCodeSymbolInfo: Codable { + let imageName: String + let symbolName: String + let location: TestLocation? + } + + struct TestSuiteRecord: Codable { + let name: String + } + + // MARK: XCTest compatibility + + extension TestIssue { + init(description: String, inFile filePath: String?, atLine lineNumber: Int) { + let location: TestLocation? + if let filePath = filePath { + location = .init(file: filePath, line: lineNumber) + } else { + location = nil + } + self.init(type: .assertionFailure, compactDescription: description, detailedDescription: description, associatedError: nil, sourceCodeContext: .init(callStack: [], location: location), attachments: []) + } + } + + import XCTest + + #if canImport(Darwin) // XCTAttachment is unavailable in swift-corelibs-xctest. + extension TestAttachment { + init(_ attachment: XCTAttachment) { + self.init( + name: attachment.name, + uniformTypeIdentifier: attachment.uniformTypeIdentifier, + payload: attachment.value(forKey: "payload") as? Data + ) + } + } + #endif + + extension TestBundle { + init(_ testBundle: Bundle) { + self.init( + bundleIdentifier: testBundle.bundleIdentifier, + bundlePath: testBundle.bundlePath + ) + } + } + + extension TestCase { + init(_ testCase: XCTestCase) { + self.init(name: testCase.name) + } + } + + extension TestErrorInfo { + init(_ error: any Swift.Error) { + self.init(description: "\\(error)", type: "\\(Swift.type(of: error))") + } + } + + #if canImport(Darwin) // XCTIssue is unavailable in swift-corelibs-xctest. + extension TestIssue { + init(_ issue: XCTIssue) { + self.init( + type: .init(issue.type), + compactDescription: issue.compactDescription, + detailedDescription: issue.detailedDescription, + associatedError: issue.associatedError.map { .init($0) }, + sourceCodeContext: .init(issue.sourceCodeContext), + attachments: issue.attachments.map { .init($0) } + ) + } + } + + extension TestIssueType { + init(_ type: XCTIssue.IssueType) { + switch type { + case .assertionFailure: self = .assertionFailure + case .thrownError: self = .thrownError + case .uncaughtException: self = .uncaughtException + case .performanceRegression: self = .performanceRegression + case .system: self = .system + case .unmatchedExpectedFailure: self = .unmatchedExpectedFailure + @unknown default: self = .unknown + } + } + } + #endif + + #if canImport(Darwin) // XCTSourceCodeLocation/XCTSourceCodeContext/XCTSourceCodeFrame/XCTSourceCodeSymbolInfo is unavailable in swift-corelibs-xctest. + extension TestLocation { + init(_ location: XCTSourceCodeLocation) { + self.init( + file: location.fileURL.absoluteString, + line: location.lineNumber + ) + } + } + + extension TestSourceCodeContext { + init(_ context: XCTSourceCodeContext) { + self.init( + callStack: context.callStack.map { .init($0) }, + location: context.location.map { .init($0) } + ) + } + } + + extension TestSourceCodeFrame { + init(_ frame: XCTSourceCodeFrame) { + self.init( + address: frame.address, + symbolInfo: (try? frame.symbolInfo()).map { .init($0) }, + symbolicationError: frame.symbolicationError.map { .init($0) } + ) + } + } + + extension TestSourceCodeSymbolInfo { + init(_ symbolInfo: XCTSourceCodeSymbolInfo) { + self.init( + imageName: symbolInfo.imageName, + symbolName: symbolInfo.symbolName, + location: symbolInfo.location.map { .init($0) } + ) + } + } + #endif + + extension TestSuiteRecord { + init(_ testSuite: XCTestSuite) { + self.init(name: testSuite.name) + } + } + #endif + """ } diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift index 007611ad..5ee78857 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift @@ -17,7 +17,59 @@ import SWBCore final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable { static let identifier = "org.swift.test-entry-point-generator" + override func commandLineFromTemplate(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, optionContext: (any DiscoveredCommandLineToolSpecInfo)?, specialArgs: [String] = [], lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] { + var args = 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)]) + } + for input in cbc.inputs { + if input.fileType.conformsTo(identifier: "text") { + args.append(contentsOf: ["--linker-filelist", .path(input.absolutePath)]) + } else if input.fileType.conformsTo(identifier: "compiled.mach-o") { + // Do nothing + } else { + delegate.error("Unexpected input of type '\(input.fileType)' to test entry point generation") + } + } + } + return args + } + override func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? { TestEntryPointGenerationTaskAction() } + + public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, indexStorePaths: [Path], indexUnitBasePaths: [Path]) async { + var commandLine = 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)]) + } + + delegate.createTask( + type: self, + dependencyData: nil, + payload: nil, + ruleInfo: defaultRuleInfo(cbc, delegate), + additionalSignatureData: "", + commandLine: commandLine, + additionalOutput: [], + environment: environmentFromSpec(cbc, delegate), + workingDirectory: cbc.producer.defaultWorkingDirectory, + inputs: cbc.inputs.map { delegate.createNode($0.absolutePath) }, + outputs: cbc.outputs.map { delegate.createNode($0) }, + mustPrecede: [], + action: createTaskAction(cbc, delegate), + execDescription: resolveExecutionDescription(cbc, delegate), + preparesForIndexing: true, + enableSandboxing: enableSandboxing, + llbuildControlDisabled: true, + additionalTaskOrderingOptions: [] + ) + } } diff --git a/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift b/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift index fe4b56ef..23ec56f0 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointTaskProducer.swift @@ -13,6 +13,7 @@ import SWBCore import SWBTaskConstruction import SWBMacro +import SWBUtil class TestEntryPointTaskProducer: PhasedTaskProducer, TaskProducer { func generateTasks() async -> [any PlannedTask] { @@ -21,8 +22,54 @@ class TestEntryPointTaskProducer: PhasedTaskProducer, TaskProducer { 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) + + guard let configuredTarget = context.configuredTarget else { + context.error("Cannot generate a test entry point without a target") + return + } + var indexStoreDirectories: OrderedSet = [] + var linkerFileLists: OrderedSet = [] + var indexUnitBasePaths: OrderedSet = [] + var binaryPaths: OrderedSet = [] + for directDependency in context.globalProductPlan.dependencies(of: configuredTarget) { + let settings = context.globalProductPlan.planRequest.buildRequestContext.getCachedSettings(directDependency.parameters, target: directDependency.target) + guard settings.productType?.conformsTo(identifier: "com.apple.product-type.bundle.unit-test") == true else { + continue + } + guard settings.globalScope.evaluate(BuiltinMacros.SWIFT_INDEX_STORE_ENABLE) else { + context.error("Cannot perform test discovery for '\(directDependency.target.name)' because index while building is disabled") + continue + } + let path = settings.globalScope.evaluate(BuiltinMacros.SWIFT_INDEX_STORE_PATH) + guard !path.isEmpty else { + continue + } + indexStoreDirectories.append(path) + + for arch in settings.globalScope.evaluate(BuiltinMacros.ARCHS) { + for variant in settings.globalScope.evaluate(BuiltinMacros.BUILD_VARIANTS) { + let innerScope = settings.globalScope + .subscope(binding: BuiltinMacros.archCondition, to: arch) + .subscope(binding: BuiltinMacros.variantCondition, to: variant) + let linkerFileListPath = innerScope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__) + if !linkerFileListPath.isEmpty { + linkerFileLists.append(linkerFileListPath) + } + let objroot = innerScope.evaluate(BuiltinMacros.OBJROOT) + if !objroot.isEmpty { + indexUnitBasePaths.append(objroot) + } + + let binaryPath = innerScope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(innerScope.evaluate(BuiltinMacros.EXECUTABLE_PATH)).normalize() + binaryPaths.append(binaryPath) + } + } + } + + let inputs: [FileToBuild] = linkerFileLists.map { FileToBuild(absolutePath: $0, fileType: self.context.workspaceContext.core.specRegistry.getSpec("text") as! FileTypeSpec) } + binaryPaths.map { FileToBuild(absolutePath: $0, fileType: self.context.workspaceContext.core.specRegistry.getSpec("compiled.mach-o") as! FileTypeSpec) } + + let cbc = CommandBuildContext(producer: context, scope: scope, inputs: inputs, outputs: [outputPath]) + await context.testEntryPointGenerationToolSpec.constructTasks(cbc, delegate, indexStorePaths: indexStoreDirectories.elements, indexUnitBasePaths: indexUnitBasePaths.elements) } } return tasks diff --git a/Sources/SWBUtil/CMakeLists.txt b/Sources/SWBUtil/CMakeLists.txt index 91348fd9..9d2d611d 100644 --- a/Sources/SWBUtil/CMakeLists.txt +++ b/Sources/SWBUtil/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(SWBUtil HashContext.swift Headermap.swift HeavyCache.swift + IndexStore.swift Int.swift InterningArena.swift IO.swift diff --git a/Sources/SWBUtil/Cache.swift b/Sources/SWBUtil/Cache.swift index ad5b932d..eaa9e4e0 100644 --- a/Sources/SWBUtil/Cache.swift +++ b/Sources/SWBUtil/Cache.swift @@ -87,6 +87,13 @@ public final class Cache: NSObject, KeyValueStorage, NSCac return nil } set { + #if os(Linux) + if let newValue = newValue { + cache.value.setObject(ValueWrapper(newValue), forKey: KeyWrapper(key)) + } else { + cache.value.removeObject(forKey: KeyWrapper(key)) + } + #else if let newValue, let cacheableValue = newValue as? (any CacheableValue) { cache.value.setObject(ValueWrapper(newValue), forKey: KeyWrapper(key), cost: cacheableValue.cost) } else if let newValue = newValue { @@ -94,6 +101,7 @@ public final class Cache: NSObject, KeyValueStorage, NSCac } else { cache.value.removeObject(forKey: KeyWrapper(key)) } + #endif } } @@ -112,12 +120,16 @@ public final class Cache: NSObject, KeyValueStorage, NSCac } let value = try body() + #if os(Linux) + cache.value.setObject(ValueWrapper(value), forKey: wrappedKey) + #else if let cacheableValue = value as? (any CacheableValue) { cache.value.setObject(ValueWrapper(value), forKey: wrappedKey, cost: cacheableValue.cost) } else { cache.value.setObject(ValueWrapper(value), forKey: wrappedKey) } + #endif return value } diff --git a/Sources/SWBUtil/IndexStore.swift b/Sources/SWBUtil/IndexStore.swift new file mode 100644 index 00000000..8b4515fc --- /dev/null +++ b/Sources/SWBUtil/IndexStore.swift @@ -0,0 +1,389 @@ +//===----------------------------------------------------------------------===// +// +// 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 SWBCSupport +import Foundation + +public final class IndexStore { + + public struct TestCaseClass { + public struct TestMethod: Hashable, Comparable { + public let name: String + public let isAsync: Bool + + public static func < (lhs: IndexStore.TestCaseClass.TestMethod, rhs: IndexStore.TestCaseClass.TestMethod) -> Bool { + return (lhs.name, (lhs.isAsync ? 1 : 0)) < (rhs.name, (rhs.isAsync ? 1 : 0)) + } + } + + public var name: String + public var module: String + public var testMethods: [TestMethod] + @available(*, deprecated, message: "use testMethods instead") public var methods: [String] + } + + fileprivate var impl: IndexStoreImpl { _impl as! IndexStoreImpl } + private let _impl: Any + + fileprivate init(_ impl: IndexStoreImpl) { + self._impl = impl + } + + static public func open(store path: Path, api: IndexStoreAPI) throws -> IndexStore { + let impl = try IndexStoreImpl.open(store: path, api: api.impl) + return IndexStore(impl) + } + + public func listTests(in objectFiles: [Path]) throws -> [TestCaseClass] { + return try impl.listTests(in: objectFiles) + } + + @available(*, deprecated, message: "use listTests(in:) instead") + public func listTests(inObjectFile object: Path) throws -> [TestCaseClass] { + return try impl.listTests(inObjectFile: object) + } +} + +public final class IndexStoreAPI { + fileprivate var impl: IndexStoreAPIImpl { + _impl as! IndexStoreAPIImpl + } + private let _impl: Any + + public init(dylib path: Path) throws { + self._impl = try IndexStoreAPIImpl(dylib: path) + } +} + +private final class IndexStoreImpl { + typealias TestCaseClass = IndexStore.TestCaseClass + + let api: IndexStoreAPIImpl + + let store: indexstore_t + + private init(store: indexstore_t, api: IndexStoreAPIImpl) { + self.store = store + self.api = api + } + + static public func open(store path: Path, api: IndexStoreAPIImpl) throws -> IndexStoreImpl { + if let store = try api.call({ api.fn.store_create(path.str, &$0) }) { + return IndexStoreImpl(store: store, api: api) + } + throw StubError.error("Unable to open store at \(path.str)") + } + + public func listTests(in objectFiles: [Path]) throws -> [TestCaseClass] { + var inheritance = [String: [String: String]]() + var testMethods = [String: [String: [(name: String, async: Bool)]]]() + + for objectFile in objectFiles { + // Get the records of this object file. + guard let unitReader = try? self.api.call ({ self.api.fn.unit_reader_create(store, unitName(object: objectFile), &$0) }) else { + continue + } + let records = try getRecords(unitReader: unitReader) + let moduleName = self.api.fn.unit_reader_get_module_name(unitReader).str + for record in records { + // get tests info + let testsInfo = try self.getTestsInfo(record: record) + // merge results across module + for (className, parentClassName) in testsInfo.inheritance { + inheritance[moduleName, default: [:]][className] = parentClassName + } + for (className, classTestMethods) in testsInfo.testMethods { + testMethods[moduleName, default: [:]][className, default: []].append(contentsOf: classTestMethods) + } + } + } + + // merge across inheritance in module boundries + func flatten(moduleName: String, className: String) -> [String: (name: String, async: Bool)] { + var allMethods = [String: (name: String, async: Bool)]() + + if let parentClassName = inheritance[moduleName]?[className] { + let parentMethods = flatten(moduleName: moduleName, className: parentClassName) + allMethods.merge(parentMethods, uniquingKeysWith: { (lhs, _) in lhs }) + } + + for method in testMethods[moduleName]?[className] ?? [] { + allMethods[method.name] = (name: method.name, async: method.async) + } + + return allMethods + } + + var testCaseClasses = [TestCaseClass]() + for (moduleName, classMethods) in testMethods { + for className in classMethods.keys { + let methods = flatten(moduleName: moduleName, className: className) + .map { (name, info) in TestCaseClass.TestMethod(name: name, isAsync: info.async) } + .sorted() + testCaseClasses.append(TestCaseClass(name: className, module: moduleName, testMethods: methods, methods: methods.map(\.name))) + } + } + + return testCaseClasses + } + + + @available(*, deprecated, message: "use listTests(in:) instead") + public func listTests(inObjectFile object: Path) throws -> [TestCaseClass] { + // Get the records of this object file. + let unitReader = try api.call{ self.api.fn.unit_reader_create(store, unitName(object: object), &$0) } + let records = try getRecords(unitReader: unitReader) + + // Get the test classes. + var inheritance = [String: String]() + var testMethods = [String: [(name: String, async: Bool)]]() + + for record in records { + let testsInfo = try self.getTestsInfo(record: record) + inheritance.merge(testsInfo.inheritance, uniquingKeysWith: { (lhs, _) in lhs }) + testMethods.merge(testsInfo.testMethods, uniquingKeysWith: { (lhs, _) in lhs }) + } + + func flatten(className: String) -> [(method: String, async: Bool)] { + var results = [(String, Bool)]() + if let parentClassName = inheritance[className] { + let parentMethods = flatten(className: parentClassName) + results.append(contentsOf: parentMethods) + } + if let methods = testMethods[className] { + results.append(contentsOf: methods) + } + return results + } + + let moduleName = self.api.fn.unit_reader_get_module_name(unitReader).str + + var testCaseClasses = [TestCaseClass]() + for className in testMethods.keys { + let methods = flatten(className: className) + .map { TestCaseClass.TestMethod(name: $0.method, isAsync: $0.async) } + .sorted() + testCaseClasses.append(TestCaseClass(name: className, module: moduleName, testMethods: methods, methods: methods.map(\.name))) + } + + return testCaseClasses + } + + private func getTestsInfo(record: String) throws -> (inheritance: [String: String], testMethods: [String: [(name: String, async: Bool)]] ) { + let recordReader = try api.call{ self.api.fn.record_reader_create(store, record, &$0) } + + // scan for inheritance + + let inheritanceStoreRef = StoreRef([String: String](), api: self.api) + let inheritancePointer = unsafeBitCast(Unmanaged.passUnretained(inheritanceStoreRef), to: UnsafeMutableRawPointer.self) + + _ = self.api.fn.record_reader_occurrences_apply_f(recordReader, inheritancePointer) { inheritancePointer , occ -> Bool in + let inheritanceStoreRef = Unmanaged>.fromOpaque(inheritancePointer!).takeUnretainedValue() + let fn = inheritanceStoreRef.api.fn + + // Get the symbol. + let sym = fn.occurrence_get_symbol(occ) + let symbolProperties = fn.symbol_get_properties(sym) + // We only care about symbols that are marked unit tests and are instance methods. + if symbolProperties & UInt64(INDEXSTORE_SYMBOL_PROPERTY_UNITTEST.rawValue) == 0 { + return true + } + if fn.symbol_get_kind(sym) != INDEXSTORE_SYMBOL_KIND_CLASS{ + return true + } + + let parentClassName = fn.symbol_get_name(sym).str + + let childClassNameStoreRef = StoreRef("", api: inheritanceStoreRef.api) + let childClassNamePointer = unsafeBitCast(Unmanaged.passUnretained(childClassNameStoreRef), to: UnsafeMutableRawPointer.self) + _ = fn.occurrence_relations_apply_f(occ!, childClassNamePointer) { childClassNamePointer, relation in + guard let relation = relation else { return true } + let childClassNameStoreRef = Unmanaged>.fromOpaque(childClassNamePointer!).takeUnretainedValue() + let fn = childClassNameStoreRef.api.fn + + // Look for the base class. + if fn.symbol_relation_get_roles(relation) != UInt64(INDEXSTORE_SYMBOL_ROLE_REL_BASEOF.rawValue) { + return true + } + + let childClassNameSym = fn.symbol_relation_get_symbol(relation) + childClassNameStoreRef.instance = fn.symbol_get_name(childClassNameSym).str + return true + } + + if !childClassNameStoreRef.instance.isEmpty { + inheritanceStoreRef.instance[childClassNameStoreRef.instance] = parentClassName + } + + return true + } + + // scan for methods + + let testMethodsStoreRef = StoreRef([String: [(name: String, async: Bool)]](), api: api) + let testMethodsPointer = unsafeBitCast(Unmanaged.passUnretained(testMethodsStoreRef), to: UnsafeMutableRawPointer.self) + + _ = self.api.fn.record_reader_occurrences_apply_f(recordReader, testMethodsPointer) { testMethodsPointer , occ -> Bool in + let testMethodsStoreRef = Unmanaged>.fromOpaque(testMethodsPointer!).takeUnretainedValue() + let fn = testMethodsStoreRef.api.fn + + // Get the symbol. + let sym = fn.occurrence_get_symbol(occ) + let symbolProperties = fn.symbol_get_properties(sym) + // We only care about symbols that are marked unit tests and are instance methods. + if symbolProperties & UInt64(INDEXSTORE_SYMBOL_PROPERTY_UNITTEST.rawValue) == 0 { + return true + } + if fn.symbol_get_kind(sym) != INDEXSTORE_SYMBOL_KIND_INSTANCEMETHOD { + return true + } + + let classNameStoreRef = StoreRef("", api: testMethodsStoreRef.api) + let classNamePointer = unsafeBitCast(Unmanaged.passUnretained(classNameStoreRef), to: UnsafeMutableRawPointer.self) + + _ = fn.occurrence_relations_apply_f(occ!, classNamePointer) { classNamePointer, relation in + guard let relation = relation else { return true } + let classNameStoreRef = Unmanaged>.fromOpaque(classNamePointer!).takeUnretainedValue() + let fn = classNameStoreRef.api.fn + + // Look for the class. + if fn.symbol_relation_get_roles(relation) != UInt64(INDEXSTORE_SYMBOL_ROLE_REL_CHILDOF.rawValue) { + return true + } + + let classNameSym = fn.symbol_relation_get_symbol(relation) + classNameStoreRef.instance = fn.symbol_get_name(classNameSym).str + return true + } + + if !classNameStoreRef.instance.isEmpty { + let methodName = fn.symbol_get_name(sym).str + let isAsync = symbolProperties & UInt64(INDEXSTORE_SYMBOL_PROPERTY_SWIFT_ASYNC.rawValue) != 0 + testMethodsStoreRef.instance[classNameStoreRef.instance, default: []].append((name: methodName, async: isAsync)) + } + + return true + } + + return ( + inheritance: inheritanceStoreRef.instance, + testMethods: testMethodsStoreRef.instance + ) + + } + + private func getRecords(unitReader: indexstore_unit_reader_t?) throws -> [String] { + let builder = StoreRef([String](), api: api) + + let ctx = unsafeBitCast(Unmanaged.passUnretained(builder), to: UnsafeMutableRawPointer.self) + _ = self.api.fn.unit_reader_dependencies_apply_f(unitReader, ctx) { ctx , unit -> Bool in + let store = Unmanaged>.fromOpaque(ctx!).takeUnretainedValue() + let fn = store.api.fn + if fn.unit_dependency_get_kind(unit) == INDEXSTORE_UNIT_DEPENDENCY_RECORD { + store.instance.append(fn.unit_dependency_get_name(unit).str) + } + return true + } + + return builder.instance + } + + private func unitName(object: Path) -> String { + let initialSize = 64 + var buf = UnsafeMutablePointer.allocate(capacity: initialSize) + let len = self.api.fn.store_get_unit_name_from_output_path(store, object.str, buf, initialSize) + + if len + 1 > initialSize { + buf.deallocate() + buf = UnsafeMutablePointer.allocate(capacity: len + 1) + _ = self.api.fn.store_get_unit_name_from_output_path(store, object.str, buf, len + 1) + } + + defer { + buf.deallocate() + } + + return String(cString: buf) + } +} + +private class StoreRef { + let api: IndexStoreAPIImpl + var instance: T + init(_ instance: T, api: IndexStoreAPIImpl) { + self.instance = instance + self.api = api + } +} + +private final class IndexStoreAPIImpl { + + /// The path of the index store dylib. + private let path: Path + + /// Handle of the dynamic library. + private let dylib: LibraryHandle + + /// The index store API functions. + fileprivate let fn: swiftbuild_indexstore_functions_t + + fileprivate func call(_ fn: (inout indexstore_error_t?) -> T) throws -> T { + var error: indexstore_error_t? = nil + let ret = fn(&error) + + if let error = error { + if let desc = self.fn.error_get_description(error) { + throw StubError.error(String(cString: desc)) + } + throw StubError.error("Unable to get description for error: \(error)") + } + + return ret + } + + public init(dylib path: Path) throws { + self.path = path + self.dylib = try Library.open(path) + + var api = swiftbuild_indexstore_functions_t() + api.store_create = Library.lookup(dylib, "indexstore_store_create") + api.store_get_unit_name_from_output_path = Library.lookup(dylib, "indexstore_store_get_unit_name_from_output_path") + api.unit_reader_create = Library.lookup(dylib, "indexstore_unit_reader_create") + api.error_get_description = Library.lookup(dylib, "indexstore_error_get_description") + api.unit_reader_dependencies_apply_f = Library.lookup(dylib, "indexstore_unit_reader_dependencies_apply_f") + api.unit_reader_get_module_name = Library.lookup(dylib, "indexstore_unit_reader_get_module_name") + api.unit_dependency_get_kind = Library.lookup(dylib, "indexstore_unit_dependency_get_kind") + api.unit_dependency_get_name = Library.lookup(dylib, "indexstore_unit_dependency_get_name") + api.record_reader_create = Library.lookup(dylib, "indexstore_record_reader_create") + api.symbol_get_name = Library.lookup(dylib, "indexstore_symbol_get_name") + api.symbol_get_properties = Library.lookup(dylib, "indexstore_symbol_get_properties") + api.symbol_get_kind = Library.lookup(dylib, "indexstore_symbol_get_kind") + api.record_reader_occurrences_apply_f = Library.lookup(dylib, "indexstore_record_reader_occurrences_apply_f") + api.occurrence_get_symbol = Library.lookup(dylib, "indexstore_occurrence_get_symbol") + api.occurrence_relations_apply_f = Library.lookup(dylib, "indexstore_occurrence_relations_apply_f") + api.symbol_relation_get_symbol = Library.lookup(dylib, "indexstore_symbol_relation_get_symbol") + api.symbol_relation_get_roles = Library.lookup(dylib, "indexstore_symbol_relation_get_roles") + + self.fn = api + } +} + +extension indexstore_string_ref_t { + fileprivate var str: String { + return String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: data), + length: length, + encoding: .utf8, + freeWhenDone: false + )! + } +} diff --git a/Sources/SWBWindowsPlatform/Specs/Windows.xcspec b/Sources/SWBWindowsPlatform/Specs/Windows.xcspec index df09990f..9c58f4f4 100644 --- a/Sources/SWBWindowsPlatform/Specs/Windows.xcspec +++ b/Sources/SWBWindowsPlatform/Specs/Windows.xcspec @@ -42,6 +42,31 @@ BasedOn = com.apple.product-type.tool; }, + { + Domain = windows; + Type = ProductType; + Identifier = com.apple.product-type.bundle.unit-test; + BasedOn = com.apple.product-type.library.dynamic; + DefaultBuildProperties = { + // Index store data is required to discover XCTest tests + COMPILER_INDEX_STORE_ENABLE = YES; + SWIFT_INDEX_STORE_ENABLE = YES; + // Testability is needed to generate code to invoke discovered XCTest tests + SWIFT_ENABLE_TESTABILITY = YES; + }; + }, + + { + Domain = windows; + Type = ProductType; + Identifier = com.apple.product-type.tool.swiftpm-test-runner; + BasedOn = default:com.apple.product-type.tool.swiftpm-test-runner; + DefaultBuildProperties = { + EXECUTABLE_SUFFIX = ".$(EXECUTABLE_EXTENSION)"; + EXECUTABLE_EXTENSION = "exe"; + }; + }, + { Domain = windows; Type = ProductType; diff --git a/Sources/SwiftBuild/ProjectModel/BuildSettings.swift b/Sources/SwiftBuild/ProjectModel/BuildSettings.swift index a8909c03..ae10d9de 100644 --- a/Sources/SwiftBuild/ProjectModel/BuildSettings.swift +++ b/Sources/SwiftBuild/ProjectModel/BuildSettings.swift @@ -99,6 +99,7 @@ extension ProjectModel { case SUPPORTS_TEXT_BASED_API case SUPPRESS_WARNINGS case SWIFT_ENABLE_BARE_SLASH_REGEX + case SWIFT_INDEX_STORE_ENABLE case SWIFT_INSTALL_MODULE case SWIFT_PACKAGE_NAME case SWIFT_USER_MODULE_VERSION @@ -146,6 +147,7 @@ extension ProjectModel { case SPECIALIZATION_SDK_OPTIONS case SWIFT_VERSION case SWIFT_ACTIVE_COMPILATION_CONDITIONS + case DYLIB_INSTALL_NAME_BASE } public enum Platform: Hashable, CaseIterable, Sendable { diff --git a/Sources/SwiftBuild/ProjectModel/Targets.swift b/Sources/SwiftBuild/ProjectModel/Targets.swift index 0f8a201a..efd4749c 100644 --- a/Sources/SwiftBuild/ProjectModel/Targets.swift +++ b/Sources/SwiftBuild/ProjectModel/Targets.swift @@ -316,6 +316,7 @@ extension ProjectModel { case executable = "com.apple.product-type.tool" case hostBuildTool = "com.apple.product-type.tool.host-build" case unitTest = "com.apple.product-type.bundle.unit-test" + case swiftpmTestRunner = "com.apple.product-type.tool.swiftpm-test-runner" case bundle = "com.apple.product-type.bundle" case packageProduct = "packageProduct" } diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index e6c0caaf..f3a0c446 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -399,7 +399,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { @Test(.requireSDKs(.host), .skipHostOS(.macOS), .skipHostOS(.windows, "cannot find testing library")) func unitTestWithGeneratedEntryPoint() async throws { - try await withTemporaryDirectory { (tmpDir: Path) in + try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in let testProject = try await TestProject( "TestProject", sourceRoot: tmpDir, @@ -417,14 +417,32 @@ fileprivate struct BuildOperationTests: CoreBasedTests { "SDKROOT": "$(HOST_PLATFORM)", "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)", "SWIFT_VERSION": swiftVersion, + "INDEX_DATA_STORE_DIR": "\(tmpDir.join("index").str)", + "LINKER_DRIVER": "swiftc" ]) ], targets: [ TestStandardTarget( - "test", + "UnitTestRunner", + type: .swiftpmTestRunner, + buildConfigurations: [ + TestBuildConfiguration("Debug", + buildSettings: [:]), + ], + buildPhases: [ + TestSourcesBuildPhase(), + TestFrameworksBuildPhase([ + "MyTests.so" + ]) + ], + dependencies: ["MyTests"] + ), + TestStandardTarget( + "MyTests", type: .unitTest, buildConfigurations: [ TestBuildConfiguration("Debug", buildSettings: [ + "DYLIB_INSTALL_NAME_BASE": "$ORIGIN", "LD_RUNPATH_SEARCH_PATHS": "@loader_path/", ]) ], @@ -433,10 +451,10 @@ fileprivate struct BuildOperationTests: CoreBasedTests { TestFrameworksBuildPhase([ TestBuildFile(.target("library")), ]) - ], - dependencies: [ + ], dependencies: [ "library" - ] + ], + productReferenceName: "MyTests.so" ), TestStandardTarget( "library", @@ -444,6 +462,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildConfigurations: [ TestBuildConfiguration("Debug", buildSettings: [ "DYLIB_INSTALL_NAME_BASE": "$ORIGIN", + "LD_RUNPATH_SEARCH_PATHS": "@loader_path/", // FIXME: Find a way to make these default "EXECUTABLE_PREFIX": "lib", @@ -457,7 +476,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ]) 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("library.swift")) { stream in @@ -467,12 +486,19 @@ fileprivate struct BuildOperationTests: CoreBasedTests { try await tester.fs.writeFileContents(projectDir.join("test.swift")) { stream in stream <<< """ import Testing + import XCTest import library @Suite struct MySuite { - @Test func myTest() async throws { + @Test func myTest() { #expect(foo() == 42) } } + + final class MYXCTests: XCTestCase { + func testFoo() { + XCTAssertTrue(true) + } + } """ } @@ -483,13 +509,19 @@ fileprivate struct BuildOperationTests: CoreBasedTests { 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] + environment = ["LD_LIBRARY_PATH": "\(toolchain.path.join("usr/lib/swift/linux").str):\(projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)"))"] } 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")) + do { + let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "UnitTestRunner")).str), arguments: [], environment: environment) + #expect(String(decoding: executionResult.stdout, as: UTF8.self).contains("Executed 1 test, with 0 failures")) + } + do { + let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "UnitTestRunner")).str), arguments: ["--testing-library", "swift-testing"], environment: environment) + #expect(String(decoding: executionResult.stderr, as: UTF8.self).contains("Test run with 1 test in 1 suite passed")) + } } } } diff --git a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift index d40bc363..ce4515df 100644 --- a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift @@ -300,7 +300,7 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { } @Test(.requireSDKs(.linux)) - func unitTestTarget_linux() async throws { + func unitTestRunnerTarget_linux() async throws { let swiftCompilerPath = try await self.swiftCompilerPath let swiftVersion = try await self.swiftVersion let testProject = TestProject( @@ -319,9 +319,26 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { "PRODUCT_NAME": "$(TARGET_NAME)", "SDKROOT": "linux", "SWIFT_VERSION": swiftVersion, + "INDEX_DATA_STORE_DIR": "/index", + "LINKER_DRIVER": "swiftc" ]), ], targets: [ + TestStandardTarget( + "UnitTestRunner", + type: .swiftpmTestRunner, + buildConfigurations: [ + TestBuildConfiguration("Debug", + buildSettings: [:]), + ], + buildPhases: [ + TestSourcesBuildPhase(), + TestFrameworksBuildPhase([ + "UnitTestTarget.so" + ]) + ], + dependencies: ["UnitTestTarget"], + ), TestStandardTarget( "UnitTestTarget", type: .unitTest, @@ -335,7 +352,8 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { "TestTwo.swift", ]), ], - dependencies: [] + dependencies: [], + productReferenceName: "UnitTestTarget.so" ), ]) let core = try await getCore() @@ -346,14 +364,17 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { try await fs.writeFileContents(swiftCompilerPath) { $0 <<< "binary" } await tester.checkBuild(runDestination: .linux, fs: fs) { results in - results.checkTarget("UnitTestTarget") { target 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")]) + 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.checkInputs([ + .pathPattern(.suffix("UnitTestTarget.LinkFileList")), + .pathPattern(.suffix("UnitTestTarget.so")), + .namePattern(.any), + .namePattern(.any) + ]) 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()