Skip to content

Remove C++-based section discovery logic. #902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 111 additions & 35 deletions Sources/Testing/Discovery+Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,25 @@ struct SectionBounds: Sendable {
/// The in-memory representation of the section.
nonisolated(unsafe) var buffer: UnsafeRawBufferPointer

/// All test content section bounds found in the current process.
static var allTestContent: some RandomAccessCollection<SectionBounds> {
_testContentSectionBounds()
/// An enumeration describing the different sections discoverable by the
/// testing library.
enum Kind: Equatable, Hashable, CaseIterable {
/// The test content metadata section.
case testContent

/// The type metadata section.
case typeMetadata
}

/// All section bounds of the given kind found in the current process.
///
/// - Parameters:
/// - kind: Which kind of metadata section to return.
///
/// - Returns: A sequence of structures describing the bounds of metadata
/// sections of the given kind found in the current process.
static func all(_ kind: Kind) -> some RandomAccessCollection<SectionBounds> {
_sectionBounds(kind)
}
}

Expand All @@ -30,14 +46,17 @@ struct SectionBounds: Sendable {

/// An array containing all of the test content section bounds known to the
/// testing library.
private let _sectionBounds = Locked<[SectionBounds]>(rawValue: [])
private let _sectionBounds = Locked<[SectionBounds.Kind: [SectionBounds]]>()

/// A call-once function that initializes `_sectionBounds` and starts listening
/// for loaded Mach headers.
private let _startCollectingSectionBounds: Void = {
// Ensure _sectionBounds is initialized before we touch libobjc or dyld.
_sectionBounds.withLock { sectionBounds in
sectionBounds.reserveCapacity(Int(_dyld_image_count()))
let imageCount = Int(_dyld_image_count())
for kind in SectionBounds.Kind.allCases {
sectionBounds[kind, default: []].reserveCapacity(imageCount)
}
}

func addSectionBounds(from mh: UnsafePointer<mach_header>) {
Expand All @@ -55,12 +74,32 @@ private let _startCollectingSectionBounds: Void = {

// If this image contains the Swift section we need, acquire the lock and
// store the section's bounds.
var size = CUnsignedLong(0)
if let start = getsectiondata(mh, "__DATA_CONST", "__swift5_tests", &size), size > 0 {
_sectionBounds.withLock { sectionBounds in
let testContentSectionBounds: SectionBounds? = {
var size = CUnsignedLong(0)
if let start = getsectiondata(mh, "__DATA_CONST", "__swift5_tests", &size), size > 0 {
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
return SectionBounds(imageAddress: mh, buffer: buffer)
}
return nil
}()

let typeMetadataSectionBounds: SectionBounds? = {
var size = CUnsignedLong(0)
if let start = getsectiondata(mh, "__TEXT", "__swift5_types", &size), size > 0 {
let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size))
let sb = SectionBounds(imageAddress: mh, buffer: buffer)
sectionBounds.append(sb)
return SectionBounds(imageAddress: mh, buffer: buffer)
}
return nil
}()

if testContentSectionBounds != nil || typeMetadataSectionBounds != nil {
_sectionBounds.withLock { sectionBounds in
if let testContentSectionBounds {
sectionBounds[.testContent, default: []].append(testContentSectionBounds)
}
if let typeMetadataSectionBounds {
sectionBounds[.typeMetadata, default: []].append(typeMetadataSectionBounds)
}
}
}
}
Expand All @@ -76,51 +115,68 @@ private let _startCollectingSectionBounds: Void = {
#endif
}()

/// The Apple-specific implementation of ``SectionBounds/all``.
/// The Apple-specific implementation of ``SectionBounds/all(_:)``.
///
/// - Parameters:
/// - kind: Which kind of metadata section to return.
///
/// - Returns: An array of structures describing the bounds of all known test
/// content sections in the current process.
private func _testContentSectionBounds() -> [SectionBounds] {
private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
_startCollectingSectionBounds
return _sectionBounds.rawValue
return _sectionBounds.rawValue[kind]!
}

#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
// MARK: - ELF implementation

private import SwiftShims // For MetadataSections

/// The ELF-specific implementation of ``SectionBounds/all``.
/// The ELF-specific implementation of ``SectionBounds/all(_:)``.
///
/// - Parameters:
/// - kind: Which kind of metadata section to return.
///
/// - Returns: An array of structures describing the bounds of all known test
/// content sections in the current process.
private func _testContentSectionBounds() -> [SectionBounds] {
var result = [SectionBounds]()
private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
struct Context {
var kind: SectionBounds.Kind
var result = [SectionBounds]()
}
var context = Context(kind: kind)

withUnsafeMutablePointer(to: &result) { result in
withUnsafeMutablePointer(to: &context) { context in
swift_enumerateAllMetadataSections({ sections, context in
let context = context.assumingMemoryBound(to: Context.self)

let version = sections.load(as: UInt.self)
guard version >= 4 else {
guard context.pointee.kind != .testContent || version >= 4 else {
// This structure is too old to contain the swift5_tests field.
return true
}

let sections = sections.load(as: MetadataSections.self)
let result = context.assumingMemoryBound(to: [SectionBounds].self)

let start = UnsafeRawPointer(bitPattern: sections.swift5_tests.start)
let size = Int(clamping: sections.swift5_tests.length)
let range = switch context.pointee.kind {
case .testContent:
sections.swift5_tests
case .typeMetadata:
sections.swift5_type_metadata
}
let start = UnsafeRawPointer(bitPattern: range.start)
let size = Int(clamping: range.length)
if let start, size > 0 {
let buffer = UnsafeRawBufferPointer(start: start, count: size)
let sb = SectionBounds(imageAddress: sections.baseAddress, buffer: buffer)
result.pointee.append(sb)

context.pointee.result.append(sb)
}

return true
}, result)
}, context)
}

return result
return context.result
}

#elseif os(Windows)
Expand Down Expand Up @@ -183,33 +239,53 @@ private func _findSection(named sectionName: String, in hModule: HMODULE) -> Sec
}
}

/// The Windows-specific implementation of ``SectionBounds/all``.
/// The Windows-specific implementation of ``SectionBounds/all(_:)``.
///
/// - Parameters:
/// - kind: Which kind of metadata section to return.
///
/// - Returns: An array of structures describing the bounds of all known test
/// content sections in the current process.
private func _testContentSectionBounds() -> [SectionBounds] {
HMODULE.all.compactMap { _findSection(named: ".sw5test", in: $0) }
private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
let sectionName = switch kind {
case .testContent:
".sw5test"
case .typeMetadata:
".sw5tymd"
}
return HMODULE.all.compactMap { _findSection(named: sectionName, in: $0) }
}
#else
/// The fallback implementation of ``SectionBounds/all`` for platforms that
/// The fallback implementation of ``SectionBounds/all(_:)`` for platforms that
/// support dynamic linking.
///
/// - Parameters:
/// - kind: Ignored.
///
/// - Returns: The empty array.
private func _testContentSectionBounds() -> [SectionBounds] {
private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] {
#warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)")
return []
}
#endif
#else
// MARK: - Statically-linked implementation

/// The common implementation of ``SectionBounds/all`` for platforms that do not
/// support dynamic linking.
/// The common implementation of ``SectionBounds/all(_:)`` for platforms that do
/// not support dynamic linking.
///
/// - Parameters:
/// - kind: Which kind of metadata section to return.
///
/// - Returns: A structure describing the bounds of the test content section
/// - Returns: A structure describing the bounds of the type metadata section
/// contained in the same image as the testing library itself.
private func _testContentSectionBounds() -> CollectionOfOne<SectionBounds> {
let (sectionBegin, sectionEnd) = SWTTestContentSectionBounds
private func _sectionBounds(_ kind: SectionBounds.Kind) -> CollectionOfOne<SectionBounds> {
let (sectionBegin, sectionEnd) = switch kind {
case .testContent:
SWTTestContentSectionBounds
case .typeMetadata:
SWTTypeMetadataSectionBounds
}
let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin))
let sb = SectionBounds(imageAddress: nil, buffer: buffer)
return CollectionOfOne(sb)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Testing/Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ extension TestContent where Self: ~Copyable {
/// is used with move-only types (specifically ``ExitTest``) and
/// `Sequence.Element` must be copyable.
static func enumerateTestContent(withHint hint: TestContentAccessorHint? = nil, _ body: TestContentEnumerator) {
let testContentRecords = SectionBounds.allTestContent.lazy.flatMap(_testContentRecords(in:))
let testContentRecords = SectionBounds.all(.testContent).lazy.flatMap(_testContentRecords(in:))

var stop = false
for (imageAddress, record) in testContentRecords {
Expand Down
13 changes: 5 additions & 8 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,19 +250,16 @@ extension ExitTest {

if result == nil {
// Call the legacy lookup function that discovers tests embedded in types.
enumerateTypes(withNamesContaining: exitTestContainerTypeNameMagic) { _, type, stop in
guard let type = type as? any __ExitTestContainer.Type else {
return
}
if type.__sourceLocation == sourceLocation {
result = ExitTest(
result = types(withNamesContaining: exitTestContainerTypeNameMagic).lazy
.compactMap { $0 as? any __ExitTestContainer.Type }
.first { $0.__sourceLocation == sourceLocation }
.map { type in
ExitTest(
__expectedExitCondition: type.__expectedExitCondition,
sourceLocation: type.__sourceLocation,
body: type.__body
)
stop = true
}
}
}

return result
Expand Down
40 changes: 14 additions & 26 deletions Sources/Testing/Test+Discovery+Legacy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,35 +49,23 @@ let exitTestContainerTypeNameMagic = "__🟠$exit_test_body__"

// MARK: -

/// The type of callback called by ``enumerateTypes(withNamesContaining:_:)``.
///
/// - Parameters:
/// - imageAddress: A pointer to the start of the image. This value is _not_
/// equal to the value returned from `dlopen()`. On platforms that do not
/// support dynamic loading (and so do not have loadable images), this
/// argument is unspecified.
/// - type: A Swift type.
/// - 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

/// Enumerate all types known to Swift found in the current process whose names
/// Get all types known to Swift found in the current process whose names
/// contain a given substring.
///
/// - 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) {
withoutActuallyEscaping(typeEnumerator) { typeEnumerator in
withUnsafePointer(to: typeEnumerator) { context in
swt_enumerateTypes(withNamesContaining: nameSubstring, .init(mutating: context)) { imageAddress, type, stop, context in
let typeEnumerator = context!.load(as: TypeEnumerator.self)
let type = unsafeBitCast(type, to: Any.Type.self)
var stop2 = false
typeEnumerator(imageAddress, type, &stop2)
stop.pointee = stop2
///
/// - Returns: A sequence of Swift types whose names contain `nameSubstring`.
func types(withNamesContaining nameSubstring: String) -> some Sequence<Any.Type> {
SectionBounds.all(.typeMetadata).lazy
.map { sb in
var count = 0
let start = swt_copyTypes(in: sb.buffer.baseAddress!, sb.buffer.count, withNamesContaining: nameSubstring, count: &count)
defer {
free(start)
}
return start.withMemoryRebound(to: Any.Type.self, capacity: count) { start in
Array(UnsafeBufferPointer(start: start, count: count))
}
}
}
}.joined()
}
11 changes: 4 additions & 7 deletions Sources/Testing/Test+Discovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,11 @@ extension Test: TestContent {
}

if discoveryMode != .newOnly && generators.isEmpty {
enumerateTypes(withNamesContaining: testContainerTypeNameMagic) { imageAddress, type, _ in
guard let type = type as? any __TestContainer.Type else {
return
generators += types(withNamesContaining: testContainerTypeNameMagic).lazy
.compactMap { $0 as? any __TestContainer.Type }
.map { type in
{ @Sendable in await type.__tests }
}
generators.append { @Sendable in
await type.__tests
}
}
}

// *Now* we call all the generators and return their results.
Expand Down
Loading