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 2 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")
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be conditional on the current build configuration? (Or, if that's even possible, only enabled in a special test target dependency, and never in the production library?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The best I can do, IIUC, is:
.define("ENABLE_MOCKING", .when(configuration: .debug))

We also need executables as tests to build in debug and release at some point.

Copy link
Member

Choose a reason for hiding this comment

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

It's a rather unfortunate choice, but we also have the option to build & run tests outside SPM's limitations. (swift-atomics will likely also need to do that to be able to run lit tests for codegen verification.)

In this sort of setup, the test environment would build the package with ENABLE_MOCKING enabled on the swiftpm command line, enabling both the mocking infrastructure and the mocking tests themselves.

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

#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
private func releaseObject(_ raw: UnsafeMutableRawPointer) -> () {
Unmanaged<MockingDriver>.fromOpaque(raw).release()
}
#elseif os(Linux) || os(FreeBSD) || os(Android)
private func releaseObject(_ raw: UnsafeMutableRawPointer?) -> () {
guard let object = raw else { return }
Unmanaged<MockingDriver>.fromOpaque(object).release()
}
#else
#error("Unsupported Platform")
#endif


internal let key: pthread_key_t = {
var raw = pthread_key_t()
guard 0 == pthread_key_create(&raw, releaseObject) 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
defer {
if let object = priorMocking {
pthread_setspecific(key, Unmanaged.passUnretained(object).toOpaque())
} else {
pthread_setspecific(key, nil)
}
}

let driver = MockingDriver()
guard 0 == pthread_setspecific(key, Unmanaged.passRetained(driver).toOpaque()) else {
milseman marked this conversation as resolved.
Show resolved Hide resolved
fatalError("Unable to set TLSData")
}

return try f(driver)
}
}

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

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

return contextualMockingEnabled
}

Loading