Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
224 changes: 185 additions & 39 deletions Sources/Testing/ABI/Encoded/ABI.EncodedTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extension ABI {
var displayName: String?

/// The source location of this test.
public var sourceLocation: EncodedSourceLocation<V>
var sourceLocation: EncodedSourceLocation<V>

/// A type implementing the JSON encoding of ``Test/ID`` for the ABI entry
/// point and event stream output.
Expand Down Expand Up @@ -73,8 +73,26 @@ extension ABI {
/// is `nil`.
var isParameterized: Bool?

/// A type describing a parameter to a parameterized test function.
///
/// - Warning: Parameter info is not yet part of the JSON schema.
struct Parameter: Sendable, Codable {
/// The name of the parameter, if known.
var name: String?

/// The fully-qualified name of the parameter's type.
var typeName: String
}

/// Information about the parameters to this test.
///
/// If this instance does not represent a _parameterized test function_, the
/// value of this property is `nil`.
///
/// - Warning: Parameter info is not yet part of the JSON schema.
var _parameters: [Parameter]?

/// An equivalent of ``tags`` that preserved ABIv6.3 support.
/// An equivalent of ``tags`` that preserves ABIv6.3 support.
var _tags: [String]?

/// The tags associated with the test.
Expand All @@ -97,43 +115,6 @@ extension ABI {
/// @Available(Swift, introduced: 6.4)
/// }
var timeLimit: Double?

init(encoding test: borrowing Test) {
if test.isSuite {
kind = .suite
} else {
kind = .function
isParameterized = test.isParameterized
}
name = test.name
displayName = test.displayName
sourceLocation = EncodedSourceLocation(encoding: test.sourceLocation)
id = ID(encoding: test.id)

// Experimental fields
if V.includesExperimentalFields {
if isParameterized == true {
_testCases = test.uncheckedTestCases?.map(EncodedTestCase.init(encoding:))
}
let tags = test.tags
if !tags.isEmpty {
self._tags = tags.map(String.init(describing:))
}
}

if V.versionNumber >= ABI.v6_4.versionNumber {
self.tags = test.tags.sorted().map { tag in
switch tag.kind {
case .staticMember(let value): value
}
}
let bugs = test.associatedBugs
if !bugs.isEmpty {
self.bugs = bugs
}
self.timeLimit = test.timeLimit.map { $0 / .seconds(1) }
}
}
}
}

Expand Down Expand Up @@ -172,3 +153,168 @@ extension ABI {
extension ABI.EncodedTest: Codable {}
extension ABI.EncodedTest.Kind: Codable {}
extension ABI.EncodedTestCase: Codable {}

// MARK: - Conversion to/from library types

extension ABI.EncodedTest {
/// Initialize an instance of this type from the given value.
///
/// - Parameters:
/// - test: The test to initialize this instance from.
public init(encoding test: borrowing Test) {
if test.isSuite {
kind = .suite
} else {
kind = .function
isParameterized = test.isParameterized
}
name = test.name
displayName = test.displayName
sourceLocation = ABI.EncodedSourceLocation(encoding: test.sourceLocation)
id = ID(encoding: test.id)

// Experimental fields
if V.includesExperimentalFields {
if isParameterized == true {
_testCases = test.uncheckedTestCases?.map(ABI.EncodedTestCase.init(encoding:))
_parameters = test.parameters?.map { parameter in
Parameter(
name: parameter.secondName ?? parameter.firstName,
typeName: parameter.typeInfo.fullyQualifiedName
)
}
}
let tags = test.tags
if !tags.isEmpty {
self._tags = tags.map(String.init(describing:))
}
}

if V.versionNumber >= ABI.v6_4.versionNumber {
self.tags = test.tags.sorted().map { tag in
switch tag.kind {
case .staticMember(let value): value
}
}
let bugs = test.associatedBugs
if !bugs.isEmpty {
self.bugs = bugs
}
self.timeLimit = test.timeLimit.map { $0 / .seconds(1) }
}
}
}

@_spi(ForToolsIntegrationOnly)
extension Test {
/// Attempt to reconstruct an instance of ``TypeInfo`` from an encoded test.
///
/// - Parameters:
/// - test: The test that may contain type information.
///
/// - Returns: On success, an instance of ``TypeInfo`` describing the suite
/// type containing or equalling `test`. On failure, `nil`.
private static func _makeTypeInfo<V>(for test: ABI.EncodedTest<V>) -> TypeInfo? {
// Find the module name, which for XCTest compatibility is split from the
// rest of the test ID by a period character instead of a slash character.
let testID = test.id.stringValue
let splitByPeriod = rawIdentifierAwareSplit(testID, separator: ".", maxSplits: 1)
var testIDComponents = rawIdentifierAwareSplit(testID, separator: "/")
guard let moduleName = splitByPeriod.first,
let firstComponent = testIDComponents.first,
moduleName.endIndex < firstComponent.endIndex else {
// The string wasn't structured as expected for a Swift Testing or XCTest
// test ID.
return nil
}

// Replace the first component string, which is currently shaped like
// "ModuleName.TypeName", with ["ModuleName", "TypeName"]
let secondTestIDComponent = testID[moduleName.endIndex ..< firstComponent.endIndex].dropFirst()
testIDComponents[0] = moduleName
testIDComponents.insert(secondTestIDComponent, at: 1)

if test.kind == .function {
if let lastComponent = testIDComponents.last?.utf8,
lastComponent.first != UInt8(ascii: "`"),
lastComponent.contains(UInt8(ascii: ":")) {
// The last component of the test ID (when split by slash characters)
// appears to be a source location. Remove it as it's not part of the
// suite type.
testIDComponents.removeLast()
}

// The last component of the test ID is the name of the test function.
// Remove that too.
testIDComponents.removeLast()
}

// Recombine the module name with the rest of the test ID to produce the
// fully-qualified type name. Join everything by slashes.
return TypeInfo(fullyQualifiedNameComponents: testIDComponents.map(String.init))
}

/// Initialize an instance of this type from the given value.
///
/// - Parameters:
/// - test: The encoded test to initialize this instance from.
///
/// The resulting instance of ``Test`` cannot be run; attempting to do so will
/// throw an error.
public init?<V>(decoding test: ABI.EncodedTest<V>) {
let sourceLocation = SourceLocation(decoding: test.sourceLocation) ?? .unknown
let typeInfo = Self._makeTypeInfo(for: test)

// Construct the (partial) list of traits available in the encoded test.
// Note we do not try to encode _all_ traits because many trait types simply
// cannot be represented as JSON.
var traits = [any Trait]()
if let tags = test.tags ?? test._tags {
let tags = tags.map(Tag.init(userProvidedStringValue:))
traits.append(Tag.List(tags: tags))
}
if let bugs = test.bugs {
traits += bugs
}
if let timeLimit = test.timeLimit {
traits.append(TimeLimitTrait(timeLimit: .seconds(timeLimit)))
}

switch test.kind {
case .suite:
guard let typeInfo else {
return nil
}
self.init(
displayName: test.displayName,
traits: traits,
sourceLocation: sourceLocation,
containingTypeInfo: typeInfo,
isSynthesized: true
)
case .function:
let parameters = test._parameters.map { parameters in
parameters.enumerated().map { i, parameter in
Testing.Test.Parameter(
index: i,
firstName: parameter.name ?? "_",
typeInfo: TypeInfo(fullyQualifiedName: parameter.typeName, mangledName: nil)
)
}
}

self.init(
name: test.name,
displayName: test.displayName,
traits: traits,
sourceBounds: __SourceBounds(lowerBoundOnly: sourceLocation),
containingTypeInfo: typeInfo,
xcTestCompatibleSelector: nil,
testCases: { () -> Test.Case.Generator<CollectionOfOne<Void>> in
throw APIMisuseError(description: "This instance of 'Test' was synthesized at runtime and cannot be run directly.")
},
parameters: parameters ?? []
)
}
}
}
2 changes: 1 addition & 1 deletion Sources/Testing/ABI/EntryPoints/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha

// Post an event for every discovered test. These events are turned into
// JSON objects if JSON output is enabled.
for test in tests {
for test in tests where !test.isHidden {
Event.post(.testDiscovered, for: (test, nil), configuration: configuration)
}
} else {
Expand Down
8 changes: 6 additions & 2 deletions Sources/Testing/Parameterization/Test.Case.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,15 @@ extension Test {
@_spi(ForToolsIntegrationOnly)
public var typeInfo: TypeInfo

init(index: Int, firstName: String, secondName: String? = nil, type: Any.Type) {
init(index: Int, firstName: String, secondName: String? = nil, typeInfo: TypeInfo) {
self.index = index
self.firstName = firstName
self.secondName = secondName
self.typeInfo = TypeInfo(describing: type)
self.typeInfo = typeInfo
}

init(index: Int, firstName: String, secondName: String? = nil, type: Any.Type) {
self.init(index: index, firstName: firstName, secondName: secondName, typeInfo: TypeInfo(describing: type))
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions Sources/Testing/Parameterization/TypeInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ public struct TypeInfo: Sendable {
/// Initialize an instance of this type with the specified names.
///
/// - Parameters:
/// - fullyQualifiedComponents: The fully-qualified name components of the
/// type.
/// - unqualified: The unqualified name of the type.
/// - fullyQualifiedNameComponents: The fully-qualified name components of
/// the type.
/// - unqualifiedName: The unqualified name of the type. If `nil`, the last
/// string in `fullyQualifiedNameComponents` is used instead.
/// - mangled: The mangled name of the type, if available.
init(fullyQualifiedNameComponents: [String], unqualifiedName: String, mangledName: String? = nil) {
init(fullyQualifiedNameComponents: [String], unqualifiedName: String? = nil, mangledName: String? = nil) {
let unqualifiedName = unqualifiedName ?? fullyQualifiedNameComponents.last ?? fullyQualifiedNameComponents.joined(separator: ".")
_kind = .nameOnly(
fullyQualifiedComponents: fullyQualifiedNameComponents,
unqualified: unqualifiedName,
Expand Down
16 changes: 12 additions & 4 deletions Sources/Testing/Test.ID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ extension Test: Identifiable {
///
/// The value of this property should be set to `nil` for instances of
/// ``Test`` that represent test suite types.
public var sourceLocation: SourceLocation?
public var sourceLocation: SourceLocation? {
willSet {
precondition(newValue != .unknown, "Cannot set the source location of an instance of 'Test.ID' to '.unknown'. Set to 'nil' instead.")
}
}

/// Initialize an instance of this type with the specified fully qualified
/// name components.
Expand Down Expand Up @@ -59,7 +63,9 @@ extension Test: Identifiable {
public init(moduleName: String, nameComponents: [String], sourceLocation: SourceLocation?) {
self.moduleName = moduleName
self.nameComponents = nameComponents
self.sourceLocation = sourceLocation
if let sourceLocation, sourceLocation != .unknown {
self.sourceLocation = sourceLocation
}
}

/// Initialize an instance of this type representing the specified test
Expand All @@ -83,7 +89,7 @@ extension Test: Identifiable {
/// - typeInfo: The test suite type info.
///
/// This initializer produces a test ID corresponding to the given type info
/// as if it described a suite (regardless of whether the ttype has the
/// as if it described a suite (regardless of whether the type has the
/// ``Suite(_:_:)`` attribute applied to it.)
@_spi(ForToolsIntegrationOnly)
public init(typeInfo: TypeInfo) {
Expand Down Expand Up @@ -125,7 +131,9 @@ extension Test: Identifiable {

if !isSuite {
result.nameComponents.append(name)
result.sourceLocation = sourceLocation
if sourceLocation != .unknown {
result.sourceLocation = sourceLocation
}
}

return result
Expand Down
Loading