-
Notifications
You must be signed in to change notification settings - Fork 28
Unify tests on all supported platforms to ensure consistent behavior and add more tests. #133
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
base: main
Are you sure you want to change the base?
Conversation
84050f5
to
1c7f185
Compare
❤️ Resolves: #100 |
…and add more tests.
1c7f185
to
471a3ab
Compare
guard case .success(let currentState) = self.state else { | ||
return | ||
} | ||
|
||
guard self.shutdownFlag.add(1, ordering: .sequentiallyConsistent).newValue == 1 else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be >=1 instead of ==1? I think as written this will run the shutdown procedure n-1 times for n calls.
guard case .success(let ioPort) = ioCompletionPort, | ||
case .success(let monitorThreadHandle) = monitorThread else { | ||
return | ||
} | ||
// Make sure we don't shutdown the same instance twice | ||
guard self.shutdownFlag.add(1, ordering: .relaxed).newValue == 1 else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to be >=1?
_ body: (UnsafeMutablePointer<STARTUPINFOEXW>) throws -> Result | ||
) rethrows -> Result { | ||
var info: STARTUPINFOEXW = STARTUPINFOEXW() | ||
info.StartupInfo.cb = DWORD(MemoryLayout<STARTUPINFOEXW>.size) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can use MemoryLayout.size(ofValue: info)
to avoid repeating the type name (seems safer)
GetExitCodeProcess(self.processInformation.hProcess, &exitCode) | ||
return exitCode == STILL_ACTIVE | ||
#else | ||
return kill(self.processIdentifier.value, 0) == 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason to use kill 0 instead of waitpid with WNOHANG+WNOWAIT?
@@ -194,6 +198,19 @@ extension Execution { | |||
} | |||
} | |||
} | |||
|
|||
private func isStillAlive() -> Bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could already have exited by the time you return, perhaps a better name is "isPotentiallyStillAlive"? The only definitive case we can know is that it did exit.
arguments: ["/c", "set"], | ||
environment: .custom([ | ||
"Path": "C:\\Windows\\system32;C:\\Windows", | ||
"ComSpec": "C:\\Windows\\System32\\cmd.exe", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can use this to compute the system32 directory:
private func _url(for id: KNOWNFOLDERID) -> URL? {
var pszPath: PWSTR?
let hr: HRESULT = withUnsafePointer(to: id) { id in
SHGetKnownFolderPath(id, DWORD(WinSDK.KF_FLAG_DEFAULT.rawValue), nil, &pszPath)
}
guard hr >= 0 else { return nil }
defer { CoTaskMemFree(pszPath) }
return URL(filePath: String(decodingCString: pszPath!, as: UTF16.self), directoryHint: .isDirectory)
}
_url(for: FOLDERID_System) // C:\\Windows\\System32
var buffer: Data! | ||
while let result = try await group.next() { | ||
if let result: Data = result { | ||
buffer = result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buffer = result | |
return result |
buffer = result | ||
} | ||
} | ||
return buffer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return buffer | |
preconditionFailure("unreachable") |
output: .string(limit: 16), | ||
error: .discarded | ||
) | ||
Issue.record("Expected to throw") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this you can use:
#expect(throws: Error.self) {
// code...
}
let grandChildPid = try #require(pid_t(output)) | ||
// Make sure the grand child `/usr/bin/yes` actually exited | ||
// This is unfortunately racy because the pid isn't immediately invalided | ||
// once the exit exits. Allow a few failures and delay to counter this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// once the exit exits. Allow a few failures and delay to counter this | |
// once `kill` returns. Allow a few failures and delay to counter this |
?
try await withThrowingTaskGroup(of: Void.self) { group in | ||
for _ in 0 ..< 8 { | ||
group.addTask { | ||
// This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftover comment? nothing here is using bash
@@ -226,7 +231,6 @@ final class AsyncIO: Sendable { | |||
} | |||
} | |||
|
|||
|
|||
private func registerFileDescriptor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should registerFileDescriptor precondition that the fd is not already in the _registration
map?
#expect(result.value == expected) | ||
} | ||
|
||
@Test func stressTestWithLittleOutput() async throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✘ Test stressTestWithLittleOutput() recorded an issue at IntegrationTests.swift:1460:6: Caught error: An error occurred within the AsyncIO subsystem: failed to add 27 to epoll list. Underlying error: UnderlyingError(rawValue: 17)
Could this be because _safelyClose
does not call AsyncIO.shared.removeRegistration for the fd before closing it? Reading I/O fails because of low limit, fails, calls _safelyClose, fd is closed, fd # gets reused, tries to re-add, already in the epoll list, boom.
This PR refactors and consolidates tests across all platforms and introduces numerous additional tests.
Now, tests are categorized into three main types:
IntegrationTests
: This serves as the primary test file that evaluates the end-to-end functionality of Subprocess by executing child processes.{Unix|Darwin|Linux|Windows}Tests
: These tests are platform-specific and cater to the unique characteristics of each operating system.AsyncIOTests
: These tests are dedicated to unit testing theAsyncIO
functionality specifically.These newly established test categories have revealed a few bugs in the implementation, which are also addressed in this PR:
monitorProcessTermination
function inside thewithAsyncTaskCleanupHandler
. This adjustment ensures that the teardown sequence can still be executed even if thebody
closure terminates prematurely, but the child process remains stuck.STARTUPINFOEXW
to ensure that only the explicitly provided file descriptors are inheritable.AsyncIO.shutdown
is idempotent.Resolves: #97