Skip to content

Commit

Permalink
Add swift-testing support to swift_test.
Browse files Browse the repository at this point in the history
Test discovery and execution is handled through the v0 JSON ABI entry point provided by the swift-testing framework. This requires version 0.11.0 or higher of the package (or Xcode 16 beta 5 or higher).

PiperOrigin-RevId: 665308132
  • Loading branch information
allevato authored and swiple-rules-gardener committed Aug 20, 2024
1 parent 3cb393f commit 26e2624
Show file tree
Hide file tree
Showing 8 changed files with 656 additions and 63 deletions.
10 changes: 9 additions & 1 deletion swift/toolchains/xcode_swift_toolchain.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,19 @@ def _swift_linkopts_cc_info(
),
)

def _test_linking_context(apple_toolchain, target_triple, toolchain_label):
def _test_linking_context(
apple_toolchain,
target_triple,
toolchain_label,
xcode_config):
"""Returns a `CcLinkingContext` containing linker flags for test binaries.
Args:
apple_toolchain: The `apple_common.apple_toolchain()` object.
target_triple: The target triple `struct`.
toolchain_label: The label of the Swift toolchain that will act as the
owner of the linker input propagating the flags.
xcode_config: The Xcode configuration.
Returns:
A `CcLinkingContext` that will provide linker flags to `swift_test`
Expand All @@ -261,6 +266,8 @@ def _test_linking_context(apple_toolchain, target_triple, toolchain_label):
"-Wl,-weak_framework,XCTest",
"-Wl,-weak-lXCTestSwiftSupport",
]
if _is_xcode_at_least_version(xcode_config, "16.0"):
linkopts.append("-Wl,-weak_framework,Testing")

if platform_developer_framework_dir:
linkopts.extend([
Expand Down Expand Up @@ -616,6 +623,7 @@ def _xcode_swift_toolchain_impl(ctx):
apple_toolchain = apple_toolchain,
target_triple = target_triple,
toolchain_label = ctx.label,
xcode_config = xcode_config,
)

# `--define=SWIFT_USE_TOOLCHAIN_ROOT=<path>` is a rapid development feature
Expand Down
33 changes: 27 additions & 6 deletions tools/test_discoverer/TestDiscoverer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ struct TestDiscoverer: ParsableCommand {
}
}

// These shenanigans are necessary because `XCTestSuite.default.run()` doesn't like to be called
// from an `async main()` (it crashes in the runtime's stack allocator if it tries to run an
// async test method), but we have to do async work to run swift-testing tests. See
// https://forums.swift.org/t/74010 for additional context.
var contents = """
import BazelTestObservation
import Foundation
Expand All @@ -112,17 +116,34 @@ struct TestDiscoverer: ParsableCommand {
@main
struct Main {
static func main() {
do {
try loadTestingLibraries()
} catch {
print("Fatal error loading runtime libraries: \\(error)")
exit(1)
}
do {
try XCTestRunner.run(__allDiscoveredXCTests)
try XUnitTestRecorder.shared.writeXML()
guard !XUnitTestRecorder.shared.hasFailure else {
exit(1)
}
} catch {
print("Test runner failed with \\(error)")
print("Fatal error running XCTest tests: \\(error)")
exit(1)
}
Task {
do {
try await SwiftTestingRunner.run()
} catch {
print("Fatal error running swift-testing tests: \\(error)")
exit(1)
}
do {
try XUnitTestRecorder.shared.writeXML()
} catch {
print("Fatal error writing test results to XML: \\(error)")
exit(1)
}
exit(XUnitTestRecorder.shared.hasFailure ? 1 : 0)
}
_asyncMainDrainQueue()
}
}
Expand Down
3 changes: 3 additions & 0 deletions tools/test_observer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ swift_library(
testonly = 1,
srcs = [
"BazelXMLTestObserver.swift",
"JSON.swift",
"LinuxXCTestRunner.swift",
"Locked.swift",
"ObjectiveCXCTestRunner.swift",
"RuntimeLibraries.swift",
"ShardingFilteringTestCollector.swift",
"StringInterpolation+XMLEscaping.swift",
"SwiftTestingRunner.swift",
"XUnitTestRecorder.swift",
],
module_name = "BazelTestObservation",
Expand Down
211 changes: 211 additions & 0 deletions tools/test_observer/JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright 2024 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

/// A lightweight `Codable` JSON type.
public enum JSON: Sendable {
case null
case bool(Bool)
case number(Number)
case string(String)
case array([JSON])
case object([String: JSON])

public static func number(_ value: Int) -> JSON {
.number(Number(value))
}

public static func number(_ value: Double) -> JSON {
.number(Number(value))
}
}

/// A wrapper around `NSNumber` that is `Sendable` and simplifies other interactions.
///
/// The only way to represent 64-bit integers without loss of precision in Foundation's JSON
/// `Codable` implementations is to use `NSNumber` as the encoded type.
public struct Number: @unchecked Sendable {
/// The underlying `NSNumber` that wraps the numeric value.
private let value: NSNumber

/// The `Int` value of the receiver.
public var intValue: Int {
return value.intValue
}

/// The `Double` value of the receiver.
public var doubleValue: Double {
return value.doubleValue
}

/// Creates a new `Number` from the given integer.
public init(_ value: Int) {
self.value = NSNumber(value: value)
}

/// Creates a new `Number` from the given floating-point value.
public init(_ value: Double) {
self.value = NSNumber(value: value)
}
}

extension JSON {
/// Creates a new JSON value by decoding the given UTF-8-encoded JSON string represented as
/// `Data`.
public init(byDecoding data: Data) throws {
self = try JSONDecoder().decode(JSON.self, from: data)
}

/// A `Data` representing the UTF-8-encoded JSON string value of the receiver.
public var encodedData: Data {
get throws {
return try JSONEncoder().encode(self)
}
}
}

extension JSON: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
self = .null
}
}

extension JSON: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self = .bool(value)
}
}

extension JSON: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .number(value)
}
}

extension JSON: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .number(value)
}
}

extension JSON: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
}
}

extension JSON: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: JSON...) {
self = .array(elements)
}
}

extension JSON: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, JSON)...) {
self = .object(.init(uniqueKeysWithValues: elements))
}
}

extension JSON: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard !container.decodeNil() else {
self = .null
return
}

if let bool = try container.decode(ifValueIs: Bool.self) {
self = .bool(bool)
} else if let number = try container.decode(ifValueIs: Double.self) {
// In what appears to be a bug in Foundation, perfectly legitimate floating point values
// (e.g., 348956.52160425) are failing to decode through `NSNumber`. But we have to use
// `NSNumber` to handle 64-bit integers, like the `Int.min` that swift-testing requires for
// the verbosity level to avoid printing anything to stdout when listing tests. To deal with
// both cases, we try to decode as a `Double` first, and if that fails, we try to decode as an
// `NSNumber`.
self = .number(Number(number))
} else if let number = try container.decode(ifValueIs: Number.self) {
self = .number(number)
} else if let string = try container.decode(ifValueIs: String.self) {
self = .string(string)
} else if let array = try container.decode(ifValueIs: [JSON].self) {
self = .array(array)
} else if let object = try container.decode(ifValueIs: [String: JSON].self) {
self = .object(object)
} else {
throw DecodingError.typeMismatch(
JSON.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "unable to decode as a supported JSON type"))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .null:
try container.encodeNil()
case .bool(let bool):
try container.encode(bool)
case .number(let number):
try container.encode(number)
case .string(let string):
try container.encode(string)
case .array(let array):
try container.encode(array)
case .object(let object):
try container.encode(object)
}
}
}

extension Number: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let int = try container.decode(ifValueIs: Int.self) {
self = .init(int)
} else if let double = try container.decode(ifValueIs: Double.self) {
self = .init(double)
} else {
throw DecodingError.typeMismatch(
JSON.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "unable to decode as a supported number type"))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if value.objCType.pointee == UInt8(ascii: "d") {
try container.encode(value.doubleValue)
} else {
try container.encode(value.intValue)
}
}
}

extension SingleValueDecodingContainer {
/// Decodes a value of the given type if the value in the container is of the same type, or
/// returns nil if the value is of a different type.
fileprivate func decode<T: Decodable>(ifValueIs type: T.Type) throws -> T? {
do {
return try self.decode(type)
} catch DecodingError.typeMismatch {
return nil
}
}
}
18 changes: 4 additions & 14 deletions tools/test_observer/LinuxXCTestRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,12 @@
import Foundation
import XCTest

@available(
*, deprecated,
message: """
Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which \
test deprecated functionality) without warnings.
"""
)
public typealias XCTestRunner = LinuxXCTestRunner

@available(
*, deprecated,
message: """
Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which \
test deprecated functionality) without warnings.
"""
)
/// A test runner for tests that use the XCTest framework on Linux.
///
/// This test runner uses test case entries that were constructed by scanning the symbol graph
/// output of the compiler.
@MainActor
public enum LinuxXCTestRunner {
/// A wrapper around a single test from an `XCTestCaseEntry` used by the test collector.
Expand Down
Loading

1 comment on commit 26e2624

@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.