Skip to content

Commit 2691517

Browse files
authored
Include a note after Swift Testing tests finish if any XCTests failed (#9646)
This adds a note in the console output after Swift Testing tests finish if one or more XCTests failed, so the user knows to look earlier in the log output for those details. Fixes rdar://168311253 ### Motivation: Swift Package Manager runs XCTests followed by Swift Testing tests, and they are separate subprocess invocations. Each test framework prints its own output to the console and each has a summary at the end, which includes the total number of failures. Since Swift Testing finishes second (assuming both frameworks are enabled), its summary always appears at the bottom and it can potentially be misleading to a user if there were one or more XCTest failures but zero Swift Testing failures because they may incorrectly believe zero tests failed overall. Long-term, we are planning to unify the console output between these testing frameworks so that they present a consolidated summary. However, that remains an ambition that is still being planned and will likely take time to arrive. Another motivator for this is that soon, I anticipate we'll land an enhancement in Swift Testing which will expand its failure summary to span more lines and make it easier for users to locate which Swift Testing tests failed. (For details, see swiftlang/swift-testing#1420.) This will be helpful, but also has the potential to exacerbate the pre-existing confusion situation if (say) a mixture of Swift Testing and XCTests failed. ### Modifications: Modify the `swift test` command such that if Swift Testing is enabled, it will keep track of whether any XCTests failed before running Swift Testing, and if any did, emit a note at the very end (after Swift Testing finishes) indicating that to the user. ### Result: When relevant, the new note described above will be included in the console output.
1 parent 02dc656 commit 2691517

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

Sources/Commands/SwiftTestCommand.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,13 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
279279
@OptionGroup()
280280
var options: TestCommandOptions
281281

282+
/// The text of a note emitted after Swift Testing tests finish running if
283+
/// at least one XCTest has failed, to inform the user.
284+
///
285+
/// - Note: This is exposed as a property so it can be referenced by an
286+
/// accompanying test as well as the implementation.
287+
public static let xctestFailedNote = "Note: One or more XCTests failed, see logging above for details."
288+
282289
private func run(_ swiftCommandState: SwiftCommandState, buildParameters: BuildParameters, testProducts: [BuiltTestProduct]) async throws {
283290
// Remove test output from prior runs and validate priors.
284291
if self.options.enableExperimentalTestOutput && buildParameters.triple.supportsTestSummary {
@@ -359,6 +366,9 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
359366

360367
// Run Swift Testing (parallel or not, it has a single entry point.)
361368
if options.testLibraryOptions.isEnabled(.swiftTesting, swiftCommandState: swiftCommandState) {
369+
// Determine whether any XCTest runs performed above failed, before Swift Testing runs.
370+
let anyXCTestFailed = results.reduce() == .failure
371+
362372
lazy var testEntryPointPath = testProducts.lazy.compactMap(\.testEntryPointPath).first
363373
if options.testLibraryOptions.isExplicitlyEnabled(.swiftTesting, swiftCommandState: swiftCommandState) || testEntryPointPath == nil {
364374
results.append(
@@ -377,6 +387,15 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
377387
debug: "Skipping automatic Swift Testing invocation because a test entry point path is present: \(testEntryPointPath)"
378388
)
379389
}
390+
391+
// After running Swift Testing tests, if we determined that any XCTests failed earlier,
392+
// emit a message informing the user so they aren't misled and know to look elsewhere for
393+
// those details.
394+
if anyXCTestFailed {
395+
// In theory this could, or should, use `observabilityScope.print(_:verbose:)`,
396+
// but that causes tests which check for this output to fail in CI.
397+
print(Self.xctestFailedNote)
398+
}
380399
}
381400

382401
switch results.reduce() {

Tests/CommandsTests/TestCommandTests.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,110 @@ struct TestCommandTests {
734734
}
735735
}
736736

737+
/// An argument to the test function `noteXCTestFailures()`.
738+
struct XCTestFailureNoteTestArgument: CustomStringConvertible {
739+
/// The relative path to a test fixture in this project.
740+
var fixturePath: String
741+
742+
/// The setting representing whether XCTest should be enabled or disabled
743+
/// for the test command, if any. When the value of this property is `nil`,
744+
/// no flag will be passed. If non-`nil`, either `--enable-xctest` or
745+
/// `--disable-xctest` will be passed representing the value. The default
746+
/// value is `nil`, meaning no flag will be passed but the command defaults
747+
/// to having XCTest enabled.
748+
var isXCTestEnabled: Bool? = nil
749+
750+
/// The setting representing whether Swift Testing should be enabled or disabled
751+
/// for the test command, if any. When the value of this property is `nil`,
752+
/// no flag will be passed. If non-`nil`, either `--enable-swift-testing` or
753+
/// `--disable-swift-testing` will be passed representing the value. The default
754+
/// value is `nil`, meaning no flag will be passed but the command defaults
755+
/// to having Swift Testing enabled.
756+
var isSwiftTestingEnabled: Bool? = nil
757+
758+
/// Whether the test command output is expected to include the note.
759+
var expectNote: Bool
760+
761+
var description: String {
762+
var description = "fixture: '\((fixturePath as NSString).lastPathComponent)'"
763+
if let isXCTestEnabled {
764+
description.append(", XCTest enabled: \(isXCTestEnabled)")
765+
}
766+
if let isSwiftTestingEnabled {
767+
description.append(", Swift Testing enabled: \(isSwiftTestingEnabled)")
768+
}
769+
description.append(", expectNote: \(expectNote)")
770+
return description
771+
}
772+
}
773+
774+
/// Test whether a note is emitted to stdout indicating that XCTests failed
775+
/// after Swift Testing tests finish running.
776+
@Test(
777+
.tags(
778+
.Feature.TargetType.Test,
779+
.Feature.CommandLineArguments.TestEnableXCTest,
780+
.Feature.CommandLineArguments.TestDisableXCTest,
781+
.Feature.CommandLineArguments.TestEnableSwiftTesting,
782+
.Feature.CommandLineArguments.TestDisableSwiftTesting,
783+
),
784+
arguments: [
785+
.init(
786+
fixturePath: "Miscellaneous/TestDiscovery/Simple",
787+
expectNote: false,
788+
),
789+
.init(
790+
fixturePath: "Miscellaneous/TestSingleFailureXCTest",
791+
expectNote: true,
792+
),
793+
.init(
794+
fixturePath: "Miscellaneous/TestSingleFailureSwiftTesting",
795+
expectNote: false,
796+
),
797+
.init(
798+
fixturePath: "Miscellaneous/TestSingleFailureXCTest",
799+
isXCTestEnabled: false,
800+
expectNote: false,
801+
),
802+
.init(
803+
fixturePath: "Miscellaneous/TestSingleFailureXCTest",
804+
isSwiftTestingEnabled: false,
805+
expectNote: false,
806+
),
807+
.init(
808+
fixturePath: "Miscellaneous/TestSingleFailureXCTest",
809+
isXCTestEnabled: false,
810+
isSwiftTestingEnabled: false,
811+
expectNote: false,
812+
),
813+
] as [XCTestFailureNoteTestArgument]
814+
)
815+
func noteXCTestFailures(noteArgument arg: XCTestFailureNoteTestArgument) async throws {
816+
try await fixture(name: arg.fixturePath) { fixturePath in
817+
var args: [String] = []
818+
819+
switch arg.isXCTestEnabled {
820+
case .none: break
821+
case .some(true): args.append("--enable-xctest")
822+
case .some(false): args.append("--disable-xctest")
823+
}
824+
825+
switch arg.isSwiftTestingEnabled {
826+
case .none: break
827+
case .some(true): args.append("--enable-swift-testing")
828+
case .some(false): args.append("--disable-swift-testing")
829+
}
830+
831+
let (stdout, stderr) = try await execute(
832+
args,
833+
packagePath: fixturePath,
834+
buildSystem: .native,
835+
throwIfCommandFails: false,
836+
)
837+
#expect(stdout.contains(SwiftTestCommand.xctestFailedNote) == arg.expectNote, "stdout: \(stdout), stderr: \(stderr)")
838+
}
839+
}
840+
737841
@Test(
738842
.tags(
739843
.Feature.TargetType.Executable,

0 commit comments

Comments
 (0)