diff --git a/Makefile b/Makefile index a7c034ba7..5cc37b631 100644 --- a/Makefile +++ b/Makefile @@ -183,6 +183,7 @@ integration: init-block $(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIVolumes || exit_code=1 ; \ $(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIKernelSet || exit_code=1 ; \ $(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIAnonymousVolumes || exit_code=1 ; \ + $(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --no-parallel --filter TestCLINoParallelCases || exit_code=1 ; \ echo Ensuring apiserver stopped after the CLI integration tests ; \ scripts/ensure-container-stopped.sh ; \ exit $${exit_code} ; \ diff --git a/Tests/CLITests/Subcommands/Images/TestCLIImages.swift b/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift similarity index 85% rename from Tests/CLITests/Subcommands/Images/TestCLIImages.swift rename to Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift index 20d921c59..5bcb4fbca 100644 --- a/Tests/CLITests/Subcommands/Images/TestCLIImages.swift +++ b/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift @@ -20,71 +20,6 @@ import Foundation import Testing class TestCLIImagesCommand: CLITest { - func doRemoveImages(images: [String]? = nil) throws { - var args = [ - "image", - "rm", - ] - - if let images { - args.append(contentsOf: images) - } else { - args.append("--all") - } - - let (_, _, error, status) = try run(arguments: args) - if status != 0 { - throw CLIError.executionFailed("command failed: \(error)") - } - } - - func isImagePresent(targetImage: String) throws -> Bool { - let images = try doListImages() - return images.contains(where: { image in - if image.reference == targetImage { - return true - } - return false - }) - } - - func doListImages() throws -> [Image] { - let (_, output, error, status) = try run(arguments: [ - "image", - "list", - "--format", - "json", - ]) - if status != 0 { - throw CLIError.executionFailed("command failed: \(error)") - } - - guard let jsonData = output.data(using: .utf8) else { - throw CLIError.invalidOutput("image list output invalid \(output)") - } - - let decoder = JSONDecoder() - return try decoder.decode([Image].self, from: jsonData) - } - - func doImageTag(image: String, newName: String) throws { - let tagArgs = [ - "image", - "tag", - image, - newName, - ] - - let (_, _, error, status) = try run(arguments: tagArgs) - if status != 0 { - throw CLIError.executionFailed("command failed: \(error)") - } - } - -} - -extension TestCLIImagesCommand { - @Test func testPull() throws { do { try doPull(imageName: alpine) @@ -375,26 +310,6 @@ extension TestCLIImagesCommand { "Expected validation error message in output") } - @Test func testMaxConcurrentDownloadsFlag() throws { - // Test that the flag is accepted with valid values - do { - try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "1"]) - let imagePresent = try isImagePresent(targetImage: alpine) - #expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=1") - - // Clean up - try? doRemoveImages(images: [alpine]) - - // Test with higher concurrency - try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "6"]) - let imagePresent2 = try isImagePresent(targetImage: alpine) - #expect(imagePresent2, "Expected image to be pulled with maxConcurrentDownloads=6") - } catch { - Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)") - return - } - } - @Test func testImageSaveAndLoadStdinStdout() throws { do { // 1. pull image diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift similarity index 98% rename from Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift rename to Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift index 500b88f8f..0087fa927 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift @@ -513,13 +513,14 @@ class TestCLIRunCommand: CLITest { let response = try await client.execute(request, timeout: .seconds(retryDelaySeconds)) try #require(response.status == .ok) success = true + print("request to \(url) succeeded") } catch { print("request to \(url) failed, error \(error)") try await Task.sleep(for: .seconds(retryDelaySeconds)) } retriesRemaining -= 1 } - #expect(success, "Request to \(url) failed after \(retries - retriesRemaining) retries") + try #require(success, "Request to \(url) failed after \(retries - retriesRemaining) retries") try doStop(name: name) } catch { Issue.record("failed to run container \(error)") @@ -561,13 +562,14 @@ class TestCLIRunCommand: CLITest { let response = try await client.execute(request, timeout: .seconds(retryDelaySeconds)) try #require(response.status == .ok) success = true + print("request to \(url) succeeded") } catch { print("request to \(url) failed, error: \(error)") try await Task.sleep(for: .seconds(retryDelaySeconds)) } retriesRemaining -= 1 } - #expect(success, "Request to \(url) failed after \(retries - retriesRemaining) retries") + try #require(success, "Request to \(url) failed after \(retries - retriesRemaining) retries") try doStop(name: name) } catch { Issue.record("failed to run container \(error)") diff --git a/Tests/CLITests/TestCLINoParallelCases.swift b/Tests/CLITests/TestCLINoParallelCases.swift new file mode 100644 index 000000000..24472d5de --- /dev/null +++ b/Tests/CLITests/TestCLINoParallelCases.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerClient +import ContainerizationOCI +import Foundation +import Testing + +/// Tests that need total control over environment to avoid conflicts. +class TestCLINoParallelCases: CLITest { + @Test func testImageSingleConcurrentDownload() throws { + // removing this image during parallel tests breaks stuff! + _ = try? run(arguments: ["image", "rm", alpine]) + do { + try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "1"]) + let imagePresent = try isImagePresent(targetImage: alpine) + #expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=1") + } catch { + Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)") + return + } + } + + @Test func testImageManyConcurrentDownloads() throws { + // removing this image during parallel tests breaks stuff! + _ = try? run(arguments: ["image", "rm", alpine]) + do { + try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "64"]) + let imagePresent = try isImagePresent(targetImage: alpine) + #expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=64") + } catch { + Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)") + return + } + } +} diff --git a/Tests/CLITests/Utilities/CLITest.swift b/Tests/CLITests/Utilities/CLITest.swift index 83f83b02f..4da0afaf8 100644 --- a/Tests/CLITests/Utilities/CLITest.swift +++ b/Tests/CLITests/Utilities/CLITest.swift @@ -484,4 +484,65 @@ class CLITest { return try await body(tempDir) } + + func doRemoveImages(images: [String]? = nil) throws { + var args = [ + "image", + "rm", + ] + + if let images { + args.append(contentsOf: images) + } else { + args.append("--all") + } + + let (_, _, error, status) = try run(arguments: args) + if status != 0 { + throw CLIError.executionFailed("command failed: \(error)") + } + } + + func isImagePresent(targetImage: String) throws -> Bool { + let images = try doListImages() + return images.contains(where: { image in + if image.reference == targetImage { + return true + } + return false + }) + } + + func doListImages() throws -> [Image] { + let (_, output, error, status) = try run(arguments: [ + "image", + "list", + "--format", + "json", + ]) + if status != 0 { + throw CLIError.executionFailed("command failed: \(error)") + } + + guard let jsonData = output.data(using: .utf8) else { + throw CLIError.invalidOutput("image list output invalid \(output)") + } + + let decoder = JSONDecoder() + return try decoder.decode([Image].self, from: jsonData) + } + + func doImageTag(image: String, newName: String) throws { + let tagArgs = [ + "image", + "tag", + image, + newName, + ] + + let (_, _, error, status) = try run(arguments: tagArgs) + if status != 0 { + throw CLIError.executionFailed("command failed: \(error)") + } + } }