Skip to content

Commit 0a6d86c

Browse files
authored
Move spawnAndWait(forExecutableAtPath:) to a separate file. (#695)
Bookkeeping—move that function to its own file to make it easier to read exit tests' code. ### 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 22d6c76 commit 0a6d86c

File tree

3 files changed

+165
-151
lines changed

3 files changed

+165
-151
lines changed

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ add_library(Testing
2828
Events/TimeValue.swift
2929
ExitTests/ExitCondition.swift
3030
ExitTests/ExitTest.swift
31+
ExitTests/SpawnProcess.swift
3132
ExitTests/WaitFor.swift
3233
Expectations/Expectation.swift
3334
Expectations/Expectation+Macro.swift

Sources/Testing/ExitTests/ExitTest.swift

Lines changed: 1 addition & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -338,162 +338,12 @@ extension ExitTest {
338338
childEnvironment["SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION"] = String(decoding: json, as: UTF8.self)
339339
}
340340

341-
return try await _spawnAndWait(
341+
return try await spawnAndWait(
342342
forExecutableAtPath: childProcessExecutablePath,
343343
arguments: childArguments,
344344
environment: childEnvironment
345345
)
346346
}
347347
}
348-
349-
/// Spawn a process and wait for it to terminate.
350-
///
351-
/// - Parameters:
352-
/// - executablePath: The path to the executable to spawn.
353-
/// - arguments: The arguments to pass to the executable, not including the
354-
/// executable path.
355-
/// - environment: The environment block to pass to the executable.
356-
///
357-
/// - Returns: The exit condition of the spawned process.
358-
///
359-
/// - Throws: Any error that prevented the process from spawning or its exit
360-
/// condition from being read.
361-
private static func _spawnAndWait(
362-
forExecutableAtPath executablePath: String,
363-
arguments: [String],
364-
environment: [String: String]
365-
) async throws -> ExitCondition {
366-
// Darwin and Linux differ in their optionality for the posix_spawn types we
367-
// use, so use this typealias to paper over the differences.
368-
#if SWT_TARGET_OS_APPLE
369-
typealias P<T> = T?
370-
#elseif os(Linux) || os(FreeBSD)
371-
typealias P<T> = T
372-
#endif
373-
374-
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
375-
let pid = try withUnsafeTemporaryAllocation(of: P<posix_spawn_file_actions_t>.self, capacity: 1) { fileActions in
376-
guard 0 == posix_spawn_file_actions_init(fileActions.baseAddress!) else {
377-
throw CError(rawValue: swt_errno())
378-
}
379-
defer {
380-
_ = posix_spawn_file_actions_destroy(fileActions.baseAddress!)
381-
}
382-
383-
// Do not forward standard I/O.
384-
_ = posix_spawn_file_actions_addopen(fileActions.baseAddress!, STDIN_FILENO, "/dev/null", O_RDONLY, 0)
385-
_ = posix_spawn_file_actions_addopen(fileActions.baseAddress!, STDOUT_FILENO, "/dev/null", O_WRONLY, 0)
386-
_ = posix_spawn_file_actions_addopen(fileActions.baseAddress!, STDERR_FILENO, "/dev/null", O_WRONLY, 0)
387-
388-
return try withUnsafeTemporaryAllocation(of: P<posix_spawnattr_t>.self, capacity: 1) { attrs in
389-
guard 0 == posix_spawnattr_init(attrs.baseAddress!) else {
390-
throw CError(rawValue: swt_errno())
391-
}
392-
defer {
393-
_ = posix_spawnattr_destroy(attrs.baseAddress!)
394-
}
395-
#if SWT_TARGET_OS_APPLE
396-
// Close all other file descriptors open in the parent. Note that Linux
397-
// does not support this flag and, unlike Foundation.Process, we do not
398-
// attempt to emulate it.
399-
_ = posix_spawnattr_setflags(attrs.baseAddress!, CShort(POSIX_SPAWN_CLOEXEC_DEFAULT))
400-
#endif
401-
402-
var argv: [UnsafeMutablePointer<CChar>?] = [strdup(executablePath)]
403-
argv += arguments.lazy.map { strdup($0) }
404-
argv.append(nil)
405-
defer {
406-
for arg in argv {
407-
free(arg)
408-
}
409-
}
410-
411-
var environ: [UnsafeMutablePointer<CChar>?] = environment.map { strdup("\($0.key)=\($0.value)") }
412-
environ.append(nil)
413-
defer {
414-
for environ in environ {
415-
free(environ)
416-
}
417-
}
418-
419-
var pid = pid_t()
420-
guard 0 == posix_spawn(&pid, executablePath, fileActions.baseAddress!, attrs.baseAddress, argv, environ) else {
421-
throw CError(rawValue: swt_errno())
422-
}
423-
return pid
424-
}
425-
}
426-
427-
return try await wait(for: pid)
428-
#elseif os(Windows)
429-
// NOTE: Windows processes are responsible for handling their own
430-
// command-line escaping. This code is adapted from the code in
431-
// swift-corelibs-foundation (SEE: quoteWindowsCommandLine()) which was
432-
// itself adapted from the code published by Microsoft at
433-
// https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
434-
let commandLine = (CollectionOfOne(executablePath) + arguments).lazy
435-
.map { arg in
436-
if !arg.contains(where: {" \t\n\"".contains($0)}) {
437-
return arg
438-
}
439-
440-
var quoted = "\""
441-
var unquoted = arg.unicodeScalars
442-
while !unquoted.isEmpty {
443-
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
444-
let backslashCount = unquoted.count
445-
quoted.append(String(repeating: "\\", count: backslashCount * 2))
446-
break
447-
}
448-
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
449-
if (unquoted[firstNonBackslash] == "\"") {
450-
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
451-
quoted.append(String(unquoted[firstNonBackslash]))
452-
} else {
453-
quoted.append(String(repeating: "\\", count: backslashCount))
454-
quoted.append(String(unquoted[firstNonBackslash]))
455-
}
456-
unquoted.removeFirst(backslashCount + 1)
457-
}
458-
quoted.append("\"")
459-
return quoted
460-
}.joined(separator: " ")
461-
let environ = environment.map { "\($0.key)=\($0.value)"}.joined(separator: "\0") + "\0\0"
462-
463-
let processHandle: HANDLE! = try commandLine.withCString(encodedAs: UTF16.self) { commandLine in
464-
try environ.withCString(encodedAs: UTF16.self) { environ in
465-
var processInfo = PROCESS_INFORMATION()
466-
467-
var startupInfo = STARTUPINFOW()
468-
startupInfo.cb = DWORD(MemoryLayout.size(ofValue: startupInfo))
469-
guard CreateProcessW(
470-
nil,
471-
.init(mutating: commandLine),
472-
nil,
473-
nil,
474-
false,
475-
DWORD(CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT),
476-
.init(mutating: environ),
477-
nil,
478-
&startupInfo,
479-
&processInfo
480-
) else {
481-
throw Win32Error(rawValue: GetLastError())
482-
}
483-
_ = CloseHandle(processInfo.hThread)
484-
485-
return processInfo.hProcess
486-
}
487-
}
488-
defer {
489-
CloseHandle(processHandle)
490-
}
491-
492-
return try await wait(for: processHandle)
493-
#else
494-
#warning("Platform-specific implementation missing: process spawning unavailable")
495-
throw SystemError(description: "Exit tests are unimplemented on this platform.")
496-
#endif
497-
}
498348
}
499349
#endif
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
private import _TestingInternals
12+
13+
#if !SWT_NO_EXIT_TESTS
14+
/// Spawn a process and wait for it to terminate.
15+
///
16+
/// - Parameters:
17+
/// - executablePath: The path to the executable to spawn.
18+
/// - arguments: The arguments to pass to the executable, not including the
19+
/// executable path.
20+
/// - environment: The environment block to pass to the executable.
21+
///
22+
/// - Returns: The exit condition of the spawned process.
23+
///
24+
/// - Throws: Any error that prevented the process from spawning or its exit
25+
/// condition from being read.
26+
func spawnAndWait(
27+
forExecutableAtPath executablePath: String,
28+
arguments: [String],
29+
environment: [String: String]
30+
) async throws -> ExitCondition {
31+
// Darwin and Linux differ in their optionality for the posix_spawn types we
32+
// use, so use this typealias to paper over the differences.
33+
#if SWT_TARGET_OS_APPLE
34+
typealias P<T> = T?
35+
#elseif os(Linux) || os(FreeBSD)
36+
typealias P<T> = T
37+
#endif
38+
39+
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
40+
let pid = try withUnsafeTemporaryAllocation(of: P<posix_spawn_file_actions_t>.self, capacity: 1) { fileActions in
41+
guard 0 == posix_spawn_file_actions_init(fileActions.baseAddress!) else {
42+
throw CError(rawValue: swt_errno())
43+
}
44+
defer {
45+
_ = posix_spawn_file_actions_destroy(fileActions.baseAddress!)
46+
}
47+
48+
// Do not forward standard I/O.
49+
_ = posix_spawn_file_actions_addopen(fileActions.baseAddress!, STDIN_FILENO, "/dev/null", O_RDONLY, 0)
50+
_ = posix_spawn_file_actions_addopen(fileActions.baseAddress!, STDOUT_FILENO, "/dev/null", O_WRONLY, 0)
51+
_ = posix_spawn_file_actions_addopen(fileActions.baseAddress!, STDERR_FILENO, "/dev/null", O_WRONLY, 0)
52+
53+
return try withUnsafeTemporaryAllocation(of: P<posix_spawnattr_t>.self, capacity: 1) { attrs in
54+
guard 0 == posix_spawnattr_init(attrs.baseAddress!) else {
55+
throw CError(rawValue: swt_errno())
56+
}
57+
defer {
58+
_ = posix_spawnattr_destroy(attrs.baseAddress!)
59+
}
60+
#if SWT_TARGET_OS_APPLE
61+
// Close all other file descriptors open in the parent. Note that Linux
62+
// does not support this flag and, unlike Foundation.Process, we do not
63+
// attempt to emulate it.
64+
_ = posix_spawnattr_setflags(attrs.baseAddress!, CShort(POSIX_SPAWN_CLOEXEC_DEFAULT))
65+
#endif
66+
67+
var argv: [UnsafeMutablePointer<CChar>?] = [strdup(executablePath)]
68+
argv += arguments.lazy.map { strdup($0) }
69+
argv.append(nil)
70+
defer {
71+
for arg in argv {
72+
free(arg)
73+
}
74+
}
75+
76+
var environ: [UnsafeMutablePointer<CChar>?] = environment.map { strdup("\($0.key)=\($0.value)") }
77+
environ.append(nil)
78+
defer {
79+
for environ in environ {
80+
free(environ)
81+
}
82+
}
83+
84+
var pid = pid_t()
85+
guard 0 == posix_spawn(&pid, executablePath, fileActions.baseAddress!, attrs.baseAddress, argv, environ) else {
86+
throw CError(rawValue: swt_errno())
87+
}
88+
return pid
89+
}
90+
}
91+
92+
return try await wait(for: pid)
93+
#elseif os(Windows)
94+
// NOTE: Windows processes are responsible for handling their own
95+
// command-line escaping. This code is adapted from the code in
96+
// swift-corelibs-foundation (SEE: quoteWindowsCommandLine()) which was
97+
// itself adapted from the code published by Microsoft at
98+
// https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
99+
let commandLine = (CollectionOfOne(executablePath) + arguments).lazy
100+
.map { arg in
101+
if !arg.contains(where: {" \t\n\"".contains($0)}) {
102+
return arg
103+
}
104+
105+
var quoted = "\""
106+
var unquoted = arg.unicodeScalars
107+
while !unquoted.isEmpty {
108+
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
109+
let backslashCount = unquoted.count
110+
quoted.append(String(repeating: "\\", count: backslashCount * 2))
111+
break
112+
}
113+
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
114+
if (unquoted[firstNonBackslash] == "\"") {
115+
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
116+
quoted.append(String(unquoted[firstNonBackslash]))
117+
} else {
118+
quoted.append(String(repeating: "\\", count: backslashCount))
119+
quoted.append(String(unquoted[firstNonBackslash]))
120+
}
121+
unquoted.removeFirst(backslashCount + 1)
122+
}
123+
quoted.append("\"")
124+
return quoted
125+
}.joined(separator: " ")
126+
let environ = environment.map { "\($0.key)=\($0.value)"}.joined(separator: "\0") + "\0\0"
127+
128+
let processHandle: HANDLE! = try commandLine.withCString(encodedAs: UTF16.self) { commandLine in
129+
try environ.withCString(encodedAs: UTF16.self) { environ in
130+
var processInfo = PROCESS_INFORMATION()
131+
132+
var startupInfo = STARTUPINFOW()
133+
startupInfo.cb = DWORD(MemoryLayout.size(ofValue: startupInfo))
134+
guard CreateProcessW(
135+
nil,
136+
.init(mutating: commandLine),
137+
nil,
138+
nil,
139+
false,
140+
DWORD(CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT),
141+
.init(mutating: environ),
142+
nil,
143+
&startupInfo,
144+
&processInfo
145+
) else {
146+
throw Win32Error(rawValue: GetLastError())
147+
}
148+
_ = CloseHandle(processInfo.hThread)
149+
150+
return processInfo.hProcess
151+
}
152+
}
153+
defer {
154+
CloseHandle(processHandle)
155+
}
156+
157+
return try await wait(for: processHandle)
158+
#else
159+
#warning("Platform-specific implementation missing: process spawning unavailable")
160+
throw SystemError(description: "Exit tests are unimplemented on this platform.")
161+
#endif
162+
}
163+
#endif

0 commit comments

Comments
 (0)