Skip to content

Commit

Permalink
Minor updates for StrictConcurrency and documentation updates
Browse files Browse the repository at this point in the history
  • Loading branch information
mlink committed Apr 15, 2024
1 parent 2fdacca commit 711c42a
Show file tree
Hide file tree
Showing 14 changed files with 107 additions and 49 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,32 @@ on:
jobs:
spm:
name: SwiftPM build and test
runs-on: macos-13
runs-on: macos-14
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
sudo xcode-select -s /Applications/Xcode_15.3.app
- uses: actions/checkout@v3
- name: Build swift packages
run: swift build -v
- name: Run tests
run: swift test -v
carthage:
name: Xcode project build and test
runs-on: macos-13
runs-on: macos-14
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
sudo xcode-select -s /Applications/Xcode_15.3.app
- uses: actions/checkout@v3
- name: Build xcode project
run: xcodebuild build -scheme 'SubprocessMocks' -derivedDataPath .build
- name: Run tests
run: xcodebuild test -scheme 'Subprocess' -derivedDataPath .build
cocoapods:
name: Pod lib lint
runs-on: macos-13
runs-on: macos-14
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
sudo xcode-select -s /Applications/Xcode_15.3.app
- uses: actions/checkout@v3
- name: Lib lint
run: pod lib lint --verbose Subprocess.podspec --allow-warnings
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 3.0.3 - 2024-04-15

### Changed
- Correctly turned on `StrictConcurrency` in Swift 5.10 and earlier and adeed non-breaking conformance to `Sendable`.
- Updated documentation for closure based usage where `nonisolated(unsafe)` is required to avoid an error in projects that use `StrictConcurrency`.

## 3.0.2 - 2024-02-07

### Added
Expand Down
93 changes: 71 additions & 22 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
// swift-tools-version: 5.9
// swift-tools-version: 5.10

import PackageDescription

#if swift(<6)
let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("StrictConcurrency"),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("ImplicitOpenExistentials"),
.enableUpcomingFeature("BareSlashRegexLiterals"),
.enableUpcomingFeature("ConciseMagicFile"),
]
#else
let swiftSettings: [SwiftSetting] = []
#endif

let package = Package(
name: "Subprocess",
platforms: [ .macOS("10.15.4") ],
Expand Down Expand Up @@ -46,30 +33,92 @@ let package = Package(
targets: [
.target(
name: "Subprocess",
dependencies: [],
swiftSettings: swiftSettings
dependencies: []
),
.target(
name: "SubprocessMocks",
dependencies: [
.target(name: "Subprocess")
],
swiftSettings: swiftSettings
]
),
.testTarget(
name: "UnitTests",
dependencies: [
.target(name: "Subprocess"),
.target(name: "SubprocessMocks")
],
swiftSettings: swiftSettings
]
),
.testTarget(
name: "SystemTests",
dependencies: [
.target(name: "Subprocess")
],
swiftSettings: swiftSettings
]
)
]
)

for target in package.targets {
var swiftSettings = target.swiftSettings ?? []

// According to Swift's piecemeal adoption plan features that were
// upcoming features that become language defaults and are still enabled
// as upcoming features will result in a compiler error. Currently in the
// latest 5.10 compiler this doesn't happen, the compiler ignores it.
//
// If the situation does change and enabling default language features
// does result in an error in future versions we attempt to guard against
// this by using the hasFeature(x) compiler directive to see if we have a
// feature already, or if we can enable it. It's safe to enable features
// that don't exist in older compiler versions as the compiler will ignore
// features it doesn't have implemented.

// swift 6
#if !hasFeature(ConciseMagicFile)
swiftSettings.append(.enableUpcomingFeature("ConciseMagicFile"))
#endif

#if !hasFeature(ForwardTrailingClosures)
swiftSettings.append(.enableUpcomingFeature("ForwardTrailingClosures"))
#endif

#if !hasFeature(StrictConcurrency)
swiftSettings.append(.enableUpcomingFeature("StrictConcurrency"))
// StrictConcurrency is under experimental features in Swift <=5.10 contrary to some posts and documentation
swiftSettings.append(.enableExperimentalFeature("StrictConcurrency"))
#endif

#if !hasFeature(BareSlashRegexLiterals)
swiftSettings.append(.enableUpcomingFeature("BareSlashRegexLiterals"))
#endif

#if !hasFeature(ImplicitOpenExistentials)
swiftSettings.append(.enableUpcomingFeature("ImplicitOpenExistentials"))
#endif

#if !hasFeature(ImportObjcForwardDeclarations)
swiftSettings.append(.enableUpcomingFeature("ImportObjcForwardDeclarations"))
#endif

#if !hasFeature(DisableOutwardActorInference)
swiftSettings.append(.enableUpcomingFeature("DisableOutwardActorInference"))
#endif

#if !hasFeature(InternalImportsByDefault)
swiftSettings.append(.enableUpcomingFeature("InternalImportsByDefault"))
#endif

#if !hasFeature(IsolatedDefaultValues)
swiftSettings.append(.enableUpcomingFeature("IsolatedDefaultValues"))
#endif

#if !hasFeature(GlobalConcurrency)
swiftSettings.append(.enableUpcomingFeature("GlobalConcurrency"))
#endif

// swift 7
#if !hasFeature(ExistentialAny)
swiftSettings.append(.enableUpcomingFeature("ExistentialAny"))
#endif

target.swiftSettings = swiftSettings
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,16 @@ if process.exitCode == 0 {
```swift
let command: [String] = ...
let process = Subprocess(command)
nonisolated(unsafe) var outputData: Data?
nonisolated(unsafe) var errorData: Data?

// The outputHandler and errorHandler are invoked serially
try process.launch(outputHandler: { data in
// Handle new data read from stdout
outputData = data
}, errorHandler: { data in
// Handle new data read from stderr
errorData = data
}, terminationHandler: { process in
// Handle process termination, all scheduled calls to
// the outputHandler and errorHandler are guaranteed to
Expand Down
2 changes: 1 addition & 1 deletion Sources/Subprocess/Pipe+AsyncBytes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extension Pipe {

public func makeAsyncIterator() -> AsyncStream<Element>.Iterator {
AsyncStream { continuation in
pipe.fileHandleForReading.readabilityHandler = { handle in
pipe.fileHandleForReading.readabilityHandler = { @Sendable handle in
let availableData = handle.availableData

guard !availableData.isEmpty else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Subprocess/Shell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Foundation
public class Shell {

/// OptionSet representing output handling
public struct OutputOptions: OptionSet {
public struct OutputOptions: OptionSet, Sendable {
public let rawValue: Int

/// Processes data written to stdout
Expand Down
8 changes: 4 additions & 4 deletions Sources/Subprocess/Subprocess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Combine
/// Class used for asynchronous process execution
public class Subprocess: @unchecked Sendable {
/// Output options.
public struct OutputOptions: OptionSet {
public struct OutputOptions: OptionSet, Sendable {
public let rawValue: Int

/// Buffer standard output.
Expand Down Expand Up @@ -118,7 +118,7 @@ public class Subprocess: @unchecked Sendable {
///
/// It is the callers responsibility to ensure that any reads occur if waiting for the process to exit otherwise a deadlock can happen if the process is waiting to write to its output buffer.
/// A task group can be used to wait for exit while reading the output. If the output is discardable consider passing (`[]`) an empty set for the options which effectively flushes output to null.
public func run(options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: () async -> Void) {
public func run(options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: @Sendable () async -> Void) {
let standardOutput: Pipe.AsyncBytes = {
if options.contains(.standardOutput) {
let pipe = Pipe()
Expand Down Expand Up @@ -162,7 +162,7 @@ public class Subprocess: @unchecked Sendable {
}
}
}
let waitUntilExit = {
let waitUntilExit = { @Sendable in
await task.value
}

Expand Down Expand Up @@ -216,7 +216,7 @@ public class Subprocess: @unchecked Sendable {
/// }
/// }
///
public func run<Input>(standardInput: Input, options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: () async -> Void) where Input : AsyncSequence, Input.Element == UInt8 {
public func run<Input>(standardInput: Input, options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: @Sendable () async -> Void) where Input : AsyncSequence, Input.Element == UInt8 {
process.standardInput = try SubprocessDependencyBuilder.shared.makeInputPipe(sequence: standardInput)
return try run(options: options)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Subprocess/SubprocessDependencyBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public protocol SubprocessDependencyFactory {
/// Default implementation of SubprocessDependencyFactory
public struct SubprocessDependencyBuilder: SubprocessDependencyFactory {
private static let queue = DispatchQueue(label: "\(Self.self)")
private static var _shared: any SubprocessDependencyFactory = SubprocessDependencyBuilder()
nonisolated(unsafe) private static var _shared: any SubprocessDependencyFactory = SubprocessDependencyBuilder()
/// Shared instance used for dependency creation
public static var shared: any SubprocessDependencyFactory {
get {
Expand Down Expand Up @@ -80,7 +80,7 @@ public struct SubprocessDependencyBuilder: SubprocessDependencyFactory {
return try FileHandle(forReadingFrom: url)
}

public func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence, Input.Element == UInt8 {
public func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence & Sendable, Input.Element == UInt8 {
let pipe = Pipe()
// see here: https://developer.apple.com/forums/thread/690382
let result = fcntl(pipe.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SubprocessMocks/MockOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import Foundation

/// A way to supply data to mock methods
public protocol MockOutput {
public protocol MockOutput: Sendable {
var data: Data { get }
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SubprocessMocks/MockProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Subprocess
#endif

/// Interface used for mocking a process
public struct MockProcess {
public struct MockProcess: Sendable {

/// The underlying `MockProcessReference`
public var reference: MockProcessReference
Expand Down Expand Up @@ -100,7 +100,7 @@ open class MockProcessReference: Process {

/// Creates a new `MockProcessReference` calling run stub block
/// - Parameter block: Block used to stub `Process.run`
public init(withRunBlock block: @escaping (MockProcess) -> Void) {
public init(withRunBlock block: @escaping @Sendable (MockProcess) -> Void) {
context = Context(runStub: { mock in
Task(priority: .userInitiated) {
block(mock)
Expand Down
4 changes: 2 additions & 2 deletions Sources/SubprocessMocks/MockSubprocess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public extension Subprocess {
/// - Parameters:
/// - command: The command to mock
/// - runBlock: Block called with a `MockProcess` to mock process execution.
static func stub(_ command: [String], runBlock: ((MockProcess) -> Void)? = nil) {
static func stub(_ command: [String], runBlock: (@Sendable (MockProcess) -> Void)? = nil) {
let mock = MockProcessReference(withRunBlock: runBlock ?? { $0.exit() })
MockSubprocessDependencyBuilder.shared.stub(command, process: mock)
}
Expand Down Expand Up @@ -116,7 +116,7 @@ public extension Subprocess {
/// - file: Source file where expect was called (Default: #file)
/// - line: Line number of source file where expect was called (Default: #line)
/// - runBlock: Block called with a `MockProcess` to mock process execution
static func expect(_ command: [String], input: Input? = nil, file: StaticString = #file, line: UInt = #line, runBlock: ((MockProcess) -> Void)? = nil) {
static func expect(_ command: [String], input: Input? = nil, file: StaticString = #file, line: UInt = #line, runBlock: (@Sendable (MockProcess) -> Void)? = nil) {
let mock = MockProcessReference(withRunBlock: runBlock ?? { $0.exit() })
MockSubprocessDependencyBuilder.shared.expect(command, input: input, process: mock, file: file, line: line)
}
Expand Down
5 changes: 2 additions & 3 deletions Sources/SubprocessMocks/MockSubprocessDependencyBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ public final class MockPipe: Pipe {
}

class MockSubprocessDependencyBuilder {

class MockItem {
var used = false
var command: [String]
Expand All @@ -108,7 +107,7 @@ class MockSubprocessDependencyBuilder {

var mocks: [MockItem] = []

static let shared = MockSubprocessDependencyBuilder()
nonisolated(unsafe) static let shared = MockSubprocessDependencyBuilder()

init() { SubprocessDependencyBuilder.shared = self }

Expand Down Expand Up @@ -242,7 +241,7 @@ extension MockSubprocessDependencyBuilder: SubprocessDependencyFactory {
return handle
}

func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence, Input.Element == UInt8 {
func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence & Sendable, Input.Element == UInt8 {
let semaphore = DispatchSemaphore(value: 0)
let pipe = MockPipe()

Expand Down
2 changes: 1 addition & 1 deletion Tests/SystemTests/SubprocessSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ final class SubprocessSystemTests: XCTestCase {
switch line {
case "hello":
Task {
input.yield("world\n")
_ = input.yield("world\n")
}
case "world":
input.yield("and\nuniverse")
Expand Down
8 changes: 4 additions & 4 deletions Tests/UnitTests/SubprocessTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ final class SubprocessTests: XCTestCase {
func testGetPID() throws {
// Given
let mockCalled = expectation(description: "Mock setup called")
var expectedPID: Int32?
nonisolated(unsafe) var expectedPID: Int32?
Subprocess.expect(command) { mock in
expectedPID = mock.reference.processIdentifier
mockCalled.fulfill()
Expand Down Expand Up @@ -185,7 +185,7 @@ final class SubprocessTests: XCTestCase {

// MARK: suspend

func testSuspend() throws {
@MainActor func testSuspend() throws {
// Given
let semaphore = DispatchSemaphore(value: 0)
let suspendCalled = expectation(description: "Suspend called")
Expand All @@ -210,7 +210,7 @@ final class SubprocessTests: XCTestCase {

// MARK: resume

func testResume() throws {
@MainActor func testResume() throws {
// Given
let semaphore = DispatchSemaphore(value: 0)
let resumeCalled = expectation(description: "Resume called")
Expand All @@ -235,7 +235,7 @@ final class SubprocessTests: XCTestCase {

// MARK: kill

func testKill() throws {
@MainActor func testKill() throws {
// Given
let semaphore = DispatchSemaphore(value: 0)
let terminateCalled = expectation(description: "Terminate called")
Expand Down

0 comments on commit 711c42a

Please sign in to comment.