Skip to content

Commit

Permalink
Support throwAssertion matcher on SwiftPM on Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
ikesyo committed May 13, 2021
1 parent 055336f commit 24a6cf3
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 12 deletions.
88 changes: 84 additions & 4 deletions Sources/Nimble/Matchers/ThrowAssertion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,89 @@
import CwlPreconditionTesting
#elseif canImport(CwlPosixPreconditionTesting)
import CwlPosixPreconditionTesting
#elseif canImport(Glibc)
// swiftlint:disable all
import Glibc

// This function is called from the signal handler to shut down the thread and return 1 (indicating a SIGILL was received).
private func callThreadExit() {
pthread_exit(UnsafeMutableRawPointer(bitPattern: 1))
}

// When called, this signal handler simulates a function call to `callThreadExit`
private func sigIllHandler(code: Int32, info: UnsafeMutablePointer<siginfo_t>?, uap: UnsafeMutableRawPointer?) -> Void {
guard let context = uap?.assumingMemoryBound(to: ucontext_t.self) else { return }

// 1. Decrement the stack pointer
context.pointee.uc_mcontext.gregs.15 /* REG_RSP */ -= Int64(MemoryLayout<Int>.size)

// 2. Save the old Instruction Pointer to the stack.
let rsp = context.pointee.uc_mcontext.gregs.15 /* REG_RSP */
if let ump = UnsafeMutablePointer<Int64>(bitPattern: Int(rsp)) {
ump.pointee = rsp
}

// 3. Set the Instruction Pointer to the new function's address
var f: @convention(c) () -> Void = callThreadExit
withUnsafePointer(to: &f) { $0.withMemoryRebound(to: Int64.self, capacity: 1) { ptr in
context.pointee.uc_mcontext.gregs.16 /* REG_RIP */ = ptr.pointee
} }
}

/// 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.
public class BadInstructionException {
}

/// 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.
/// 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.
/// - parameter block: a function without parameters that will be run
/// - returns: if an SIGILL is raised during the execution of `block` then a BadInstructionException will be returned, otherwise `nil`.
public func catchBadInstruction(block: @escaping () -> Void) -> BadInstructionException? {
// Construct the signal action
var sigActionPrev = sigaction()
var sigActionNew = sigaction()
sigemptyset(&sigActionNew.sa_mask)
sigActionNew.sa_flags = SA_SIGINFO
sigActionNew.__sigaction_handler = .init(sa_sigaction: sigIllHandler)

// Install the signal action
if sigaction(SIGILL, &sigActionNew, &sigActionPrev) != 0 {
fatalError("Sigaction error: \(errno)")
}

defer {
// Restore the previous signal action
if sigaction(SIGILL, &sigActionPrev, nil) != 0 {
fatalError("Sigaction error: \(errno)")
}
}

var b = block
let caught: Bool = withUnsafeMutablePointer(to: &b) { blockPtr in
// Run the block on its own thread
var handlerThread: pthread_t = 0
let e = pthread_create(&handlerThread, nil, { arg in
guard let arg = arg else { return nil }
(arg.assumingMemoryBound(to: (() -> Void).self).pointee)()
return nil
}, blockPtr)
precondition(e == 0, "Unable to create thread")

// Wait for completion and get the result. It will be either `nil` or bitPattern 1
var rawResult: UnsafeMutableRawPointer? = nil
let e2 = pthread_join(handlerThread, &rawResult)
precondition(e2 == 0, "Thread join failed")
return Int(bitPattern: rawResult) != 0
}

return caught ? BadInstructionException() : nil
}
// swiftlint:enable all
#endif

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

var actualError: Error?
Expand Down Expand Up @@ -43,9 +121,11 @@ public func throwAssertion<Out>() -> Predicate<Out> {
return PredicateResult(bool: caughtException != nil, message: message)
}
#else
fatalError("The throwAssertion Nimble matcher can only run on x86_64 platforms with " +
"Objective-C (e.g. macOS, iPhone 5s or later simulators). You can silence this error " +
"by placing the test case inside an #if arch(x86_64) or canImport(Darwin) conditional statement")
let message = """
The throwAssertion Nimble matcher can only run on x86_64 platforms.
You can silence this error by placing the test case inside an #if arch(x86_64) conditional statement.
"""
fatalError(message)
#endif
}
}
16 changes: 8 additions & 8 deletions Tests/NimbleTests/Matchers/ThrowAssertionTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ private let error: Error = NSError(domain: "test", code: 0, userInfo: nil)

final class ThrowAssertionTest: XCTestCase {
func testPositiveMatch() {
#if canImport(Darwin)
#if arch(x86_64)
expect { () -> Void in fatalError() }.to(throwAssertion())
#endif
}

func testErrorThrown() {
#if canImport(Darwin)
#if arch(x86_64)
expect { throw error }.toNot(throwAssertion())
#endif
}

func testPostAssertionCodeNotRun() {
#if canImport(Darwin)
#if arch(x86_64)
var reachedPoint1 = false
var reachedPoint2 = false

Expand All @@ -34,7 +34,7 @@ final class ThrowAssertionTest: XCTestCase {
}

func testNegativeMatch() {
#if canImport(Darwin)
#if arch(x86_64)
var reachedPoint1 = false

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

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

func testNegativeMessage() {
#if canImport(Darwin)
#if arch(x86_64)
failsWithErrorMessage("expected to not throw an assertion") {
expect { () -> Void in fatalError() }.toNot(throwAssertion())
}
#endif
}

func testNonVoidClosure() {
#if canImport(Darwin)
#if arch(x86_64)
expect { () -> Int in fatalError() }.to(throwAssertion())
#endif
}

func testChainOnThrowAssertion() {
#if canImport(Darwin)
#if arch(x86_64)
expect { () -> Int in return 5 }.toNot(throwAssertion()).to(equal(5))
#endif
}
Expand Down

0 comments on commit 24a6cf3

Please sign in to comment.