Skip to content
Closed
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")
]),
.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
160 changes: 160 additions & 0 deletions Sources/SystemInternals/Mocking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
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.
//

public struct Trace {
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 {
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 {
// Whether to bypass this shim and go straight to the syscall
public var enableMocking = false

// 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()
func releaseObject(_ raw: UnsafeMutableRawPointer) -> () {
Unmanaged<MockingDriver>.fromOpaque(raw).release()
}
guard 0 == pthread_key_create(&raw, releaseObject) else {
fatalError("Unable to create key")
}
// TODO: All threads are sharing the same object; this is wrong
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lorentey this is where I'm probably using TLS wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this looks doable -- the key thing is that you only want to create the key here, but leave its value NULL. The setspecific call needs to be moved within withMockingEnabled -- the mocking driver must only be set on threads that want to mock, and the wrappers need to bypass all mocking if they see getspecific return NULL.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware of nested withMockingEnabled invocations, it's easy to accidentally overwrite an active pointer & leak memory (setspecific doesn't run the destructor on the previous value -- so saving & restoring will work)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withMockingEnabled does swap out the state, but I think we can simplify it by having it swap out a reference. Good idea, and we should also add a test for that.

let object = MockingDriver()

guard 0 == pthread_setspecific(raw, Unmanaged.passRetained(object).toOpaque()) else {
fatalError("Unable to set TLSData")
}
return raw
}()

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

// TODO: Do we need a lazy initialization check here?
return Unmanaged<MockingDriver>.fromOpaque(pthread_getspecific(key)!).takeUnretainedValue()
}

extension MockingDriver {
/// Whether mocking is enabled on this thread
public static var enabled: Bool { mockingEnabled }

/// 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.enableMocking
defer {
currentMockingDriver.enableMocking = priorMocking
}
currentMockingDriver.enableMocking = true

let oldTrace = currentMockingDriver.trace
defer { currentMockingDriver.trace = oldTrace }

currentMockingDriver.trace.clear()
return try f(currentMockingDriver)
}
}

// Check TLS for mocking
@inline(never)
private var contextualMockingEnabled: Bool {
return currentMockingDriver.enableMocking
}

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

return contextualMockingEnabled
}

Loading