From feb72c544f9c53e984c0434d68e43f66c3264386 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Oct 2024 16:38:22 -0400 Subject: [PATCH 1/3] Add a `Bundle.testTarget` property. This PR adds an experimental class property to `Bundle` in the Foundation cross-import overlay. The property's value represents the bundle containing the test target. On Apple platforms, this is an XCTest bundle. Elsewhere, it's just the main bundle. This is an experimental interface only. --- Package.swift | 1 + .../_Testing_Foundation/CMakeLists.txt | 1 + .../Support/Additions/BundleAdditions.swift | 83 +++++++++++++++++++ Sources/Testing/Support/Environment.swift | 8 +- Sources/Testing/Test+Discovery.swift | 6 +- .../_Testing_Foundation/FoundationTests.swift | 14 +++- 6 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift diff --git a/Package.swift b/Package.swift index b109c0447..791ac7283 100644 --- a/Package.swift +++ b/Package.swift @@ -104,6 +104,7 @@ let package = Package( name: "_Testing_Foundation", dependencies: [ "Testing", + "_TestingInternals" ], path: "Sources/Overlays/_Testing_Foundation", swiftSettings: .packageSettings diff --git a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt index 0c942cfa3..adb0ffec5 100644 --- a/Sources/Overlays/_Testing_Foundation/CMakeLists.txt +++ b/Sources/Overlays/_Testing_Foundation/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(_Testing_Foundation Attachments/Attachable+Encodable+NSSecureCoding.swift Attachments/Attachable+Encodable.swift Events/Clock+Date.swift + Support/Additions/BundleAdditions.swift ReexportTesting.swift) target_link_libraries(_Testing_Foundation PUBLIC diff --git a/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift new file mode 100644 index 000000000..3b9a069d1 --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift @@ -0,0 +1,83 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if canImport(Foundation) +@_spi(ForSwiftTestingOnly) private import Testing +public import Foundation + +extension Bundle { +#if SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING && !SWT_NO_FILE_IO + /// A string that appears within all auto-generated types conforming to the + /// `__TestContainer` protocol. + private static let _testContainerTypeNameMagic = "__🟠$test_container__" + + /// Storage for ``testTarget``. + /// + /// On Apple platforms, the bundle containing test content is a loadable + /// XCTest bundle. By the time this property is read, the bundle should have + /// already been loaded. + private static let _testTarget: Bundle? = { + // If the calling environment sets "XCTestBundlePath" (as Xcode does), then + // we can rely on that variable rather than walking loaded images looking + // for test content. + if let envBundlePath = Environment.variable(named: "XCTestBundlePath"), + let bundle = Bundle(path: envBundlePath) { + return bundle + } + + // Find the first image loaded into the current process that contains any + // test content. + var imageAddress: UnsafeRawPointer? + enumerateTypes(withNamesContaining: _testContainerTypeNameMagic) { thisImageAddress, _, stop in + imageAddress = thisImageAddress + stop = true + } + + // Get the path to the image we found. + var info = Dl_info() + guard let imageAddress, 0 != dladdr(imageAddress, &info), let imageName = info.dli_fname else { + return nil + } + + // Construct a lazy sequence of URLs corresponding to the directories that + // contain the loaded image. + let imageURL = URL(fileURLWithFileSystemRepresentation: imageName, isDirectory: false, relativeTo: nil) + let containingDirectoryURLs = sequence(first: imageURL) { url in + try? url.resourceValues(forKeys: [.parentDirectoryURLKey]).parentDirectory + }.dropFirst() + + // Find the directory most likely to contain our test content and return it. + return containingDirectoryURLs.lazy + .filter { $0.pathExtension.caseInsensitiveCompare("xctest") == .orderedSame } + .compactMap(Bundle.init(url:)) + .first { _ in true } + }() +#endif + + /// A bundle representing the currently-running test target. + /// + /// On Apple platforms, this bundle represents the test bundle built by Xcode + /// or Swift Package Manager. On other platforms, it is equal to the main + /// bundle and represents the test executable built by Swift Package Manager. + /// + /// If more than one test bundle has been loaded into the current process, the + /// value of this property represents the first test bundle found by the + /// testing library at runtime. + @_spi(Experimental) + public static var testTarget: Bundle { +#if SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING && !SWT_NO_FILE_IO + _testTarget ?? main +#else + // On other platforms, the main executable contains test content. + main +#endif + } +} +#endif diff --git a/Sources/Testing/Support/Environment.swift b/Sources/Testing/Support/Environment.swift index e10505877..18b6c8d0e 100644 --- a/Sources/Testing/Support/Environment.swift +++ b/Sources/Testing/Support/Environment.swift @@ -15,7 +15,7 @@ private import _TestingInternals /// This type can be used to access the current process' environment variables. /// /// This type is not part of the public interface of the testing library. -enum Environment { +package enum Environment { #if SWT_NO_ENVIRONMENT_VARIABLES /// Storage for the simulated environment. /// @@ -92,7 +92,7 @@ enum Environment { /// Get all environment variables in the current process. /// /// - Returns: A copy of the current process' environment dictionary. - static func get() -> [String: String] { + package static func get() -> [String: String] { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue #elseif SWT_TARGET_OS_APPLE @@ -140,7 +140,7 @@ enum Environment { /// /// - Returns: The value of the specified environment variable, or `nil` if it /// is not set for the current process. - static func variable(named name: String) -> String? { + package static func variable(named name: String) -> String? { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue[name] #elseif SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING @@ -221,7 +221,7 @@ enum Environment { /// - String values beginning with the letters `"t"`, `"T"`, `"y"`, or `"Y"` /// are interpreted as `true`; and /// - All other non-`nil` string values are interpreted as `false`. - static func flag(named name: String) -> Bool? { + package static func flag(named name: String) -> Bool? { variable(named: name).map { if let signedValue = Int64($0) { return signedValue != 0 diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 389d4cc92..000e4664b 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -58,7 +58,8 @@ extension Test { /// - stop: An `inout` boolean variable indicating whether type enumeration /// should stop after the function returns. Set `stop` to `true` to stop /// type enumeration. -typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, _ stop: inout Bool) -> Void +@_spi(ForSwiftTestingOnly) +public typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, _ stop: inout Bool) -> Void /// Enumerate all types known to Swift found in the current process whose names /// contain a given substring. @@ -66,7 +67,8 @@ typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, /// - Parameters: /// - nameSubstring: A string which the names of matching classes all contain. /// - body: A function to invoke, once per matching type. -func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator: TypeEnumerator) { +@_spi(ForSwiftTestingOnly) +public func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator: TypeEnumerator) { withoutActuallyEscaping(typeEnumerator) { typeEnumerator in withUnsafePointer(to: typeEnumerator) { context in swt_enumerateTypes(withNamesContaining: nameSubstring, .init(mutating: context)) { imageAddress, type, stop, context in diff --git a/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift b/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift index 04c0f57dd..352202643 100644 --- a/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift +++ b/Tests/TestingTests/_Testing_Foundation/FoundationTests.swift @@ -8,17 +8,29 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if canImport(Foundation) && !SWT_NO_UTC_CLOCK +#if canImport(Foundation) @testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import _Testing_Foundation @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing import Foundation struct FoundationTests { +#if !SWT_NO_UTC_CLOCK @Test("Casting Test.Clock.Instant to Date") func castTestClockInstantToDate() { let instant = Test.Clock.Instant.now let date = Date(instant) #expect(TimeInterval(instant.timeComponentsSince1970.seconds) == date.timeIntervalSince1970.rounded(.down)) } +#endif + +#if !SWT_NO_DYNAMIC_LINKING && !SWT_NO_FILE_IO + @Test("Test content bundle") + func testTargetBundle() { + let reportedTestTargetBundle = Bundle.testTarget + final class C {} + let actualTestTargetBundle = Bundle(for: C.self) + #expect(actualTestTargetBundle == reportedTestTargetBundle) + } +#endif } #endif From 87b80bc3e60278b4f0b7eb5ca7cae71c212628e7 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Dec 2024 11:26:28 -0500 Subject: [PATCH 2/3] Avoid package keyword, don't expose a bunch of stuff as SPI when one will do --- Package.swift | 1 - .../Support/Additions/BundleAdditions.swift | 46 ++++++------------- Sources/Testing/Support/Environment.swift | 8 ++-- Sources/Testing/Test+Discovery.swift | 37 +++++++++++++-- Sources/_TestingInternals/include/Stubs.h | 11 +++++ 5 files changed, 62 insertions(+), 41 deletions(-) diff --git a/Package.swift b/Package.swift index 791ac7283..b109c0447 100644 --- a/Package.swift +++ b/Package.swift @@ -104,7 +104,6 @@ let package = Package( name: "_Testing_Foundation", dependencies: [ "Testing", - "_TestingInternals" ], path: "Sources/Overlays/_Testing_Foundation", swiftSettings: .packageSettings diff --git a/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift index 3b9a069d1..b6c644c5c 100644 --- a/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift +++ b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift @@ -24,40 +24,20 @@ extension Bundle { /// XCTest bundle. By the time this property is read, the bundle should have /// already been loaded. private static let _testTarget: Bundle? = { - // If the calling environment sets "XCTestBundlePath" (as Xcode does), then - // we can rely on that variable rather than walking loaded images looking - // for test content. - if let envBundlePath = Environment.variable(named: "XCTestBundlePath"), - let bundle = Bundle(path: envBundlePath) { - return bundle - } - - // Find the first image loaded into the current process that contains any - // test content. - var imageAddress: UnsafeRawPointer? - enumerateTypes(withNamesContaining: _testContainerTypeNameMagic) { thisImageAddress, _, stop in - imageAddress = thisImageAddress - stop = true - } + Test.testBundlePath.flatMap { imagePath in + // Construct a lazy sequence of URLs corresponding to the directories that + // contain the loaded image. + let imageURL = URL(fileURLWithFileSystemRepresentation: imagePath, isDirectory: false, relativeTo: nil) + let containingDirectoryURLs = sequence(first: imageURL) { url in + try? url.resourceValues(forKeys: [.parentDirectoryURLKey]).parentDirectory + } - // Get the path to the image we found. - var info = Dl_info() - guard let imageAddress, 0 != dladdr(imageAddress, &info), let imageName = info.dli_fname else { - return nil + // Find the directory most likely to contain our test content and return it. + return containingDirectoryURLs.lazy + .filter { $0.pathExtension.caseInsensitiveCompare("xctest") == .orderedSame } + .compactMap(Bundle.init(url:)) + .first { _ in true } } - - // Construct a lazy sequence of URLs corresponding to the directories that - // contain the loaded image. - let imageURL = URL(fileURLWithFileSystemRepresentation: imageName, isDirectory: false, relativeTo: nil) - let containingDirectoryURLs = sequence(first: imageURL) { url in - try? url.resourceValues(forKeys: [.parentDirectoryURLKey]).parentDirectory - }.dropFirst() - - // Find the directory most likely to contain our test content and return it. - return containingDirectoryURLs.lazy - .filter { $0.pathExtension.caseInsensitiveCompare("xctest") == .orderedSame } - .compactMap(Bundle.init(url:)) - .first { _ in true } }() #endif @@ -70,6 +50,8 @@ extension Bundle { /// If more than one test bundle has been loaded into the current process, the /// value of this property represents the first test bundle found by the /// testing library at runtime. + /// + /// - Note: This property accesses the file system the first time it is used. @_spi(Experimental) public static var testTarget: Bundle { #if SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING && !SWT_NO_FILE_IO diff --git a/Sources/Testing/Support/Environment.swift b/Sources/Testing/Support/Environment.swift index 18b6c8d0e..e10505877 100644 --- a/Sources/Testing/Support/Environment.swift +++ b/Sources/Testing/Support/Environment.swift @@ -15,7 +15,7 @@ private import _TestingInternals /// This type can be used to access the current process' environment variables. /// /// This type is not part of the public interface of the testing library. -package enum Environment { +enum Environment { #if SWT_NO_ENVIRONMENT_VARIABLES /// Storage for the simulated environment. /// @@ -92,7 +92,7 @@ package enum Environment { /// Get all environment variables in the current process. /// /// - Returns: A copy of the current process' environment dictionary. - package static func get() -> [String: String] { + static func get() -> [String: String] { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue #elseif SWT_TARGET_OS_APPLE @@ -140,7 +140,7 @@ package enum Environment { /// /// - Returns: The value of the specified environment variable, or `nil` if it /// is not set for the current process. - package static func variable(named name: String) -> String? { + static func variable(named name: String) -> String? { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue[name] #elseif SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING @@ -221,7 +221,7 @@ package enum Environment { /// - String values beginning with the letters `"t"`, `"T"`, `"y"`, or `"Y"` /// are interpreted as `true`; and /// - All other non-`nil` string values are interpreted as `false`. - package static func flag(named name: String) -> Bool? { + static func flag(named name: String) -> Bool? { variable(named: name).map { if let signedValue = Int64($0) { return signedValue != 0 diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 000e4664b..d37be0912 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -43,6 +43,37 @@ extension Test { } } } + +#if SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING && !SWT_NO_FILE_IO + @_spi(ForSwiftTestingOnly) + public static var testBundlePath: String? { + // If the calling environment sets "XCTestBundlePath" (as Xcode does), then + // we can rely on that variable rather than walking loaded images looking + // for test content. + if let envBundlePath = Environment.variable(named: "XCTestBundlePath") { + var s = stat() + if 0 == stat(envBundlePath, &s) && swt_S_ISDIR(s.st_mode) { + return envBundlePath + } + } + + // Find the first image loaded into the current process that contains any + // test content. + var imageAddress: UnsafeRawPointer? + enumerateTypes(withNamesContaining: _testContainerTypeNameMagic) { thisImageAddress, _, stop in + imageAddress = thisImageAddress + stop = true + } + + // Get the path to the image we found. + var info = Dl_info() + guard let imageAddress, 0 != dladdr(imageAddress, &info), let imageName = info.dli_fname else { + return nil + } + + return String(validatingCString: imageName) + } +#endif } // MARK: - @@ -58,8 +89,7 @@ extension Test { /// - stop: An `inout` boolean variable indicating whether type enumeration /// should stop after the function returns. Set `stop` to `true` to stop /// type enumeration. -@_spi(ForSwiftTestingOnly) -public typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, _ stop: inout Bool) -> Void +typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, _ stop: inout Bool) -> Void /// Enumerate all types known to Swift found in the current process whose names /// contain a given substring. @@ -67,8 +97,7 @@ public typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: An /// - Parameters: /// - nameSubstring: A string which the names of matching classes all contain. /// - body: A function to invoke, once per matching type. -@_spi(ForSwiftTestingOnly) -public func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator: TypeEnumerator) { +func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator: TypeEnumerator) { withoutActuallyEscaping(typeEnumerator) { typeEnumerator in withUnsafePointer(to: typeEnumerator) { context in swt_enumerateTypes(withNamesContaining: nameSubstring, .init(mutating: context)) { imageAddress, type, stop, context in diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 4e114f751..6c4a4d9ed 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -52,6 +52,17 @@ static int swt_errno(void) { } #if !SWT_NO_FILE_IO +#if __has_include() && defined(S_ISDIR) +/// Check if a given `mode_t` value indicates that a file is a directory. +/// +/// This function is exactly equivalent to the `S_ISDIR()` macro. It is +/// necessary because the mode flag macros are not imported into Swift +/// consistently across platforms. +static bool swt_S_ISDIR(mode_t mode) { + return S_ISDIR(mode); +} +#endif + #if __has_include() && defined(S_ISFIFO) /// Check if a given `mode_t` value indicates that a file is a pipe (FIFO.) /// From b6a2f2366a8dc45a3ae393b6d3668cc6957b364b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 10 Dec 2024 11:35:17 -0500 Subject: [PATCH 3/3] Remove duplicate constant declaration --- .../Support/Additions/BundleAdditions.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift index b6c644c5c..63fade54b 100644 --- a/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift +++ b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift @@ -14,10 +14,6 @@ public import Foundation extension Bundle { #if SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING && !SWT_NO_FILE_IO - /// A string that appears within all auto-generated types conforming to the - /// `__TestContainer` protocol. - private static let _testContainerTypeNameMagic = "__🟠$test_container__" - /// Storage for ``testTarget``. /// /// On Apple platforms, the bundle containing test content is a loadable