Skip to content
Open
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: 9 additions & 3 deletions Sources/Subprocess/IO/Input.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ public struct NoInput: InputProtocol {

/// Writes the input to the subprocess asynchronously.
public func write(with writer: StandardInputWriter) async throws {
fatalError("Unexpected call to \(#function)")
// Intentional no-op
// NoInput redirects stdin to /dev/null, so there is no
// data to write to the subprocess.
}

internal init() {}
Expand Down Expand Up @@ -93,7 +95,9 @@ public struct FileDescriptorInput: InputProtocol {

/// Writes the input to the subprocess asynchronously.
public func write(with writer: StandardInputWriter) async throws {
fatalError("Unexpected call to \(#function)")
// Intentional no-op
// FileDescriptorInput reads from a pre-existing file
// descriptor, so there is no data to write to the subprocess.
}

internal init(
Expand Down Expand Up @@ -148,7 +152,9 @@ internal struct CustomWriteInput: InputProtocol {
/// Asynchronously write the input to the subprocess using the
/// write file descriptor.
public func write(with writer: StandardInputWriter) async throws {
fatalError("Unexpected call to \(#function)")
// Intentional no-op
// CustomWriteInput exposes the StandardInputWriter directly
// to the caller's closure, so writing is handled there instead.
}

internal init() {}
Expand Down
16 changes: 6 additions & 10 deletions Sources/Subprocess/IO/Output.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ internal import Dispatch
// MARK: - Output

/// A type that serves as the output target for a subprocess.
///
/// Instead of creating custom implementations of ``OutputProtocol``, use the
/// built-in implementations provided by the `Subprocess` library.
public protocol OutputProtocol: Sendable, ~Copyable {
associatedtype OutputType: Sendable

Expand Down Expand Up @@ -111,12 +108,10 @@ public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol, ErrorOut

/// Creates a string from a raw span.
public func output(from span: RawSpan) throws -> String? {
// FIXME: Span to String
var array: [UInt8] = []
for index in 0..<span.byteCount {
array.append(span.unsafeLoad(fromByteOffset: index, as: UInt8.self))
span.withUnsafeBytes { ptr in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(not a issue) This TODO was more about switching to a String initializer that takes a Span directly. I don't think that API is ready yet unfortunately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I know the intention. I was looking for it so eagerly the other day too. For now at least this change stops us from potentially reallocating the array... still need a Span one though unfortunately.

let array = Array(ptr)
return String(decodingBytes: array, as: Encoding.self)
}
return String(decodingBytes: array, as: Encoding.self)
}

internal init(limit: Int, encoding: Encoding.Type) {
Expand Down Expand Up @@ -165,7 +160,7 @@ public struct BytesOutput: OutputProtocol, ErrorOutputProtocol {

/// Creates an array from a ``RawSpan``.
public func output(from span: RawSpan) throws -> [UInt8] {
fatalError("Not implemented")
span.withUnsafeBytes { Array($0) }
}

internal init(limit: Int) {
Expand Down Expand Up @@ -378,7 +373,8 @@ extension OutputProtocol where OutputType == Void {

/// Converts the output from a raw span to the expected output type.
public func output(from span: RawSpan) throws {
fatalError("Unexpected call to \(#function)")
// When OutputType is Void, there is no output to process,
// So this is effectively a no-op.
}
}

Expand Down
63 changes: 63 additions & 0 deletions Tests/SubprocessTests/ProtocolConformanceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

import Testing
import Subprocess

#if canImport(Darwin)
import Foundation
#else
import FoundationEssentials
#endif

@Suite(.serialized)
struct ProtocolConformanceTests {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(nit) Any particular reason to create a new test file instead of just reusing IntegrationTests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm used to having a struct for one particular kind of test, so I created another one here. Honestly I didn't look closely what IntegrationTest contains. I can move back if you prefer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ahh not at all. It's probably a good idea to split them up anyways.


@Test func customOutputJSON() async throws {
struct JSONOutput<T: Decodable & Sendable>: OutputProtocol {
typealias OutputType = T

func output(from span: RawSpan) throws -> T {
try span.withUnsafeBytes { buffer in
let data = Data(bytes: buffer.baseAddress!, count: buffer.count)
return try JSONDecoder().decode(T.self, from: data)
}
}
}

struct Item: Codable, Sendable, Equatable {
let title: String
}

let json = #"{"title":"Hello from Subprocess"}"#

#if os(Windows)
let result = try await Subprocess.run(
.name("powershell.exe"),
arguments: ["-Command", "Write-Output '\(json)'"],
output: JSONOutput<Item>(),
error: .discarded
)
#else
let result = try await Subprocess.run(
.name("echo"),
arguments: [json],
output: JSONOutput<Item>(),
error: .discarded
)
#endif

#expect(result.terminationStatus.isSuccess)
#expect(result.standardOutput == Item(title: "Hello from Subprocess"))
#expect(result.standardOutput.title == "Hello from Subprocess")
}

}
Loading