Skip to content

Instantly share code, notes, and snippets.

@ZevEisenberg
Created March 31, 2026 18:28
Show Gist options
  • Select an option

  • Save ZevEisenberg/cf4c80f2e0a18ebc312a56e89039ff23 to your computer and use it in GitHub Desktop.

Select an option

Save ZevEisenberg/cf4c80f2e0a18ebc312a56e89039ff23 to your computer and use it in GitHub Desktop.
import CustomDump
import Testing
// Swift Testing-ified spiritual successor to https://github.com/ZevEisenberg/TestCleaner
/// A container for a list of test cases.
///
/// Use this when Swift Testing’s [parameterized tests](https://developer.apple.com/documentation/testing/parameterizedtesting) are not appropriate, such as when the construction of the test cases requires interacting with our internal dependency injection container, which requires state to be set via a test suite trait.
public struct TestCases<each Value> {
var testCases: [TestCase<repeat each Value>]
public init(
_ testCases: [TestCase<repeat each Value>]
) {
self.testCases = testCases
}
}
/// A single test case. Carries source location information so test failures can pinpoint the relevant test case in the source editor (if not the test navigator).
public struct TestCase<each Value> {
var value: (repeat each Value)
var involvement: TestInvolvement?
var sourceLocation: SourceLocation
var customDumpSourceLocation: CustomDumpSourceLocation
public init(
_ value: repeat each Value,
involvement: TestInvolvement? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) {
self.value = (repeat each value)
self.involvement = involvement
self.sourceLocation = sourceLocation
self.customDumpSourceLocation = CustomDumpSourceLocation(
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
}
/// Whether a test case is excluded or focused in a test run.
public enum TestInvolvement {
/// This test case is excluded, and will not be evaluated when the test is run
case excluded
/// This test case is focused, meaning that only it and other focused test cases will be evaluated. All non-focused test cases will be excluded.
case focused
}
/// Creates a test case that is skipped when running the enclosing test.
public func xTestCase<each Value>(
_ value: repeat (each Value),
involvement: TestInvolvement? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) -> TestCase<repeat each Value> {
TestCase(
repeat each value,
involvement: .excluded,
sourceLocation: sourceLocation,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
/// Creates a test case that is always run when the enclosing test is run. Causes any non-focused test cases to be skipped. If a test contains multiple focused test cases, they will all be run.
public func fTestCase<each Value>(
_ value: repeat (each Value),
involvement: TestInvolvement? = nil,
sourceLocation: SourceLocation = #_sourceLocation,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) -> TestCase<repeat each Value> {
TestCase(
repeat each value,
involvement: .focused,
sourceLocation: sourceLocation,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
extension TestCases: Sequence {
public typealias Element = (repeat each Value, SourceLocation, CustomDumpSourceLocation)
public func makeIterator() -> Iterator {
Iterator(testCases: testCases.filteredForTesting())
}
public struct Iterator: IteratorProtocol {
let testCases: [TestCase<repeat each Value>]
var currentIndex = 0
public mutating func next() -> Element? {
defer {
currentIndex += 1
}
guard testCases.indices.contains(currentIndex) else {
return nil
}
let testCase = testCases[currentIndex]
return (repeat each testCase.value, testCase.sourceLocation, testCase.customDumpSourceLocation)
}
}
}
extension Array {
func filteredForTesting<each Value>() -> [TestCase<repeat each Value>]
where Element == TestCase<repeat each Value>
{
if contains(where: { $0.involvement == .focused }) {
return filter { $0.involvement == .focused }
}
return filter { $0.involvement != .excluded }
}
}
public struct CustomDumpSourceLocation {
let fileID: StaticString
let filePath: StaticString
let line: UInt
let column: UInt
public init(
fileID: StaticString,
filePath: StaticString,
line: UInt,
column: UInt
) {
self.fileID = fileID
self.filePath = filePath
self.line = line
self.column = column
}
}
public func expectNoDifference<T: Equatable>(
_ expression1: @autoclosure () throws -> T,
_ expression2: @autoclosure () throws -> T,
_ message: @autoclosure () -> String? = nil,
sourceLocation: CustomDumpSourceLocation
) {
expectNoDifference(
try expression1(),
try expression2(),
message(),
fileID: sourceLocation.fileID,
filePath: sourceLocation.filePath,
line: sourceLocation.line,
column: sourceLocation.column
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment