Skip to content

tests: Fix EndToEnd test deadlock under swift test, but exclude from CI #144

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

Merged
merged 6 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ services:

test:
<<: *common
environment:
- JENKINS_URL
command: /bin/bash -xcl "swift test $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}"

# util
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public extension ByteBuffer {
standardInput: [self].async,
collectStandardOutput: true,
collectStandardError: false,
perStreamCollectionLimitBytes: 10 * 1024 * 1024
perStreamCollectionLimitBytes: 20 * 1024 * 1024
)

try result.exitReason.throwIfNonZero()
Expand Down
166 changes: 106 additions & 60 deletions Tests/SwiftSDKGeneratorTests/EndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,38 @@ import XCTest

@testable import SwiftSDKGenerator

extension FileManager {
func withTemporaryDirectory<T>(logger: Logger, cleanup: Bool = true, body: (URL) async throws -> T) async throws -> T {
// Create a temporary directory using a UUID. Throws if the directory already exists.
// The docs suggest using FileManager.url(for: .itemReplacementDirectory, ...) to create a temporary directory,
// but on Linux the directory name contains spaces, which means we need to be careful to quote it everywhere:
//
// `(A Document Being Saved By \(name))`
//
// https://github.com/swiftlang/swift-corelibs-foundation/blob/21b3196b33a64d53a0989881fc9a486227b4a316/Sources/Foundation/FileManager.swift#L152
var logger = logger

let temporaryDirectory = self.temporaryDirectory.appendingPathComponent(UUID().uuidString)
logger[metadataKey: "temporaryDirectory"] = "\(temporaryDirectory.path)"

try createDirectory(at: temporaryDirectory, withIntermediateDirectories: false)
defer {
// Best effort cleanup.
do {
if cleanup {
try removeItem(at: temporaryDirectory)
logger.info("Removed temporary directory")
} else {
logger.info("Keeping temporary directory")
}
} catch {}
}

logger.info("Created temporary directory")
return try await body(temporaryDirectory)
}
}

final class EndToEndTests: XCTestCase {
private let testcases = [
#"""
Expand All @@ -37,11 +69,40 @@ final class EndToEndTests: XCTestCase {

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

#if !os(macOS)
func testPackageInitExecutable() async throws {
throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143")
// 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 work around this by giving
// the `swift run swift-sdk-generator` instance its own scratch directory.
func buildSDK(inDirectory packageDirectory: FilePath, scratchPath: String, withArguments runArguments: String) async throws -> String {
let generatorOutput = try await Shell.readStdout(
"cd \(packageDirectory) && swift run --scratch-path \"\(scratchPath)\" swift-sdk-generator \(runArguments)"
)

let installCommand = try XCTUnwrap(generatorOutput.split(separator: "\n").first {
$0.contains("swift experimental-sdk install")
})

let bundleName = try XCTUnwrap(
FilePath(String(XCTUnwrap(installCommand.split(separator: " ").last))).components.last
).stem

let installedSDKs = try await Shell.readStdout("swift experimental-sdk list").components(separatedBy: "\n")

// Make sure this bundle hasn't been installed already.
if installedSDKs.contains(bundleName) {
try await Shell.run("swift experimental-sdk remove \(bundleName)")
}

let installOutput = try await Shell.readStdout(String(installCommand))
XCTAssertTrue(installOutput.contains("successfully installed"))

return bundleName
}

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

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

for runArguments in possibleArguments {
let generatorOutput = try await Shell.readStdout(
"cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)"
)

let installCommand = try XCTUnwrap(generatorOutput.split(separator: "\n").first {
$0.contains("swift experimental-sdk install")
})

let bundleName = try XCTUnwrap(
FilePath(String(XCTUnwrap(installCommand.split(separator: " ").last))).components.last
).stem

let installedSDKs = try await Shell.readStdout("swift experimental-sdk list").components(separatedBy: "\n")

// Make sure this bundle hasn't been installed already.
if installedSDKs.contains(bundleName) {
try await Shell.run("swift experimental-sdk remove \(bundleName)")
if runArguments.contains("rhel") {
// Temporarily skip the RHEL-based SDK. XCTSkip() is not suitable as it would skipping the entire test case
logger.warning("RHEL-based SDKs currently do not work with Swift 6.0: https://github.com/swiftlang/swift-sdk-generator/issues/138")
continue
}

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

for testcase in self.testcases {
let testPackageURL = FileManager.default.temporaryDirectory.appendingPathComponent("swift-sdk-generator-test")
let testPackageDir = FilePath(testPackageURL.path)
try? fm.removeItem(atPath: testPackageDir.string)
try fm.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)

try await Shell.run("swift package --package-path \(testPackageDir) init --type executable")
let main_swift = testPackageURL.appendingPathComponent("Sources/main.swift")
try testcase.write(to: main_swift, atomically: true, encoding: .utf8)

var buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")
buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
try await FileManager.default.withTemporaryDirectory(logger: logger) { tempDir in
let testPackageURL = tempDir.appendingPathComponent("swift-sdk-generator-test")
let testPackageDir = FilePath(testPackageURL.path)
try FileManager.default.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)

try await Shell.run("swift package --package-path \(testPackageDir) init --type executable")
let main_swift = testPackageURL.appendingPathComponent("Sources/main.swift")
try testcase.write(to: main_swift, atomically: true, encoding: .utf8)

var buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName)"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))

try await Shell.run("rm -rf \(testPackageDir.appending(".build"))")

buildOutput = try await Shell.readStdout(
"swift build --package-path \(testPackageDir) --experimental-swift-sdk \(bundleName) --static-swift-stdlib"
)
XCTAssertTrue(buildOutput.contains("Build complete!"))
}
}
}
}

func testRepeatedSDKBuilds() async throws {
throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143")

let fm = FileManager.default
if ProcessInfo.processInfo.environment.keys.contains("JENKINS_URL") {
throw XCTSkip("EndToEnd tests cannot currently run in CI: https://github.com/swiftlang/swift-sdk-generator/issues/145")
}

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

for runArguments in possibleArguments {
let testPackageURL = FileManager.default.temporaryDirectory.appendingPathComponent("swift-sdk-generator-test")
let testPackageDir = FilePath(testPackageURL.path)
try? fm.removeItem(atPath: testPackageDir.string)
try fm.createDirectory(atPath: testPackageDir.string, withIntermediateDirectories: true)
defer { try? fm.removeItem(atPath: testPackageDir.string) }

let firstGeneratorOutput = try await Shell.readStdout(
"cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)"
)
XCTAssert(firstGeneratorOutput.contains("swift experimental-sdk install"))

let repeatGeneratorOutput = try await Shell.readStdout(
"cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)"
)
XCTAssert(repeatGeneratorOutput.contains("swift experimental-sdk install"))
if runArguments.contains("rhel") {
// Temporarily skip the RHEL-based SDK. XCTSkip() is not suitable as it would skipping the entire test case
logger.warning("RHEL-based SDKs currently do not work with Swift 6.0: https://github.com/swiftlang/swift-sdk-generator/issues/138")
continue
}

try await FileManager.default.withTemporaryDirectory(logger: logger) { tempDir in
let _ = try await buildSDK(inDirectory: packageDirectory, scratchPath: tempDir.path, withArguments: runArguments)
let _ = try await buildSDK(inDirectory: packageDirectory, scratchPath: tempDir.path, withArguments: runArguments)
}
}
}
#endif
}