diff --git a/GeneratedTestingTarget/main.swift b/GeneratedTestingTarget/main.swift new file mode 100644 index 0000000..1f5d524 --- /dev/null +++ b/GeneratedTestingTarget/main.swift @@ -0,0 +1,117 @@ +#if canImport(Darwin) + @preconcurrency import Darwin +#elseif canImport(Glibc) + @preconcurrency import Glibc // disable concurrency safety checks for stdout/stderr flushing +#elseif canImport(CRT) + @preconcurrency import CRT +#endif + +// Must be after the Glibc import, because the first occurency of a (transitive) import determines whether +// something is a @preconcurrency import. +import XCTest + +// Type names not exported by the proprietary version of XCTest . +#if os(macOS) && !canImport(SwiftXCTest) + private typealias XCTestCaseClosure = (XCTestCase) throws -> Void + private typealias XCTestCaseEntry + = (testCaseClass: XCTestCase.Type, allTests: [(String, XCTestCaseClosure)]) +#endif + +/// The type of `T`'s generated static `allTests` property. +private typealias AllTests = [(String, (T) -> () throws -> Void)] + +private extension Array { + + /// Transforms `self` into an `XCTestCaseEntry` by erasing `T` from the type and representing + /// it as a string, or returns an empty result if `T` is-not-a `XCTestCase`. + func eraseTestTypes() -> XCTestCaseEntry where Self == AllTests { + guard let t = T.self as? XCTestCase.Type else { + // Skip any methods scraped from non-XCTestCase types. + return (testCaseClass: XCTestCase.self, allTests: []) + } + + func xcTestCaseClosure(_ f: @escaping (T) -> () throws -> Void) -> XCTestCaseClosure { + { (x: XCTestCase) throws -> Void in try f(x as! T)() } + } + + let allTests = self.map { name_f in (name_f.0, xcTestCaseClosure(name_f.1)) } + return (testCaseClass: t, allTests: allTests) + } + +} + +private extension TestCase { + + static var allTests: AllTests { + return [ + ("test1", test1), + ("testExtensionMethodsCanBeTests", testExtensionMethodsCanBeTests), + ] + } + +} + +private extension StructsAreNotTestCases.NestedStructClassesCanBeTestCases { + + static var allTests: AllTests { + return [ + ("testNestedClassMethodsCanBeTests", testNestedClassMethodsCanBeTests), + ] + } + +} + +private extension StructsAreNotTestCases { + + static var allTests: AllTests { + return [ + ("testStructExtensionMethodsAreNotTests", testStructExtensionMethodsAreNotTests), + ] + } + +} + +private extension EnumsAreNotTestCases.NestedEnumClassesCanBeTestCases { + + static var allTests: AllTests { + return [ + ("testNestedClassMethodsCanBeTests", testNestedClassMethodsCanBeTests), + ] + } + +} + +private extension NotAllClassesAreTestCases { + + static var allTests: AllTests { + return [ + ("testNonXCTestCaseMethodsAreNotTests", testNonXCTestCaseMethodsAreNotTests), + ] + } + +} + +/// Feeds `allTestCases` to `XCTMain` if the latter is available. +func runAllTestCases() { + /// The full complement of test cases and their individual tests + let allTestCases: [XCTestCaseEntry] = [ + TestCase.allTests.eraseTestTypes(), + StructsAreNotTestCases.NestedStructClassesCanBeTestCases.allTests.eraseTestTypes(), + StructsAreNotTestCases.allTests.eraseTestTypes(), + EnumsAreNotTestCases.NestedEnumClassesCanBeTestCases.allTests.eraseTestTypes(), + NotAllClassesAreTestCases.allTests.eraseTestTypes() + ] + #if !os(Windows) + // ignore SIGPIPE which is sent when writing to closed file descriptors. + _ = signal(SIGPIPE, SIG_IGN) + #endif + + atexit({ + fflush(stdout) + fflush(stderr) + }) + #if !os(macOS) || canImport(SwiftXCTest) + XCTMain(allTestCases) + #endif +} +runAllTestCases() \ No newline at end of file diff --git a/Package.swift b/Package.swift index 18362ec..98a2d88 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 import PackageDescription @@ -48,6 +48,13 @@ let package = Package( dependencies: ["DummyTestee", "XCTestImporter"], path: "Tests", exclude: ["XCTestImporter.swift", "CMakeLists.txt", "Dummy.swift"], - sources: ["Tests.swift"]) - ] + sources: ["Tests.swift"]), + // Use this for testing the generated file. + // Generate the main.swift by running this: `swift run -Xswiftc -DBUILDING_WITH_SWIFT_PACKAGE_MANAGER GenerateSwiftXCTestMain -o GeneratedTestingTarget/main.swift /home/ambrus/GenerateSwiftXCTestMain/Tests/Tests.swift` + // Then copy Tests.swift into this directory and try to build the target. `swift run GeneratedTestingTarget` + .executableTarget( + name: "GeneratedTestingTarget", + dependencies: ["Tests"], + path: "GeneratedTestingTarget") + ], ) diff --git a/README.md b/README.md index ea343a6..4391ce7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ the scope of test method bodies could produce unpredictable results. There are two methods: -1. **Swift Package Manager**: `swift test` +1. **Swift Package Manager**: `swift test -Xswiftc -DBUILDING_WITH_SWIFT_PACKAGE_MANAGER` 2. **CMake**: ` ```sh diff --git a/Sources/GenerateSwiftXCTestMain.swift b/Sources/GenerateSwiftXCTestMain.swift index a88c580..b81d572 100644 --- a/Sources/GenerateSwiftXCTestMain.swift +++ b/Sources/GenerateSwiftXCTestMain.swift @@ -45,7 +45,7 @@ struct GenerateSwiftXCTestMain: ParsableCommand { /// /// - See also: `discoveredTests()` for more information. func discoveredTests(in f: URL) throws -> TestCatalog { - let tree = try parseAndEmitDiagnostics(source: String(contentsOf: f)) + let tree = try parseAndEmitDiagnostics(source: String(contentsOf: f, encoding: .utf8)) let scraper = TestScraper() scraper.walk(tree) return scraper.result @@ -73,15 +73,18 @@ struct GenerateSwiftXCTestMain: ParsableCommand { let output = """ - import XCTest #if canImport(Darwin) - import Darwin + @preconcurrency import Darwin #elseif canImport(Glibc) - import Glibc + @preconcurrency import Glibc // disable concurrency safety checks for stdout/stderr flushing #elseif canImport(CRT) - import CRT + @preconcurrency import CRT #endif + // Must be after the Glibc import, because the first occurency of a (transitive) import determines whether + // something is a @preconcurrency import. + import XCTest + // Type names not exported by the proprietary version of XCTest . #if os(macOS) && !canImport(SwiftXCTest) private typealias XCTestCaseClosure = (XCTestCase) throws -> Void @@ -114,13 +117,12 @@ struct GenerateSwiftXCTestMain: ParsableCommand { \(tests.map(allTestsExtension).joined(separator: "\n\n")) - /// The full complement of test cases and their individual tests - private let allTestCases: [XCTestCaseEntry] = [ - \(tests.keys.map {"\($0).allTests.eraseTestTypes()"}.joined(separator: ",\n ")) - ] - /// Feeds `allTestCases` to `XCTMain` if the latter is available. func runAllTestCases() { + /// The full complement of test cases and their individual tests + let allTestCases: [XCTestCaseEntry] = [ + \(tests.keys.map {"\($0).allTests.eraseTestTypes()"}.joined(separator: ",\n ")) + ] #if !os(Windows) // ignore SIGPIPE which is sent when writing to closed file descriptors. _ = signal(SIGPIPE, SIG_IGN)