Skip to content

Commit 751f06d

Browse files
authored
Fix two flakyOutOfProcessReferenceResolver tests (#374)
* Fix flaky test by handling partial response data chunks. * Fix another flaky test * Reduce the risk of a deadlock when reading additional response chunks
1 parent 5d00682 commit 751f06d

File tree

2 files changed

+69
-57
lines changed

2 files changed

+69
-57
lines changed

Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ private class LongRunningProcess: ExternalLinkResolving {
437437

438438
try process.run()
439439

440-
let errorReadSource = DispatchSource.makeReadSource(fileDescriptor: errorOutput.fileHandleForReading.fileDescriptor)
440+
let errorReadSource = DispatchSource.makeReadSource(fileDescriptor: errorOutput.fileHandleForReading.fileDescriptor, queue: .main)
441441
errorReadSource.setEventHandler { [errorOutput] in
442442
let data = errorOutput.fileHandleForReading.availableData
443443
let errorMessage = String(data: data, encoding: .utf8)
@@ -462,23 +462,39 @@ private class LongRunningProcess: ExternalLinkResolving {
462462

463463
func sendAndWait<Request: Codable & CustomStringConvertible, Response: Codable>(request: Request?) throws -> Response {
464464
if let request = request {
465-
guard let requestString = String(
466-
data: try JSONEncoder().encode(request), encoding: .utf8)?.appending("\n"),
465+
guard let requestString = String(data: try JSONEncoder().encode(request), encoding: .utf8)?.appending("\n"),
467466
let requestData = requestString.data(using: .utf8)
468467
else {
469468
throw OutOfProcessReferenceResolver.Error.unableToEncodeRequestToClient(requestDescription: request.description)
470469
}
471470
input.fileHandleForWriting.write(requestData)
472471
}
473-
let response = output.fileHandleForReading.availableData
472+
var response = output.fileHandleForReading.availableData
474473
guard !response.isEmpty else {
475474
throw OutOfProcessReferenceResolver.Error.processDidExit(code: Int(process.terminationStatus))
476475
}
477476

478-
do {
479-
return try JSONDecoder().decode(Response.self, from: response)
480-
} catch {
481-
throw OutOfProcessReferenceResolver.Error.unableToDecodeResponseFromClient(response, error)
477+
// It's not guaranteed that the full response will be available all at once.
478+
while true {
479+
// If a pipe is empty, checking `availableData` will block until there is new data to read.
480+
do {
481+
// To avoid blocking forever we check if the response can be decoded after each chunk of data.
482+
return try JSONDecoder().decode(Response.self, from: response)
483+
} catch {
484+
if case DecodingError.dataCorrupted = error, // If the data wasn't valid JSON, read more data and try to decode it again.
485+
response.count.isMultiple(of: Int(PIPE_BUF)) // To reduce the risk of deadlocking, check that bytes so far is a multiple of the pipe buffer size.
486+
{
487+
let moreResponseData = output.fileHandleForReading.availableData
488+
guard !moreResponseData.isEmpty else {
489+
throw OutOfProcessReferenceResolver.Error.processDidExit(code: Int(process.terminationStatus))
490+
}
491+
response += moreResponseData
492+
continue
493+
}
494+
495+
// Other errors are re-thrown as wrapped errors.
496+
throw OutOfProcessReferenceResolver.Error.unableToDecodeResponseFromClient(response, error)
497+
}
482498
}
483499
}
484500

Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift

Lines changed: 45 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,25 @@ class OutOfProcessReferenceResolverTests: XCTestCase {
145145

146146
func testResolvingTopicLinkProcess() throws {
147147
#if os(macOS)
148-
throw XCTSkip("This test is flaky (rdar://91678333)")
149-
150-
// try assertResolvesTopicLink(makeResolver: { testMetadata in
151-
// let temporaryFolder = try createTemporaryDirectory()
152-
// let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable")
153-
//
154-
// let encodedMetadata = try String(data: JSONEncoder().encode(testMetadata), encoding: .utf8)!
155-
//
156-
// try """
157-
// #!/bin/bash
158-
// echo '{"bundleIdentifier":"com.test.bundle"}' # Write this resolver's bundle identifier
159-
// read # Wait for docc to send a topic URL
160-
// echo '{"resolvedInformation":\(encodedMetadata)}' # Respond with the test metadata (above)
161-
// """.write(to: executableLocation, atomically: true, encoding: .utf8)
162-
//
163-
// // `0o0700` is `-rwx------` (read, write, & execute only for owner)
164-
// try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path)
165-
// XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path))
166-
//
167-
// return try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in })
168-
// })
148+
try assertResolvesTopicLink(makeResolver: { testMetadata in
149+
let temporaryFolder = try createTemporaryDirectory()
150+
let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable")
151+
152+
let encodedMetadata = try String(data: JSONEncoder().encode(testMetadata), encoding: .utf8)!
153+
154+
try """
155+
#!/bin/bash
156+
echo '{"bundleIdentifier":"com.test.bundle"}' # Write this resolver's bundle identifier
157+
read # Wait for docc to send a topic URL
158+
echo '{"resolvedInformation":\(encodedMetadata)}' # Respond with the test metadata (above)
159+
""".write(to: executableLocation, atomically: true, encoding: .utf8)
160+
161+
// `0o0700` is `-rwx------` (read, write, & execute only for owner)
162+
try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path)
163+
XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path))
164+
165+
return try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in })
166+
})
169167

170168
#endif
171169
}
@@ -370,34 +368,32 @@ class OutOfProcessReferenceResolverTests: XCTestCase {
370368
}
371369

372370
func testForwardsErrorOutputProcess() throws {
373-
throw XCTSkip("This test is flaky rdar://88498898")
374-
375-
// #if os(macOS)
376-
// let temporaryFolder = try createTemporaryDirectory()
377-
//
378-
// let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable")
379-
// try """
380-
// #!/bin/bash
381-
// echo "Some error output" 1>&2 # Write to stderr
382-
// echo '{"bundleIdentifier":"com.test.bundle"}' # Write this resolver's bundle identifier
383-
// read # Wait for docc to send a topic URL
384-
// """.write(to: executableLocation, atomically: true, encoding: .utf8)
385-
//
386-
// // `0o0700` is `-rwx------` (read, write, & execute only for owner)
387-
// try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path)
388-
// XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path))
389-
//
390-
// let didReadErrorOutputExpectation = AsyncronousExpectation(description: "Did read forwarded error output.")
391-
// DispatchQueue.global().async {
392-
// let resolver = try? OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: {
393-
// errorMessage in
394-
// XCTAssertEqual(errorMessage, "Some error output\n")
395-
// didReadErrorOutputExpectation.fulfill()
396-
// })
397-
// XCTAssertEqual(resolver?.bundleIdentifier, "com.test.bundle")
398-
// }
399-
// XCTAssertNotEqual(didReadErrorOutputExpectation.wait(timeout: 20.0), .timedOut)
400-
// #endif
371+
#if os(macOS)
372+
let temporaryFolder = try createTemporaryDirectory()
373+
374+
let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable")
375+
try """
376+
#!/bin/bash
377+
echo '{"bundleIdentifier":"com.test.bundle"}' # Write this resolver's bundle identifier
378+
echo "Some error output" 1>&2 # Write to stderr
379+
read # Wait for docc to send a topic URL
380+
""".write(to: executableLocation, atomically: true, encoding: .utf8)
381+
382+
// `0o0700` is `-rwx------` (read, write, & execute only for owner)
383+
try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path)
384+
XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path))
385+
386+
let didReadErrorOutputExpectation = expectation(description: "Did read forwarded error output.")
387+
388+
let resolver = try? OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: {
389+
errorMessage in
390+
XCTAssertEqual(errorMessage, "Some error output\n")
391+
didReadErrorOutputExpectation.fulfill()
392+
})
393+
XCTAssertEqual(resolver?.bundleIdentifier, "com.test.bundle")
394+
395+
wait(for: [didReadErrorOutputExpectation], timeout: 20.0)
396+
#endif
401397
}
402398

403399
func assertForwardsResolverErrors(resolver: OutOfProcessReferenceResolver) throws {

0 commit comments

Comments
 (0)