Skip to content

Commit 9071f98

Browse files
authored
tests: Fix EndToEnd test deadlock under swift test, but exclude from CI (#144)
Building an SDK requires running the sdk-generator with `swift run swift-sdk-generator`. This takes a lock on `.build`, but if the tests are being run by `swift test` the outer Swift Package Manager instance will already hold this lock, causing the test to deadlock. We can avoid this by giving the `swift run swift-sdk-generator` instance its own scratch directory. This PR consolidates the code which builds SDKs in one function, then runs each SDK build with its own temporary scratch directory. This avoids the deadlock on the `.build` directory when run under `swift test`. The tests continue to pass when run under Xcode, which was not affected by the deadlock because it works in a separate scratch directory. :warning: **The tests cannot currently run in CI because SDK generator cannot use the HTTP proxy to download packages.** This PR allows the tests to be run locally with `swift test` but skips them when running in the CI. When the downloading problem is solved, the tests can be enabled in CI as well. :warning: RHEL-based Swift 6.0 SDKs built from container images currently do not currently work, as reported in #138. This PR makes it possible to run the test locally under `swift test` while working on this problem, to prevent regressions such as #141. Fixes #143
1 parent 71e2d79 commit 9071f98

File tree

3 files changed

+109
-61
lines changed

3 files changed

+109
-61
lines changed

Docker/docker-compose.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ services:
2525

2626
test:
2727
<<: *common
28+
environment:
29+
- JENKINS_URL
2830
command: /bin/bash -xcl "swift test $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}"
2931

3032
# util

Sources/SwiftSDKGenerator/SystemUtils/ByteBuffer+Utils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public extension ByteBuffer {
2121
standardInput: [self].async,
2222
collectStandardOutput: true,
2323
collectStandardError: false,
24-
perStreamCollectionLimitBytes: 10 * 1024 * 1024
24+
perStreamCollectionLimitBytes: 20 * 1024 * 1024
2525
)
2626

2727
try result.exitReason.throwIfNonZero()

Tests/SwiftSDKGeneratorTests/EndToEndTests.swift

Lines changed: 106 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,38 @@ import XCTest
1717

1818
@testable import SwiftSDKGenerator
1919

20+
extension FileManager {
21+
func withTemporaryDirectory<T>(logger: Logger, cleanup: Bool = true, body: (URL) async throws -> T) async throws -> T {
22+
// Create a temporary directory using a UUID. Throws if the directory already exists.
23+
// The docs suggest using FileManager.url(for: .itemReplacementDirectory, ...) to create a temporary directory,
24+
// but on Linux the directory name contains spaces, which means we need to be careful to quote it everywhere:
25+
//
26+
// `(A Document Being Saved By \(name))`
27+
//
28+
// https://github.com/swiftlang/swift-corelibs-foundation/blob/21b3196b33a64d53a0989881fc9a486227b4a316/Sources/Foundation/FileManager.swift#L152
29+
var logger = logger
30+
31+
let temporaryDirectory = self.temporaryDirectory.appendingPathComponent(UUID().uuidString)
32+
logger[metadataKey: "temporaryDirectory"] = "\(temporaryDirectory.path)"
33+
34+
try createDirectory(at: temporaryDirectory, withIntermediateDirectories: false)
35+
defer {
36+
// Best effort cleanup.
37+
do {
38+
if cleanup {
39+
try removeItem(at: temporaryDirectory)
40+
logger.info("Removed temporary directory")
41+
} else {
42+
logger.info("Keeping temporary directory")
43+
}
44+
} catch {}
45+
}
46+
47+
logger.info("Created temporary directory")
48+
return try await body(temporaryDirectory)
49+
}
50+
}
51+
2052
final class EndToEndTests: XCTestCase {
2153
private let testcases = [
2254
#"""
@@ -37,11 +69,40 @@ final class EndToEndTests: XCTestCase {
3769

3870
private let logger = Logger(label: "swift-sdk-generator")
3971

40-
#if !os(macOS)
41-
func testPackageInitExecutable() async throws {
42-
throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143")
72+
// Building an SDK requires running the sdk-generator with `swift run swift-sdk-generator`.
73+
// This takes a lock on `.build`, but if the tests are being run by `swift test` the outer Swift Package Manager
74+
// instance will already hold this lock, causing the test to deadlock. We can work around this by giving
75+
// the `swift run swift-sdk-generator` instance its own scratch directory.
76+
func buildSDK(inDirectory packageDirectory: FilePath, scratchPath: String, withArguments runArguments: String) async throws -> String {
77+
let generatorOutput = try await Shell.readStdout(
78+
"cd \(packageDirectory) && swift run --scratch-path \"\(scratchPath)\" swift-sdk-generator \(runArguments)"
79+
)
80+
81+
let installCommand = try XCTUnwrap(generatorOutput.split(separator: "\n").first {
82+
$0.contains("swift experimental-sdk install")
83+
})
84+
85+
let bundleName = try XCTUnwrap(
86+
FilePath(String(XCTUnwrap(installCommand.split(separator: " ").last))).components.last
87+
).stem
88+
89+
let installedSDKs = try await Shell.readStdout("swift experimental-sdk list").components(separatedBy: "\n")
90+
91+
// Make sure this bundle hasn't been installed already.
92+
if installedSDKs.contains(bundleName) {
93+
try await Shell.run("swift experimental-sdk remove \(bundleName)")
94+
}
95+
96+
let installOutput = try await Shell.readStdout(String(installCommand))
97+
XCTAssertTrue(installOutput.contains("successfully installed"))
98+
99+
return bundleName
100+
}
43101

44-
let fm = FileManager.default
102+
func testPackageInitExecutable() async throws {
103+
if ProcessInfo.processInfo.environment.keys.contains("JENKINS_URL") {
104+
throw XCTSkip("EndToEnd tests cannot currently run in CI: https://github.com/swiftlang/swift-sdk-generator/issues/145")
105+
}
45106

46107
var packageDirectory = FilePath(#filePath)
47108
packageDirectory.removeLastComponent()
@@ -58,55 +119,46 @@ final class EndToEndTests: XCTestCase {
58119
}
59120

60121
for runArguments in possibleArguments {
61-
let generatorOutput = try await Shell.readStdout(
62-
"cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)"
63-
)
64-
65-
let installCommand = try XCTUnwrap(generatorOutput.split(separator: "\n").first {
66-
$0.contains("swift experimental-sdk install")
67-
})
68-
69-
let bundleName = try XCTUnwrap(
70-
FilePath(String(XCTUnwrap(installCommand.split(separator: " ").last))).components.last
71-
).stem
72-
73-
let installedSDKs = try await Shell.readStdout("swift experimental-sdk list").components(separatedBy: "\n")
74-
75-
// Make sure this bundle hasn't been installed already.
76-
if installedSDKs.contains(bundleName) {
77-
try await Shell.run("swift experimental-sdk remove \(bundleName)")
122+
if runArguments.contains("rhel") {
123+
// Temporarily skip the RHEL-based SDK. XCTSkip() is not suitable as it would skipping the entire test case
124+
logger.warning("RHEL-based SDKs currently do not work with Swift 6.0: https://github.com/swiftlang/swift-sdk-generator/issues/138")
125+
continue
78126
}
79127

80-
let installOutput = try await Shell.readStdout(String(installCommand))
81-
XCTAssertTrue(installOutput.contains("successfully installed"))
128+
let bundleName = try await FileManager.default.withTemporaryDirectory(logger: logger) { tempDir in
129+
try await buildSDK(inDirectory: packageDirectory, scratchPath: tempDir.path, withArguments: runArguments)
130+
}
82131

83132
for testcase in self.testcases {
84-
let testPackageURL = FileManager.default.temporaryDirectory.appendingPathComponent("swift-sdk-generator-test")
85-
let testPackageDir = FilePath(testPackageURL.path)
86-
try? fm.removeItem(atPath: testPackageDir.string)
87-
try fm.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)
88-
89-
try await Shell.run("swift package --package-path \(testPackageDir) init --type executable")
90-
let main_swift = testPackageURL.appendingPathComponent("Sources/main.swift")
91-
try testcase.write(to: main_swift, atomically: true, encoding: .utf8)
92-
93-
var buildOutput = try await Shell.readStdout(
94-
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
95-
)
96-
XCTAssertTrue(buildOutput.contains("Build complete!"))
97-
try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")
98-
buildOutput = try await Shell.readStdout(
99-
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
100-
)
101-
XCTAssertTrue(buildOutput.contains("Build complete!"))
133+
try await FileManager.default.withTemporaryDirectory(logger: logger) { tempDir in
134+
let testPackageURL = tempDir.appendingPathComponent("swift-sdk-generator-test")
135+
let testPackageDir = FilePath(testPackageURL.path)
136+
try FileManager.default.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)
137+
138+
try await Shell.run("swift package --package-path \(testPackageDir) init --type executable")
139+
let main_swift = testPackageURL.appendingPathComponent("Sources/main.swift")
140+
try testcase.write(to: main_swift, atomically: true, encoding: .utf8)
141+
142+
var buildOutput = try await Shell.readStdout(
143+
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
144+
)
145+
XCTAssertTrue(buildOutput.contains("Build complete!"))
146+
147+
try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")
148+
149+
buildOutput = try await Shell.readStdout(
150+
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
151+
)
152+
XCTAssertTrue(buildOutput.contains("Build complete!"))
153+
}
102154
}
103155
}
104156
}
105157

106158
func testRepeatedSDKBuilds() async throws {
107-
throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143")
108-
109-
let fm = FileManager.default
159+
if ProcessInfo.processInfo.environment.keys.contains("JENKINS_URL") {
160+
throw XCTSkip("EndToEnd tests cannot currently run in CI: https://github.com/swiftlang/swift-sdk-generator/issues/145")
161+
}
110162

111163
var packageDirectory = FilePath(#filePath)
112164
packageDirectory.removeLastComponent()
@@ -123,22 +175,16 @@ final class EndToEndTests: XCTestCase {
123175
}
124176

125177
for runArguments in possibleArguments {
126-
let testPackageURL = FileManager.default.temporaryDirectory.appendingPathComponent("swift-sdk-generator-test")
127-
let testPackageDir = FilePath(testPackageURL.path)
128-
try? fm.removeItem(atPath: testPackageDir.string)
129-
try fm.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)
130-
defer { try? fm.removeItem(atPath: testPackageDir.string) }
131-
132-
let firstGeneratorOutput = try await Shell.readStdout(
133-
"cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)"
134-
)
135-
XCTAssert(firstGeneratorOutput.contains("swift experimental-sdk install"))
136-
137-
let repeatGeneratorOutput = try await Shell.readStdout(
138-
"cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)"
139-
)
140-
XCTAssert(repeatGeneratorOutput.contains("swift experimental-sdk install"))
178+
if runArguments.contains("rhel") {
179+
// Temporarily skip the RHEL-based SDK. XCTSkip() is not suitable as it would skipping the entire test case
180+
logger.warning("RHEL-based SDKs currently do not work with Swift 6.0: https://github.com/swiftlang/swift-sdk-generator/issues/138")
181+
continue
182+
}
183+
184+
try await FileManager.default.withTemporaryDirectory(logger: logger) { tempDir in
185+
let _ = try await buildSDK(inDirectory: packageDirectory, scratchPath: tempDir.path, withArguments: runArguments)
186+
let _ = try await buildSDK(inDirectory: packageDirectory, scratchPath: tempDir.path, withArguments: runArguments)
187+
}
141188
}
142189
}
143-
#endif
144190
}

0 commit comments

Comments
 (0)