Skip to content

Commit 042438f

Browse files
committed
Fix: Signal Handling System and Live Failure Reporting
1 parent fd9a811 commit 042438f

File tree

6 files changed

+204
-55
lines changed

6 files changed

+204
-55
lines changed

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ add_library(Testing
8181
Support/GetSymbol.swift
8282
Support/Graph.swift
8383
Support/JSON.swift
84+
Support/LiveUpdatingLine.swift
8485
Support/Locked.swift
8586
Support/Locked+Platform.swift
8687
Support/Versions.swift

Sources/Testing/Events/Recorder/Event.AdvancedConsoleOutputRecorder.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,11 @@ extension Event {
9898
private let _context = Locked(rawValue: HierarchyContext())
9999

100100
/// Initialize the hierarchical console output recorder.
101-
public init(options: Options = Options(), writingUsing write: @escaping @Sendable (String) -> Void) {
102-
self.options = options
103-
self.write = write
104-
}
101+
public init(options: Options = Options(), writingUsing write: @escaping @Sendable (String) -> Void) {
102+
self.options = options
103+
self.write = write
104+
Self.setupSignalHandlers()
105+
}
105106
}
106107
}
107108

@@ -124,6 +125,8 @@ extension Event.AdvancedConsoleOutputRecorder {
124125
}
125126

126127
private static let _livePhaseState = Locked(rawValue: LivePhaseState())
128+
@MainActor private static var _signalHandler: (any DispatchSourceSignal)?
129+
@MainActor private static var _hasActiveRecorder: Bool = false
127130

128131
private func updateLiveStats(event: Event, context: Event.Context) {
129132
Self._livePhaseState.withLock { state in
@@ -497,6 +500,7 @@ extension Event.AdvancedConsoleOutputRecorder {
497500
}
498501

499502
renderCompleteHierarchy()
503+
Self.cleanupSignalHandlers()
500504
}
501505

502506
private func renderCompleteHierarchy() {
@@ -911,6 +915,54 @@ extension Event.AdvancedConsoleOutputRecorder {
911915
return false
912916
}
913917
}
918+
919+
// MARK: - Signal Handling
920+
921+
private static func setupSignalHandlers() {
922+
Task { @MainActor in
923+
// Only set up the signal handler once
924+
guard Self._signalHandler == nil else { return }
925+
926+
Self._hasActiveRecorder = true
927+
928+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
929+
let signalType = SIGINFO
930+
#else
931+
let signalType = SIGUSR1
932+
#endif
933+
934+
signal(signalType, SIG_IGN) // Ignore default handler
935+
936+
let signalSource = DispatchSource.makeSignalSource(signal: signalType, queue: .main)
937+
signalSource.setEventHandler {
938+
Self.printGlobalOnDemandStatus()
939+
}
940+
signalSource.resume()
941+
Self._signalHandler = signalSource
942+
}
943+
}
944+
945+
@MainActor private static func printGlobalOnDemandStatus() {
946+
guard _hasActiveRecorder else {
947+
try? FileHandle.stderr.write("\nNo active test recorder found.\n")
948+
return
949+
}
950+
951+
let (total, completed, passed, failed, warnings, known, skipped) = _livePhaseState.withLock { state in
952+
(max(state.totalTests, state.completedTests), state.completedTests, state.passed,
953+
state.failed, state.warnings, state.knownIssues, state.skipped)
954+
}
955+
956+
let statusMsg = "STATUS: [\(completed)/\(total)] | ✓ \(passed) | ✗ \(failed) | ? \(warnings) | ~ \(known) | → \(skipped)"
957+
958+
try? FileHandle.stderr.write("\n\(statusMsg)\n")
959+
}
960+
961+
private static func cleanupSignalHandlers() {
962+
Task { @MainActor in
963+
Self._hasActiveRecorder = false
964+
}
965+
}
914966
}
915967

916968
// MARK: - Extensions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
/// Prints a string to the console by overwriting the current line without creating a new line.
12+
/// This is ideal for creating single-line, live-updating progress bars or status indicators.
13+
///
14+
/// - Parameter text: The text to display on the current line
15+
///
16+
/// ## Technical Details
17+
/// - Uses carriage return (`\r`) to move cursor to beginning of current line
18+
/// - Uses ANSI escape code (`\u{001B}[2K`) to clear the entire line
19+
/// - Does not append a newline character, allowing the line to be overwritten again
20+
///
21+
/// ## Example Usage
22+
/// ```swift
23+
/// printLiveUpdatingLine("Processing... 25%")
24+
/// // Later...
25+
/// printLiveUpdatingLine("Processing... 50%")
26+
/// // Later...
27+
/// printLiveUpdatingLine("Processing... 100%")
28+
/// ```
29+
public func printLiveUpdatingLine(_ text: String) {
30+
print("\r\u{001B}[2K\(text)", terminator: "")
31+
}

Tests/TestingTests/HierarchicalOutputDemo.swift

Lines changed: 32 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,39 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
@testable @_spi(ForToolsIntegrationOnly) import Testing
11+
import Testing
1212
import Foundation
1313

14-
@Suite("HierarchicalOutputDemoSuite")
15-
struct HierarchicalOutputDemoSuite {
16-
17-
@Test("passingTest")
18-
func passingTest() {
19-
#expect(1 + 1 == 2)
20-
}
21-
22-
@Test("anotherPassingTest")
23-
func anotherPassingTest() {
24-
#expect(true == true)
25-
}
26-
27-
@Test("failingTest")
28-
func failingTest() {
29-
#expect(1 + 1 == 3)
30-
}
31-
32-
static func shouldSkipTest() -> Bool {
33-
// This will always return false (i.e., should skip), but is evaluated at runtime
34-
return ProcessInfo.processInfo.environment["NEVER_SET_ENV_VAR"] != nil
35-
}
36-
37-
@Test("skippedTest", .enabled { shouldSkipTest() })
38-
func skippedTest() {
39-
#expect(1 + 1 == 2)
40-
}
41-
42-
@Test("anotherSkippedTest", .disabled { !shouldSkipTest() })
43-
func anotherSkippedTest() {
44-
#expect(true == true)
45-
}
46-
}
14+
@Suite("Hierarchical Output Demo - Progress Bar Test")
15+
struct HierarchicalOutputDemo {
4716

48-
@Suite("AnotherTestSuite")
49-
struct AnotherTestSuite {
50-
51-
@Test("quickTest")
52-
func quickTest() {
53-
#expect(2 + 2 == 4)
54-
}
55-
56-
@Test("slightlySlowerTest")
57-
func slightlySlowerTest() {
58-
#expect(3 + 3 == 6)
59-
}
60-
61-
@Test("skipInAnotherSuite", .enabled { HierarchicalOutputDemoSuite.shouldSkipTest() })
62-
func skipInAnotherSuite() {
63-
#expect(1 + 1 == 2)
64-
}
17+
@Test("Slow Test 1")
18+
func slowTest1() async throws {
19+
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
20+
#expect(1 + 1 == 2)
21+
}
22+
23+
@Test("Slow Test 2")
24+
func slowTest2() async throws {
25+
try await Task.sleep(nanoseconds: 2_500_000_000) // 2.5 seconds
26+
#expect(2 + 2 == 4)
27+
}
28+
29+
@Test("Slow Test 3")
30+
func slowTest3() async throws {
31+
try await Task.sleep(nanoseconds: 1_800_000_000) // 1.8 seconds
32+
#expect(3 + 3 == 6)
33+
}
34+
35+
@Test("Slow Test 4")
36+
func slowTest4() async throws {
37+
try await Task.sleep(nanoseconds: 2_200_000_000) // 2.2 seconds
38+
#expect(4 + 4 == 8)
39+
}
40+
41+
@Test("Slow Test 5")
42+
func slowTest5() async throws {
43+
try await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
44+
#expect(5 + 5 == 10)
45+
}
6546
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Testing
2+
import Foundation
3+
4+
@Suite("Progress Bar Visibility Demo - Watch the progress bar update!", .serialized)
5+
struct ProgressBarVisibilityDemo {
6+
7+
@Test("Slow Test 1 - 2 seconds")
8+
func slowTest1() async throws {
9+
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
10+
#expect(1 + 1 == 2)
11+
}
12+
13+
@Test("Slow Test 2 - 3 seconds")
14+
func slowTest2() async throws {
15+
try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
16+
#expect(2 + 2 == 4)
17+
}
18+
19+
@Test("Failing Slow Test - 2.5 seconds")
20+
func failingSlowTest() async throws {
21+
try await Task.sleep(nanoseconds: 2_500_000_000) // 2.5 seconds
22+
#expect(1 + 1 == 3, "This test will fail after waiting 2.5 seconds")
23+
}
24+
25+
@Test("Slow Test 3 - 4 seconds")
26+
func slowTest3() async throws {
27+
try await Task.sleep(nanoseconds: 4_000_000_000) // 4 seconds
28+
#expect(3 + 3 == 6)
29+
}
30+
31+
@Test("Another Failing Test - 1.5 seconds")
32+
func anotherFailingTest() async throws {
33+
try await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
34+
#expect(2 + 2 == 5, "Another failure after 1.5 seconds")
35+
}
36+
37+
@Test("Slow Test 4 - 3.5 seconds")
38+
func slowTest4() async throws {
39+
try await Task.sleep(nanoseconds: 3_500_000_000) // 3.5 seconds
40+
#expect(4 + 4 == 8)
41+
}
42+
43+
@Test("Final Slow Test - 2 seconds")
44+
func finalSlowTest() async throws {
45+
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
46+
#expect(5 + 5 == 10)
47+
}
48+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Testing
2+
import Foundation
3+
4+
@Suite("Quick Progress Bar Demo - 10 seconds total", .serialized)
5+
struct QuickProgressBarDemo {
6+
7+
@Test("Test 1 - 1.5s")
8+
func test1() async throws {
9+
try await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
10+
#expect(1 + 1 == 2)
11+
}
12+
13+
@Test("Test 2 - 2s")
14+
func test2() async throws {
15+
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
16+
#expect(2 + 2 == 4)
17+
}
18+
19+
@Test("FAILING Test - 1s")
20+
func failingTest() async throws {
21+
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
22+
#expect(1 + 1 == 3, "This will fail!")
23+
}
24+
25+
@Test("Test 3 - 2.5s")
26+
func test3() async throws {
27+
try await Task.sleep(nanoseconds: 2_500_000_000) // 2.5 seconds
28+
#expect(3 + 3 == 6)
29+
}
30+
31+
@Test("Test 4 - 3s")
32+
func test4() async throws {
33+
try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
34+
#expect(4 + 4 == 8)
35+
}
36+
}

0 commit comments

Comments
 (0)