From 049570052c8f1a931d395682939c05ab40f64f2e Mon Sep 17 00:00:00 2001 From: Saehej Kang <20051028+saehejkang@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:30:04 -0700 Subject: [PATCH] [build]: add tests for copy from (#1380) ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Relates to #1240 Test cases added for validation based on changes from apple/container-builder-shim#73. ## Testing - [x] Tested locally - [x] Added/updated tests - [ ] Added/updated docs --------- Co-authored-by: jwhur <57657645+JaewonHur@users.noreply.github.com> --- .../Subcommands/Build/CLIBuilderTest.swift | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift index 0c27007f..dd240daf 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift @@ -1407,5 +1407,149 @@ extension TestCLIBuildBase { ) #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") } + + @Test func testCopyFromLocalImage() throws { + let baseTempDir: URL = try createTempDir() + let tempDir: URL = try createTempDir() + defer { + try! FileManager.default.removeItem(at: baseTempDir) + try! FileManager.default.removeItem(at: tempDir) + } + + let baseImageName = "local-base:\(UUID().uuidString)" + let baseDockerfile = + """ + FROM scratch + ADD hello.txt /hello.txt + """ + let baseContext: [FileSystemEntry] = [ + .file("hello.txt", content: .data("hello\n".data(using: .utf8)!)) + ] + try createContext(tempDir: baseTempDir, dockerfile: baseDockerfile, context: baseContext) + + try self.build(tag: baseImageName, tempDir: baseTempDir) + #expect(try self.inspectImage(baseImageName) == baseImageName, "expected to have successfully built \(baseImageName)") + + let dockerfile = + """ + FROM ghcr.io/linuxcontainers/alpine:3.20 + COPY --from=\(baseImageName) /hello.txt /copied.txt + RUN cat /copied.txt + """ + try createContext(tempDir: tempDir, dockerfile: dockerfile) + + let imageName = "registry.local/copy-from-local:\(UUID().uuidString)" + try self.build(tag: imageName, tempDir: tempDir) + #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") + } + + @Test func testCopyFromBuildStage() throws { + let tempDir: URL = try createTempDir() + defer { + try! FileManager.default.removeItem(at: tempDir) + } + + let dockerfile = + """ + FROM scratch AS builder + ADD hello.txt /hello.txt + + FROM ghcr.io/linuxcontainers/alpine:3.20 + COPY --from=builder /hello.txt /copied.txt + RUN cat /copied.txt + """ + let context: [FileSystemEntry] = [ + .file("hello.txt", content: .data("hello\n".data(using: .utf8)!)) + ] + try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) + + let imageName = "registry.local/copy-from-stage:\(UUID().uuidString)" + try self.build(tag: imageName, tempDir: tempDir) + #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") + } + + @Test func testCopyRenameFromStage() throws { + let tempDir: URL = try createTempDir() + defer { + try! FileManager.default.removeItem(at: tempDir) + } + + let dockerfile = + """ + FROM scratch AS builder + ADD hello.txt /hello.txt + + FROM ghcr.io/linuxcontainers/alpine:3.20 + COPY --from=builder /hello.txt /renamed.txt + RUN cat /renamed.txt + """ + let context: [FileSystemEntry] = [ + .file("hello.txt", content: .data("hello\n".data(using: .utf8)!)) + ] + try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) + + let imageName = "registry.local/copy-rename:\(UUID().uuidString)" + try self.build(tag: imageName, tempDir: tempDir) + #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") + } + + @Test func testCopyMissingFileFails() throws { + let tempDir: URL = try createTempDir() + defer { + try! FileManager.default.removeItem(at: tempDir) + } + + let dockerfile = + """ + FROM scratch AS builder + + FROM ghcr.io/linuxcontainers/alpine:3.20 + COPY --from=builder /does-not-exist.txt /copied.txt + """ + try createContext(tempDir: tempDir, dockerfile: dockerfile) + + let imageName = "registry.local/copy-missing:\(UUID().uuidString)" + #expect(throws: Error.self) { + try self.build(tag: imageName, tempDir: tempDir) + } + } + } + + @Test func testCopyInvalidStageFails() throws { + let tempDir: URL = try createTempDir() + defer { + try! FileManager.default.removeItem(at: tempDir) + } + + let dockerfile = + """ + FROM ghcr.io/linuxcontainers/alpine:3.20 + COPY --from=not_a_stage /hello.txt /copied.txt + """ + try createContext(tempDir: tempDir, dockerfile: dockerfile) + + let imageName = "registry.local/copy-invalid-stage:\(UUID().uuidString)" + #expect(throws: Error.self) { + try self.build(tag: imageName, tempDir: tempDir) + } + } + + @Test func testCopyFromNonexistentImageFails() throws { + let tempDir: URL = try createTempDir() + defer { + try! FileManager.default.removeItem(at: tempDir) + } + + let dockerfile = + """ + FROM ghcr.io/linuxcontainers/alpine:3.20 + COPY --from=doesnotexist:latest /hello.txt /copied.txt + """ + try createContext(tempDir: tempDir, dockerfile: dockerfile) + + let imageName = "registry.local/copy-bad-image:\(UUID().uuidString)" + #expect(throws: Error.self) { + try self.build(tag: imageName, tempDir: tempDir) + } } }