Skip to content

Commit 265a7ed

Browse files
authored
Add a Pipe type. (#694)
This PR adds a move-only wrapper for POSIX and Windows anonymous pipes (i.e. those created with `pipe(2)`.) We will need pipes in order to implement platform-independent interprocess communication such as that required by exit tests or future adoption of distributed actors. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent dcc3a1f commit 265a7ed

File tree

2 files changed

+168
-17
lines changed

2 files changed

+168
-17
lines changed

Sources/Testing/Support/FileHandle.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,54 @@ struct FileHandle: ~Copyable, Sendable {
120120
_closeWhenDone = closeWhenDone
121121
}
122122

123+
/// Initialize an instance of this type with an existing POSIX file descriptor
124+
/// for reading.
125+
///
126+
/// - Parameters:
127+
/// - fd: The POSIX file descriptor to wrap. The caller is responsible for
128+
/// ensuring that this file handle is open in the expected mode and that
129+
/// another part of the system won't close it.
130+
/// - mode: The mode `fd` was opened with, such as `"wb"`.
131+
///
132+
/// - Throws: Any error preventing the stream from being opened.
133+
///
134+
/// The resulting file handle takes ownership of `fd` and closes it when it is
135+
/// deinitialized or if an error is thrown from this initializer.
136+
init(unsafePOSIXFileDescriptor fd: CInt, mode: String) throws {
137+
#if os(Windows)
138+
let fileHandle = _fdopen(fd, mode)
139+
#else
140+
let fileHandle = fdopen(fd, mode)
141+
#endif
142+
guard let fileHandle else {
143+
let errorCode = swt_errno()
144+
#if os(Windows)
145+
_close(fd)
146+
#else
147+
_TestingInternals.close(fd)
148+
#endif
149+
throw CError(rawValue: errorCode)
150+
}
151+
self.init(unsafeCFILEHandle: fileHandle, closeWhenDone: true)
152+
}
153+
123154
deinit {
124155
if _closeWhenDone {
125156
fclose(_fileHandle)
126157
}
127158
}
128159

160+
/// Close this file handle.
161+
///
162+
/// This function effectively deinitializes the file handle.
163+
///
164+
/// - Warning: This function closes the underlying C file handle even if
165+
/// `closeWhenDone` was `false` when this instance was initialized. Callers
166+
/// must take care not to close file handles they do not own.
167+
consuming func close() {
168+
_closeWhenDone = true
169+
}
170+
129171
/// Call a function and pass the underlying C file handle to it.
130172
///
131173
/// - Parameters:
@@ -383,6 +425,77 @@ extension FileHandle {
383425
}
384426
}
385427

428+
// MARK: - Pipes
429+
430+
extension FileHandle {
431+
/// A type representing a bidirectional pipe between two file handles.
432+
struct Pipe: ~Copyable, Sendable {
433+
/// The end of the pipe capable of reading.
434+
var readEnd: FileHandle
435+
436+
/// The end of the pipe capable of writing.
437+
var writeEnd: FileHandle
438+
439+
/// Initialize a new anonymous pipe.
440+
///
441+
/// - Throws: Any error that prevented creation of the pipe.
442+
init() throws {
443+
let (fdReadEnd, fdWriteEnd) = try withUnsafeTemporaryAllocation(of: CInt.self, capacity: 2) { fds in
444+
#if os(Windows)
445+
guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY) else {
446+
throw CError(rawValue: swt_errno())
447+
}
448+
#else
449+
guard 0 == pipe(fds.baseAddress) else {
450+
throw CError(rawValue: swt_errno())
451+
}
452+
#endif
453+
return (fds[0], fds[1])
454+
}
455+
456+
// NOTE: Partial initialization of a move-only type is disallowed, as is
457+
// conditional initialization of a local move-only value, which is why
458+
// this section looks a little awkward.
459+
let readEnd: FileHandle
460+
do {
461+
readEnd = try FileHandle(unsafePOSIXFileDescriptor: fdReadEnd, mode: "rb")
462+
} catch {
463+
#if os(Windows)
464+
_close(fdWriteEnd)
465+
#else
466+
_TestingInternals.close(fdWriteEnd)
467+
#endif
468+
throw error
469+
}
470+
let writeEnd = try FileHandle(unsafePOSIXFileDescriptor: fdWriteEnd, mode: "wb")
471+
self.readEnd = readEnd
472+
self.writeEnd = writeEnd
473+
}
474+
475+
/// Close the read end of this pipe.
476+
///
477+
/// - Returns: The remaining open end of the pipe.
478+
///
479+
/// After calling this function, the read end is closed and the write end
480+
/// remains open.
481+
consuming func closeReadEnd() -> FileHandle {
482+
readEnd.close()
483+
return writeEnd
484+
}
485+
486+
/// Close the write end of this pipe.
487+
///
488+
/// - Returns: The remaining open end of the pipe.
489+
///
490+
/// After calling this function, the write end is closed and the read end
491+
/// remains open.
492+
consuming func closeWriteEnd() -> FileHandle {
493+
writeEnd.close()
494+
return readEnd
495+
}
496+
}
497+
}
498+
386499
// MARK: - Attributes
387500

388501
extension FileHandle {

Tests/TestingTests/Support/FileHandleTests.swift

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ struct FileHandleTests {
4444
}
4545
}
4646

47+
#if !os(Windows) // Windows does not like invalid file descriptors.
48+
@Test("Init from invalid file descriptor")
49+
func invalidFileDescriptor() throws {
50+
#expect(throws: CError.self) {
51+
_ = try FileHandle(unsafePOSIXFileDescriptor: -1, mode: "")
52+
}
53+
}
54+
#endif
55+
4756
#if os(Windows)
4857
@Test("Can get Windows file HANDLE")
4958
func fileHANDLE() throws {
@@ -54,6 +63,16 @@ struct FileHandleTests {
5463
}
5564
#endif
5665

66+
#if SWT_TARGET_OS_APPLE
67+
@Test("close() function")
68+
func closeFunction() async throws {
69+
try await confirmation("File handle closed") { closed in
70+
let fileHandle = try fileHandleForCloseMonitoring(with: closed)
71+
fileHandle.close()
72+
}
73+
}
74+
#endif
75+
5776
@Test("Can write to a file")
5877
func canWrite() throws {
5978
try withTemporaryPath { path in
@@ -132,25 +151,25 @@ struct FileHandleTests {
132151

133152
@Test("Can recognize opened pipe")
134153
func isPipe() throws {
135-
#if os(Windows)
136-
var rHandle: HANDLE?
137-
var wHandle: HANDLE?
138-
try #require(CreatePipe(&rHandle, &wHandle, nil, 0))
139-
if let rHandle {
140-
CloseHandle(rHandle)
154+
let pipe = try FileHandle.Pipe()
155+
#expect(pipe.readEnd.isPipe as Bool)
156+
#expect(pipe.writeEnd.isPipe as Bool)
157+
}
158+
159+
#if SWT_TARGET_OS_APPLE
160+
@Test("Can close ends of a pipe")
161+
func closeEndsOfPipe() async throws {
162+
try await confirmation("File handle closed", expectedCount: 2) { closed in
163+
var pipe1 = try FileHandle.Pipe()
164+
pipe1.readEnd = try fileHandleForCloseMonitoring(with: closed)
165+
_ = pipe1.closeReadEnd()
166+
167+
var pipe2 = try FileHandle.Pipe()
168+
pipe2.writeEnd = try fileHandleForCloseMonitoring(with: closed)
169+
_ = pipe2.closeWriteEnd()
141170
}
142-
let fdWrite = _open_osfhandle(intptr_t(bitPattern: wHandle), 0)
143-
let file = try #require(_fdopen(fdWrite, "wb"))
144-
#else
145-
var fds: [CInt] = [-1, -1]
146-
try #require(0 == pipe(&fds))
147-
try #require(fds[1] >= 0)
148-
close(fds[0])
149-
let file = try #require(fdopen(fds[1], "wb"))
150-
#endif
151-
let fileHandle = FileHandle(unsafeCFILEHandle: file, closeWhenDone: true)
152-
#expect(Bool(fileHandle.isPipe))
153171
}
172+
#endif
154173

155174
@Test("/dev/null is not a TTY or pipe")
156175
func devNull() throws {
@@ -239,4 +258,23 @@ func temporaryDirectory() throws -> String {
239258
#endif
240259
}
241260

261+
#if SWT_TARGET_OS_APPLE
262+
func fileHandleForCloseMonitoring(with confirmation: Confirmation) throws -> FileHandle {
263+
let context = Unmanaged.passRetained(confirmation as AnyObject).toOpaque()
264+
let file = try #require(
265+
funopen(
266+
context,
267+
{ _, _, _ in 0 },
268+
nil,
269+
nil,
270+
{ context in
271+
let confirmation = Unmanaged<AnyObject>.fromOpaque(context!).takeRetainedValue() as! Confirmation
272+
confirmation()
273+
return 0
274+
}
275+
) as SWT_FILEHandle?
276+
)
277+
return FileHandle(unsafeCFILEHandle: file, closeWhenDone: false)
278+
}
279+
#endif
242280
#endif

0 commit comments

Comments
 (0)