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..63fade54b --- /dev/null +++ b/Sources/Overlays/_Testing_Foundation/Support/Additions/BundleAdditions.swift @@ -0,0 +1,61 @@ +// +// 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 + /// 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? = { + 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 + } + + // 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. + /// + /// - 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 + _testTarget ?? main +#else + // On other platforms, the main executable contains test content. + main +#endif + } +} +#endif diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 389d4cc92..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: - 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.) /// 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