From c785d050436f601a6f3897671d88435a844d1f51 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Tue, 5 Nov 2024 10:49:08 +0000 Subject: [PATCH 1/6] tests: Remove unused temporary directory from testRepeatedSDKBuilds --- Tests/SwiftSDKGeneratorTests/EndToEndTests.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift index c839349..a433555 100644 --- a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift +++ b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift @@ -106,8 +106,6 @@ final class EndToEndTests: XCTestCase { 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 - var packageDirectory = FilePath(#filePath) packageDirectory.removeLastComponent() packageDirectory.removeLastComponent() @@ -123,12 +121,6 @@ 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)" ) From d2512a1e3dcf271c549a174ea392241ac25916f0 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Tue, 5 Nov 2024 10:12:01 +0000 Subject: [PATCH 2/6] tests: Extract SDK build into a function The EndToEnd tests build SDKs in several places. This commit consolidates this duplicated code in one function. This makes it easier for the next commit to give each build a temporary scratch directory, which will fix the problem of the tests deadlocking when run under `swift test`. --- .../EndToEndTests.swift | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift index a433555..ea9ee65 100644 --- a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift +++ b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift @@ -38,6 +38,32 @@ final class EndToEndTests: XCTestCase { private let logger = Logger(label: "swift-sdk-generator") #if !os(macOS) + func buildSDK(inDirectory packageDirectory: FilePath, withArguments runArguments: String) async throws -> String { + 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)") + } + + let installOutput = try await Shell.readStdout(String(installCommand)) + XCTAssertTrue(installOutput.contains("successfully installed")) + + return bundleName + } + func testPackageInitExecutable() async throws { throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143") @@ -58,24 +84,7 @@ 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)") - } + let bundleName = try await buildSDK(inDirectory: packageDirectory, withArguments: runArguments) let installOutput = try await Shell.readStdout(String(installCommand)) XCTAssertTrue(installOutput.contains("successfully installed")) @@ -121,15 +130,8 @@ final class EndToEndTests: XCTestCase { } for runArguments in possibleArguments { - 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")) + let _ = try await buildSDK(inDirectory: packageDirectory, withArguments: runArguments) + let _ = try await buildSDK(inDirectory: packageDirectory, withArguments: runArguments) } } #endif From 881b9c7217878d13b96fc07e4732bebd0f48b907 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Tue, 5 Nov 2024 10:05:16 +0000 Subject: [PATCH 3/6] tests: Use a temporary scratch directory when building an SDK 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. --- .../EndToEndTests.swift | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift index ea9ee65..7ea5d18 100644 --- a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift +++ b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift @@ -17,6 +17,38 @@ import XCTest @testable import SwiftSDKGenerator +extension FileManager { + func withTemporaryDirectory(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 = [ #""" @@ -37,10 +69,14 @@ final class EndToEndTests: XCTestCase { private let logger = Logger(label: "swift-sdk-generator") + // 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. #if !os(macOS) - func buildSDK(inDirectory packageDirectory: FilePath, withArguments runArguments: String) async throws -> String { + func buildSDK(inDirectory packageDirectory: FilePath, scratchPath: String, withArguments runArguments: String) async throws -> String { let generatorOutput = try await Shell.readStdout( - "cd \(packageDirectory) && swift run swift-sdk-generator \(runArguments)" + "cd \(packageDirectory) && swift run --scratch-path \"\(scratchPath)\" swift-sdk-generator \(runArguments)" ) let installCommand = try XCTUnwrap(generatorOutput.split(separator: "\n").first { @@ -84,10 +120,9 @@ final class EndToEndTests: XCTestCase { } for runArguments in possibleArguments { - let bundleName = try await buildSDK(inDirectory: packageDirectory, withArguments: runArguments) - - 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") @@ -130,8 +165,10 @@ final class EndToEndTests: XCTestCase { } for runArguments in possibleArguments { - let _ = try await buildSDK(inDirectory: packageDirectory, withArguments: runArguments) - let _ = try await buildSDK(inDirectory: packageDirectory, withArguments: runArguments) + 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 From 1d92dfc6fa364bbb9f2fba46b55e55a7a49b2704 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Tue, 5 Nov 2024 11:10:38 +0000 Subject: [PATCH 4/6] tests: Use withTemporaryDirectory wrapper in body of testPackageInitExecutable `testPackageInitExecutable` checks that the default Swift package generated by `swift package init` can be built with each SDK under test. The packages are already generated in temporary directories, so they do not suffer from the deadlocking problem when run under `swift test`. Currently each test run uses a directory with the same name in the user's temporary directory. This means that the test has to remove anything left behind by a previous run before starting a new on. Using `withTemporaryDirectory` avoids this problem because it creates a fresh temporary directory for each run and deletes it automatically when the body closure returns. --- .../EndToEndTests.swift | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift index 7ea5d18..d56eadf 100644 --- a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift +++ b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift @@ -103,8 +103,6 @@ final class EndToEndTests: XCTestCase { func testPackageInitExecutable() async throws { throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143") - let fm = FileManager.default - var packageDirectory = FilePath(#filePath) packageDirectory.removeLastComponent() packageDirectory.removeLastComponent() @@ -125,24 +123,27 @@ final class EndToEndTests: XCTestCase { } 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!")) + } } } } From a14657f3919aadec395e931cfc8ddc1dc9dccbcc Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Tue, 5 Nov 2024 10:13:00 +0000 Subject: [PATCH 5/6] tests: Enable EndToEnd tests, but skip in CI The EndToEnd tests no longer deadlock under `swift test` and can now be run on macOS and Linux. The CI cannot currently run the end to end tests because SDK generator cannot use the CI HTTP proxy to download packages. This commit explicitly skips the tests when running under CI. The tests can be run under macOS and Linux for local testing. Fixes: #143 --- Docker/docker-compose.yaml | 2 ++ .../EndToEndTests.swift | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Docker/docker-compose.yaml b/Docker/docker-compose.yaml index 18e3832..3db9408 100644 --- a/Docker/docker-compose.yaml +++ b/Docker/docker-compose.yaml @@ -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 diff --git a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift index d56eadf..39dba58 100644 --- a/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift +++ b/Tests/SwiftSDKGeneratorTests/EndToEndTests.swift @@ -73,7 +73,6 @@ final class EndToEndTests: XCTestCase { // 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. - #if !os(macOS) 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)" @@ -101,7 +100,9 @@ final class EndToEndTests: XCTestCase { } func testPackageInitExecutable() async throws { - throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143") + 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() @@ -118,6 +119,12 @@ final class EndToEndTests: XCTestCase { } for runArguments in possibleArguments { + 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 bundleName = try await FileManager.default.withTemporaryDirectory(logger: logger) { tempDir in try await buildSDK(inDirectory: packageDirectory, scratchPath: tempDir.path, withArguments: runArguments) } @@ -149,7 +156,9 @@ final class EndToEndTests: XCTestCase { } func testRepeatedSDKBuilds() async throws { - throw XCTSkip("EndToEnd tests currently deadlock under `swift test`: https://github.com/swiftlang/swift-sdk-generator/issues/143") + 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() @@ -166,11 +175,16 @@ final class EndToEndTests: XCTestCase { } for runArguments in possibleArguments { + 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 } From 8bd80dfff668d9518bfa4c754e5034a2b3742b77 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Mon, 11 Nov 2024 13:29:47 +0000 Subject: [PATCH 6/6] generator: Increase the buffer used with gzip Packages.gz for jammy-updates now expands to more than 10MB. With the previous buffer size, ProcessExecutor.TooMuchProcessOutputError would be thrown when unpacking this file. --- Sources/SwiftSDKGenerator/SystemUtils/ByteBuffer+Utils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftSDKGenerator/SystemUtils/ByteBuffer+Utils.swift b/Sources/SwiftSDKGenerator/SystemUtils/ByteBuffer+Utils.swift index d9aa860..b56cd26 100644 --- a/Sources/SwiftSDKGenerator/SystemUtils/ByteBuffer+Utils.swift +++ b/Sources/SwiftSDKGenerator/SystemUtils/ByteBuffer+Utils.swift @@ -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()