Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Syscall mock testing #8

Merged
merged 6 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ let targets: [PackageDescription.Target] = [
path: "Sources/System"),
.target(
name: "SystemInternals",
dependencies: ["CSystem"]),
dependencies: ["CSystem"],
swiftSettings: [
.define("ENABLE_MOCKING", .when(configuration: .debug))
]),
.target(
name: "CSystem",
dependencies: []),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ try fd.closeAfter {

## Adding `SystemPackage` as a Dependency

To use the `SystemPackage` library in a SwiftPM project,
To use the `SystemPackage` library in a SwiftPM project,
add the following line to the dependencies in your `Package.swift` file:

```swift
Expand Down
44 changes: 0 additions & 44 deletions Sources/SystemInternals/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,50 +35,6 @@ public var system_errno: CInt {
}
#endif

public func system_open(_ path: UnsafePointer<CChar>, _ oflag: Int32) -> CInt {
open(path, oflag)
}

public func system_open(
_ path: UnsafePointer<CChar>, _ oflag: Int32, _ mode: mode_t
) -> CInt {
open(path, oflag, mode)
}

public func system_close(_ fd: Int32) -> Int32 {
close(fd)
}

public func system_read(
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int
) -> Int {
read(fd, buf, nbyte)
}

public func system_pread(
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t
) -> Int {
pread(fd, buf, nbyte, offset)
}

public func system_lseek(
_ fd: Int32, _ off: off_t, _ whence: Int32
) -> off_t {
lseek(fd, off, whence)
}

public func system_write(
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int
) -> Int {
write(fd, buf, nbyte)
}

public func system_pwrite(
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t
) -> Int {
pwrite(fd, buf, nbyte, offset)
}

// MARK: C stdlib decls

public func system_strerror(_ __errnum: Int32) -> UnsafeMutablePointer<Int8>! {
Expand Down
159 changes: 159 additions & 0 deletions Sources/SystemInternals/Mocking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2020 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

// Syscall mocking support.
//
// NOTE: This is currently the bare minimum needed for System's testing purposes, though we do
// eventually want to expose some solution to users.
//
// Mocking is contextual, accessible through MockingDriver.withMockingEnabled. Mocking
// state, including whether it is enabled, is stored in thread-local storage. Mocking is only
// enabled in testing builds of System currently, to minimize runtime overhead of release builds.
//

#if ENABLE_MOCKING
public struct Trace {
milseman marked this conversation as resolved.
Show resolved Hide resolved
public struct Entry: Hashable {
var name: String
var arguments: [AnyHashable]

public init(name: String, _ arguments: [AnyHashable]) {
self.name = name
self.arguments = arguments
}
}

private var entries: [Entry] = []
private var firstEntry: Int = 0

public var isEmpty: Bool { firstEntry >= entries.count }

public mutating func dequeue() -> Entry? {
guard !self.isEmpty else { return nil }
defer { firstEntry += 1 }
return entries[firstEntry]
}

internal mutating func add(_ e: Entry) {
entries.append(e)
}

public mutating func clear() { entries.removeAll() }
}

// TODO: Track
public struct WriteBuffer {
public var enabled: Bool = false

private var buffer: [UInt8] = []
private var chunkSize: Int? = nil

internal mutating func write(_ buf: UnsafeRawBufferPointer) -> Int {
guard enabled else { return 0 }
let chunk = chunkSize ?? buf.count
buffer.append(contentsOf: buf.prefix(chunk))
return chunk
}

public var contents: [UInt8] { buffer }
}

public enum ForceErrno: Equatable {
case none
case always(errno: CInt)

case counted(errno: CInt, count: Int)
}

// Provide access to the driver, context, and trace stack of mocking
public class MockingDriver {
// Record syscalls and their arguments
public var trace = Trace()

// Mock errors inside syscalls
public var forceErrno = ForceErrno.none

// A buffer to put `write` bytes into
public var writeBuffer = WriteBuffer()
}

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Darwin
#elseif os(Linux) || os(FreeBSD) || os(Android)
import Glibc
#else
#error("Unsupported Platform")
#endif

internal let key: pthread_key_t = {
var raw = pthread_key_t()
guard 0 == pthread_key_create(&raw, nil) else {
fatalError("Unable to create key")
}
return raw
}()

internal var currentMockingDriver: MockingDriver? {
#if !ENABLE_MOCKING
fatalError("Contextual mocking in non-mocking build")
#endif

guard let rawPtr = pthread_getspecific(key) else { return nil }

return Unmanaged<MockingDriver>.fromOpaque(rawPtr).takeUnretainedValue()
}

extension MockingDriver {
/// Enables mocking for the duration of `f` with a clean trace queue
/// Restores prior mocking status and trace queue after execution
public static func withMockingEnabled(
_ f: (MockingDriver) throws -> ()
) rethrows {
let priorMocking = currentMockingDriver
let driver = MockingDriver()

defer {
if let object = priorMocking {
pthread_setspecific(key, Unmanaged.passUnretained(object).toOpaque())
} else {
pthread_setspecific(key, nil)
}
_fixLifetime(driver)
}

guard 0 == pthread_setspecific(key, Unmanaged.passUnretained(driver).toOpaque()) else {
fatalError("Unable to set TLSData")
}

return try f(driver)
}
}

// Check TLS for mocking
@inline(never)
private var contextualMockingEnabled: Bool {
return currentMockingDriver != nil
}

extension MockingDriver {
public static var enabled: Bool { mockingEnabled }
}

#endif // ENABLE_MOCKING

@inline(__always)
internal var mockingEnabled: Bool {
// Fast constant-foldable check for release builds
#if ENABLE_MOCKING
return contextualMockingEnabled
#else
return false
#endif
}

146 changes: 146 additions & 0 deletions Sources/SystemInternals/Syscalls.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2020 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Darwin
#elseif os(Linux) || os(FreeBSD) || os(Android)
import Glibc
#else
#error("Unsupported Platform")
#endif

#if ENABLE_MOCKING
// Strip the mock_system prefix and the arg list suffix
private func originalSyscallName(_ s: String) -> String {
precondition(s.starts(with: "system_"))
return String(s.dropFirst("system_".count).prefix { $0.isLetter })
}

private func mockImpl(
name: String,
_ args: [AnyHashable]
) -> CInt {
let origName = originalSyscallName(name)
guard let driver = currentMockingDriver else {
fatalError("Mocking requested from non-mocking context")
}
driver.trace.add(Trace.Entry(name: origName, args))

switch driver.forceErrno {
case .none: break
case .always(let e):
errno = e
return -1
case .counted(let e, let count):
assert(count >= 1)
errno = e
driver.forceErrno = count > 1 ? .counted(errno: e, count: count-1) : .none
return -1
}

return 0
}

private func mock(
name: String = #function, _ args: AnyHashable...
) -> CInt {
precondition(mockingEnabled)
return mockImpl(name: name, args)
}
private func mockInt(
name: String = #function, _ args: AnyHashable...
) -> Int {
Int(mockImpl(name: name, args))
}

private func mockOffT(
name: String = #function, _ args: AnyHashable...
) -> off_t {
off_t(mockImpl(name: name, args))
}
#endif // ENABLE_MOCKING

// Interacting with the mocking system, tracing, etc., is a potentially significant
// amount of code size, so we hand outline that code for every syscall

// open
public func system_open(_ path: UnsafePointer<CChar>, _ oflag: Int32) -> CInt {
#if ENABLE_MOCKING
if mockingEnabled { return mock(String(cString: path), oflag) }
#endif
return open(path, oflag)
}

public func system_open(
_ path: UnsafePointer<CChar>, _ oflag: Int32, _ mode: mode_t
) -> CInt {
#if ENABLE_MOCKING
if mockingEnabled { return mock(String(cString: path), oflag, mode) }
#endif
return open(path, oflag, mode)
}

// close
public func system_close(_ fd: Int32) -> Int32 {
#if ENABLE_MOCKING
if mockingEnabled { return mock(fd) }
#endif
return close(fd)
}

// read
public func system_read(
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int
) -> Int {
#if ENABLE_MOCKING
if mockingEnabled { return mockInt(fd, buf, nbyte) }
#endif
return read(fd, buf, nbyte)
}

// pread
public func system_pread(
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int, _ offset: off_t
) -> Int {
#if ENABLE_MOCKING
if mockingEnabled { return mockInt(fd, buf, nbyte, offset) }
#endif
return pread(fd, buf, nbyte, offset)
}

// lseek
public func system_lseek(
_ fd: Int32, _ off: off_t, _ whence: Int32
) -> off_t {
#if ENABLE_MOCKING
if mockingEnabled { return mockOffT(fd, off, whence) }
#endif
return lseek(fd, off, whence)
}

// write
public func system_write(
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int
) -> Int {
#if ENABLE_MOCKING
if mockingEnabled { return mockInt(fd, buf, nbyte) }
#endif
return write(fd, buf, nbyte)
}

// pwrite
public func system_pwrite(
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int, _ offset: off_t
) -> Int {
#if ENABLE_MOCKING
if mockingEnabled { return mockInt(fd, buf, nbyte, offset) }
#endif
return pwrite(fd, buf, nbyte, offset)
}

9 changes: 0 additions & 9 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
/*
This source file is part of the Swift System open source project

Copyright (c) 2020 Apple Inc. and the Swift System project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
*/

import XCTest

import SystemTests
Expand Down
Loading