Created
March 31, 2026 18:28
-
-
Save ZevEisenberg/cf4c80f2e0a18ebc312a56e89039ff23 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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