Skip to content

Commit 39d4d6d

Browse files
authored
Merge pull request #891 from Quick/swiftpm-linux-throwassertion-redux
Support `throwAssertion` matcher on SwiftPM on Linux
2 parents 055336f + 24a6cf3 commit 39d4d6d

File tree

2 files changed

+92
-12
lines changed

2 files changed

+92
-12
lines changed

Sources/Nimble/Matchers/ThrowAssertion.swift

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,89 @@
22
import CwlPreconditionTesting
33
#elseif canImport(CwlPosixPreconditionTesting)
44
import CwlPosixPreconditionTesting
5+
#elseif canImport(Glibc)
6+
// swiftlint:disable all
7+
import Glibc
8+
9+
// This function is called from the signal handler to shut down the thread and return 1 (indicating a SIGILL was received).
10+
private func callThreadExit() {
11+
pthread_exit(UnsafeMutableRawPointer(bitPattern: 1))
12+
}
13+
14+
// When called, this signal handler simulates a function call to `callThreadExit`
15+
private func sigIllHandler(code: Int32, info: UnsafeMutablePointer<siginfo_t>?, uap: UnsafeMutableRawPointer?) -> Void {
16+
guard let context = uap?.assumingMemoryBound(to: ucontext_t.self) else { return }
17+
18+
// 1. Decrement the stack pointer
19+
context.pointee.uc_mcontext.gregs.15 /* REG_RSP */ -= Int64(MemoryLayout<Int>.size)
20+
21+
// 2. Save the old Instruction Pointer to the stack.
22+
let rsp = context.pointee.uc_mcontext.gregs.15 /* REG_RSP */
23+
if let ump = UnsafeMutablePointer<Int64>(bitPattern: Int(rsp)) {
24+
ump.pointee = rsp
25+
}
26+
27+
// 3. Set the Instruction Pointer to the new function's address
28+
var f: @convention(c) () -> Void = callThreadExit
29+
withUnsafePointer(to: &f) { $0.withMemoryRebound(to: Int64.self, capacity: 1) { ptr in
30+
context.pointee.uc_mcontext.gregs.16 /* REG_RIP */ = ptr.pointee
31+
} }
32+
}
33+
34+
/// Without Mach exceptions or the Objective-C runtime, there's nothing to put in the exception object. It's really just a boolean – either a SIGILL was caught or not.
35+
public class BadInstructionException {
36+
}
37+
38+
/// Run the provided block. If a POSIX SIGILL is received, handle it and return a BadInstructionException (which is just an empty object in this POSIX signal version). Otherwise return nil.
39+
/// NOTE: This function is only intended for use in test harnesses – use in a distributed build is almost certainly a bad choice. If a SIGILL is received, the block will be interrupted using a C `longjmp`. The risks associated with abrupt jumps apply here: most Swift functions are *not* interrupt-safe. Memory may be leaked and the program will not necessarily be left in a safe state.
40+
/// - parameter block: a function without parameters that will be run
41+
/// - returns: if an SIGILL is raised during the execution of `block` then a BadInstructionException will be returned, otherwise `nil`.
42+
public func catchBadInstruction(block: @escaping () -> Void) -> BadInstructionException? {
43+
// Construct the signal action
44+
var sigActionPrev = sigaction()
45+
var sigActionNew = sigaction()
46+
sigemptyset(&sigActionNew.sa_mask)
47+
sigActionNew.sa_flags = SA_SIGINFO
48+
sigActionNew.__sigaction_handler = .init(sa_sigaction: sigIllHandler)
49+
50+
// Install the signal action
51+
if sigaction(SIGILL, &sigActionNew, &sigActionPrev) != 0 {
52+
fatalError("Sigaction error: \(errno)")
53+
}
54+
55+
defer {
56+
// Restore the previous signal action
57+
if sigaction(SIGILL, &sigActionPrev, nil) != 0 {
58+
fatalError("Sigaction error: \(errno)")
59+
}
60+
}
61+
62+
var b = block
63+
let caught: Bool = withUnsafeMutablePointer(to: &b) { blockPtr in
64+
// Run the block on its own thread
65+
var handlerThread: pthread_t = 0
66+
let e = pthread_create(&handlerThread, nil, { arg in
67+
guard let arg = arg else { return nil }
68+
(arg.assumingMemoryBound(to: (() -> Void).self).pointee)()
69+
return nil
70+
}, blockPtr)
71+
precondition(e == 0, "Unable to create thread")
72+
73+
// Wait for completion and get the result. It will be either `nil` or bitPattern 1
74+
var rawResult: UnsafeMutableRawPointer? = nil
75+
let e2 = pthread_join(handlerThread, &rawResult)
76+
precondition(e2 == 0, "Thread join failed")
77+
return Int(bitPattern: rawResult) != 0
78+
}
79+
80+
return caught ? BadInstructionException() : nil
81+
}
82+
// swiftlint:enable all
583
#endif
684

785
public func throwAssertion<Out>() -> Predicate<Out> {
886
return Predicate { actualExpression in
9-
#if arch(x86_64) && canImport(Darwin)
87+
#if arch(x86_64) && (canImport(Darwin) || canImport(Glibc))
1088
let message = ExpectationMessage.expectedTo("throw an assertion")
1189

1290
var actualError: Error?
@@ -43,9 +121,11 @@ public func throwAssertion<Out>() -> Predicate<Out> {
43121
return PredicateResult(bool: caughtException != nil, message: message)
44122
}
45123
#else
46-
fatalError("The throwAssertion Nimble matcher can only run on x86_64 platforms with " +
47-
"Objective-C (e.g. macOS, iPhone 5s or later simulators). You can silence this error " +
48-
"by placing the test case inside an #if arch(x86_64) or canImport(Darwin) conditional statement")
124+
let message = """
125+
The throwAssertion Nimble matcher can only run on x86_64 platforms.
126+
You can silence this error by placing the test case inside an #if arch(x86_64) conditional statement.
127+
"""
128+
fatalError(message)
49129
#endif
50130
}
51131
}

Tests/NimbleTests/Matchers/ThrowAssertionTest.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ private let error: Error = NSError(domain: "test", code: 0, userInfo: nil)
66

77
final class ThrowAssertionTest: XCTestCase {
88
func testPositiveMatch() {
9-
#if canImport(Darwin)
9+
#if arch(x86_64)
1010
expect { () -> Void in fatalError() }.to(throwAssertion())
1111
#endif
1212
}
1313

1414
func testErrorThrown() {
15-
#if canImport(Darwin)
15+
#if arch(x86_64)
1616
expect { throw error }.toNot(throwAssertion())
1717
#endif
1818
}
1919

2020
func testPostAssertionCodeNotRun() {
21-
#if canImport(Darwin)
21+
#if arch(x86_64)
2222
var reachedPoint1 = false
2323
var reachedPoint2 = false
2424

@@ -34,7 +34,7 @@ final class ThrowAssertionTest: XCTestCase {
3434
}
3535

3636
func testNegativeMatch() {
37-
#if canImport(Darwin)
37+
#if arch(x86_64)
3838
var reachedPoint1 = false
3939

4040
expect { reachedPoint1 = true }.toNot(throwAssertion())
@@ -44,7 +44,7 @@ final class ThrowAssertionTest: XCTestCase {
4444
}
4545

4646
func testPositiveMessage() {
47-
#if canImport(Darwin)
47+
#if arch(x86_64)
4848
failsWithErrorMessage("expected to throw an assertion") {
4949
expect { () -> Void? in return }.to(throwAssertion())
5050
}
@@ -56,21 +56,21 @@ final class ThrowAssertionTest: XCTestCase {
5656
}
5757

5858
func testNegativeMessage() {
59-
#if canImport(Darwin)
59+
#if arch(x86_64)
6060
failsWithErrorMessage("expected to not throw an assertion") {
6161
expect { () -> Void in fatalError() }.toNot(throwAssertion())
6262
}
6363
#endif
6464
}
6565

6666
func testNonVoidClosure() {
67-
#if canImport(Darwin)
67+
#if arch(x86_64)
6868
expect { () -> Int in fatalError() }.to(throwAssertion())
6969
#endif
7070
}
7171

7272
func testChainOnThrowAssertion() {
73-
#if canImport(Darwin)
73+
#if arch(x86_64)
7474
expect { () -> Int in return 5 }.toNot(throwAssertion()).to(equal(5))
7575
#endif
7676
}

0 commit comments

Comments
 (0)