Skip to content

Commit

Permalink
Refactor the test observer to extract xUnit recording from the XCTest…
Browse files Browse the repository at this point in the history
… observer.

This is work done in preparation for supporting the swift-testing framework.

PiperOrigin-RevId: 662617920
  • Loading branch information
allevato authored and swiple-rules-gardener committed Aug 13, 2024
1 parent 29f0ec2 commit 5aa34d4
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 175 deletions.
86 changes: 38 additions & 48 deletions tools/test_discoverer/ObjcTestPrinter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,61 +20,52 @@ import Foundation
/// on an XCTest bundle but instead is an executable that queries the XCTest framework directly for
/// the tests to run.
struct ObjcTestPrinter {
/// Prints the main test runner to a Swift source file.
func printTestRunner(toFileAt url: URL) {
/// Returns the Swift source code for the test runner.
func testRunnerSource() -> String {
var contents = """
import BazelTestObservation
import Foundation
import XCTest
@main
\(availabilityAttribute)
struct Runner {
static func main() {
loadXCTest()
if let xmlObserver = BazelXMLTestObserver.default {
XCTestObservationCenter.shared.addTestObserver(xmlObserver)
}
do {
var testCollector = try ShardingFilteringTestCollector()
let shardedSuite = testCollector.shard(XCTestSuite.default)
shardedSuite.run()
if let testRun = shardedSuite.testRun {
exit(testRun.totalFailureCount == 0 ? EXIT_SUCCESS : EXIT_FAILURE)
}
} catch {
print("ERROR: \\(error); exiting.")
exit(1)
}
@MainActor
struct XCTestRunner {
struct Error: Swift.Error, CustomStringConvertible {
let description: String
}
}
private func loadXCTest() {
// We weakly linked to XCTest.framework and the Swift support dylib because the machine
// that links the test binary might not be the same that runs it, and they might have Xcode
// installed at different paths. Find the path that Bazel says they're installed at on
// *this* machine and load them.
guard let sdkRoot = ProcessInfo.processInfo.environment["SDKROOT"] else {
print("ERROR: Bazel must set the SDKROOT in order to find XCTest")
exit(1)
}
let sdkRootURL = URL(fileURLWithPath: sdkRoot)
let platformDeveloperPath = sdkRootURL // .../Developer/SDKs/MacOSX.sdk
.deletingLastPathComponent() // .../Developer/SDKs
.deletingLastPathComponent() // .../Developer
let xcTestPath = platformDeveloperPath
.appendingPathComponent("Library/Frameworks/XCTest.framework/XCTest")
.path
guard dlopen(xcTestPath, RTLD_NOW) != nil else {
print("ERROR: dlopen(\\"\\(xcTestPath)\\") failed")
exit(1)
static func run() throws {
try loadXCTest()
XCTestObservationCenter.shared.addTestObserver(BazelXMLTestObserver.default)
var testCollector = try ShardingFilteringTestCollector()
let shardedSuite = testCollector.shard(XCTestSuite.default)
shardedSuite.run()
}
let xcTestSwiftSupportPath = platformDeveloperPath
.appendingPathComponent("usr/lib/libXCTestSwiftSupport.dylib")
.path
guard dlopen(xcTestSwiftSupportPath, RTLD_NOW) != nil else {
print("ERROR: dlopen(\\"\\(xcTestSwiftSupportPath)\\") failed")
exit(1)
private static func loadXCTest() throws {
// We weakly linked to XCTest.framework and the Swift support dylib because the machine
// that links the test binary might not be the same that runs it, and they might have Xcode
// installed at different paths. Find the path that Bazel says they're installed at on
// *this* machine and load them.
guard let sdkRoot = ProcessInfo.processInfo.environment["SDKROOT"] else {
throw Error(description: "ERROR: Bazel must set the SDKROOT in order to find XCTest")
}
let sdkRootURL = URL(fileURLWithPath: sdkRoot)
let platformDeveloperPath = sdkRootURL // .../Developer/SDKs/MacOSX.sdk
.deletingLastPathComponent() // .../Developer/SDKs
.deletingLastPathComponent() // .../Developer
let xcTestPath = platformDeveloperPath
.appendingPathComponent("Library/Frameworks/XCTest.framework/XCTest")
.path
guard dlopen(xcTestPath, RTLD_NOW) != nil else {
throw Error(description: "ERROR: dlopen(\\"\\(xcTestPath)\\") failed")
}
let xcTestSwiftSupportPath = platformDeveloperPath
.appendingPathComponent("usr/lib/libXCTestSwiftSupport.dylib")
.path
guard dlopen(xcTestSwiftSupportPath, RTLD_NOW) != nil else {
throw Error(description: "ERROR: dlopen(\\"\\(xcTestSwiftSupportPath)\\") failed")
}
}
}
Expand Down Expand Up @@ -125,7 +116,6 @@ struct ObjcTestPrinter {
}
"""

createTextFile(at: url, contents: contents)
return contents
}
}
37 changes: 19 additions & 18 deletions tools/test_discoverer/SymbolGraphTestPrinter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,31 +119,34 @@ struct SymbolGraphTestPrinter {
createTextFile(at: url, contents: contents)
}

/// Prints the main test runner to a Swift source file.
func printTestRunner(toFileAt url: URL) {
/// Returns the Swift source code for the test runner.
func testRunnerSource() -> String {
guard !discoveredTests.modules.isEmpty else {
// If no tests were discovered, the user likely wrote non-XCTest-style tests that pass or fail
// based on the exit code of the process. Generate an empty source file here, which will be
// harmlessly compiled as an empty module, and the user's `main` from their own sources will
// be used instead.
createTextFile(at: url, contents: "// No tests discovered; this is intentionally empty.\n")
return
return """
@MainActor
struct XCTestRunner {
static func run() {
// No XCTest-based tests discovered; this is intentionally empty.
}
}
"""
}

var contents = """
import BazelTestObservation
import Foundation
import XCTest
@main
\(availabilityAttribute)
struct Runner {
static func main() {
if let xmlObserver = BazelXMLTestObserver.default {
XCTestObservationCenter.shared.addTestObserver(xmlObserver)
}
do {
var testCollector = try ShardingFilteringTestCollector()
@MainActor
struct XCTestRunner {
static func run() throws {
XCTestObservationCenter.shared.addTestObserver(BazelXMLTestObserver.default)
var testCollector = try ShardingFilteringTestCollector()
"""

Expand All @@ -158,11 +161,9 @@ struct SymbolGraphTestPrinter {
// We don't pass the test filter as an argument because we've already filtered the tests in the
// collector; this lets us do better filtering (i.e., regexes) than XCTest itself allows.
contents += """
XCTMain(testCollector.testsToRun)
} catch {
print("ERROR: \\(error); exiting.")
exit(1)
}
// The preferred overload is one that calls `exit`, which we don't want because we have
// post-work to do, so force the one that returns an exit code instead.
let _: CInt = XCTMain(testCollector.testsToRun)
}
}
Expand Down Expand Up @@ -219,6 +220,6 @@ struct SymbolGraphTestPrinter {
"""

createTextFile(at: url, contents: contents)
return contents
}
}
29 changes: 27 additions & 2 deletions tools/test_discoverer/TestDiscoverer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,34 @@ struct TestDiscoverer: ParsableCommand {
}
}

var contents = """
import BazelTestObservation
\(availabilityAttribute)
@main
struct Main {
static func main() async {
do {
try XCTestRunner.run()
try XUnitTestRecorder.shared.writeXML()
guard !XUnitTestRecorder.shared.hasFailure else {
exit(1)
}
} catch {
print("Test runner failed with \\(error)")
exit(1)
}
}
}
"""

let mainFileURL = URL(fileURLWithPath: mainOutput)
if objcTestDiscovery {
// Print the runner source file, which implements the `@main` type that executes the tests.
let testPrinter = ObjcTestPrinter()
testPrinter.printTestRunner(toFileAt: mainFileURL)
contents.append(testPrinter.testRunnerSource())
} else {
// For each module, print the list of test entries that were discovered in a source file that
// extends that module.
Expand All @@ -116,7 +139,9 @@ struct TestDiscoverer: ParsableCommand {
testPrinter.printTestEntries(forModule: output.moduleName, toFileAt: output.outputURL)
}
// Print the runner source file, which implements the `@main` type that executes the tests.
testPrinter.printTestRunner(toFileAt: mainFileURL)
contents.append(testPrinter.testRunnerSource())
}

createTextFile(at: mainFileURL, contents: contents)
}
}
2 changes: 2 additions & 0 deletions tools/test_observer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ swift_library(
srcs = [
"BazelXMLTestObserver.swift",
"BazelXMLTestObserverRegistration.swift",
"Locked.swift",
"StringInterpolation+XMLEscaping.swift",
"XUnitTestRecorder.swift",
],
module_name = "BazelTestObservation",
visibility = ["//visibility:public"],
Expand Down
Loading

1 comment on commit 5aa34d4

@brentleyjones
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.