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

Minor updates for StrictConcurrency and documentation updates #26

Merged
merged 2 commits into from
Apr 19, 2024
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
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 added 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
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
Loading