Skip to content

Commit 5f81b12

Browse files
committed
Add CLICopy Tests
1 parent e8973a2 commit 5f81b12

2 files changed

Lines changed: 224 additions & 3 deletions

File tree

Sources/ContainerCommands/Container/ContainerCopy.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//===----------------------------------------------------------------------===//
2-
// Copyright © 2025-2026 Apple Inc. and the container project authors.
2+
// Copyright © 2026 Apple Inc. and the container project authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -17,9 +17,9 @@
1717
import ArgumentParser
1818
import ContainerAPIClient
1919
import ContainerResource
20+
import Containerization
2021
import ContainerizationError
2122
import Foundation
22-
import Containerization
2323

2424
extension Application {
2525
public struct ContainerCopy: AsyncLoggableCommand {
@@ -59,7 +59,7 @@ extension Application {
5959
let client = ContainerClient()
6060
let srcRef = Self.parsePathRef(source)
6161
let dstRef = Self.parsePathRef(destination)
62-
62+
6363
switch (srcRef, dstRef) {
6464
case (.container(let id, let path), .local(let localPath)):
6565
let resolvedLocal = URL(fileURLWithPath: localPath).standardizedFileURL.path
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerizationExtras
18+
import Foundation
19+
import Testing
20+
21+
class TestCLICopyCommand: CLITest {
22+
private func getTestName() -> String {
23+
Test.current!.name.trimmingCharacters(in: ["(", ")"]).lowercased()
24+
}
25+
26+
@Test func testCopyHostToContainer() throws {
27+
do {
28+
let name = getTestName()
29+
try doCreate(name: name)
30+
defer {
31+
try? doStop(name: name)
32+
}
33+
try doStart(name: name)
34+
try waitForContainerRunning(name)
35+
36+
let tempFile = testDir.appendingPathComponent("testfile.txt")
37+
let content = "hello from host"
38+
try content.write(to: tempFile, atomically: true, encoding: .utf8)
39+
40+
let (_, _, error, status) = try run(arguments: [
41+
"copy",
42+
tempFile.path,
43+
"\(name):/tmp/",
44+
])
45+
if status != 0 {
46+
throw CLIError.executionFailed("copy failed: \(error)")
47+
}
48+
49+
let catOutput = try doExec(name: name, cmd: ["cat", "/tmp/testfile.txt"])
50+
#expect(
51+
catOutput.trimmingCharacters(in: .whitespacesAndNewlines) == content,
52+
"expected file content to be '\(content)', got '\(catOutput.trimmingCharacters(in: .whitespacesAndNewlines))'"
53+
)
54+
55+
try doStop(name: name)
56+
} catch {
57+
Issue.record("failed to copy file from host to container: \(error)")
58+
return
59+
}
60+
}
61+
62+
@Test func testCopyContainerToHost() throws {
63+
do {
64+
let name = getTestName()
65+
try doCreate(name: name)
66+
defer {
67+
try? doStop(name: name)
68+
}
69+
try doStart(name: name)
70+
try waitForContainerRunning(name)
71+
72+
let content = "hello from container"
73+
_ = try doExec(name: name, cmd: ["sh", "-c", "echo -n '\(content)' > /tmp/containerfile.txt"])
74+
75+
let destPath = testDir.appendingPathComponent("containerfile.txt")
76+
let (_, _, error, status) = try run(arguments: [
77+
"copy",
78+
"\(name):/tmp/containerfile.txt",
79+
destPath.path,
80+
])
81+
if status != 0 {
82+
throw CLIError.executionFailed("copy failed: \(error)")
83+
}
84+
85+
let hostContent = try String(contentsOfFile: destPath.path, encoding: .utf8)
86+
#expect(
87+
hostContent == content,
88+
"expected file content to be '\(content)', got '\(hostContent)'"
89+
)
90+
91+
try doStop(name: name)
92+
} catch {
93+
Issue.record("failed to copy file from container to host: \(error)")
94+
return
95+
}
96+
}
97+
98+
@Test func testCopyUsingCpAlias() throws {
99+
do {
100+
let name = getTestName()
101+
try doCreate(name: name)
102+
defer {
103+
try? doStop(name: name)
104+
}
105+
try doStart(name: name)
106+
try waitForContainerRunning(name)
107+
108+
let tempFile = testDir.appendingPathComponent("aliasfile.txt")
109+
let content = "testing cp alias"
110+
try content.write(to: tempFile, atomically: true, encoding: .utf8)
111+
112+
let (_, _, error, status) = try run(arguments: [
113+
"cp",
114+
tempFile.path,
115+
"\(name):/tmp/",
116+
])
117+
if status != 0 {
118+
throw CLIError.executionFailed("cp alias failed: \(error)")
119+
}
120+
121+
let catOutput = try doExec(name: name, cmd: ["cat", "/tmp/aliasfile.txt"])
122+
#expect(
123+
catOutput.trimmingCharacters(in: .whitespacesAndNewlines) == content,
124+
"expected file content to be '\(content)', got '\(catOutput.trimmingCharacters(in: .whitespacesAndNewlines))'"
125+
)
126+
127+
try doStop(name: name)
128+
} catch {
129+
Issue.record("failed to copy file using cp alias: \(error)")
130+
return
131+
}
132+
}
133+
134+
@Test func testCopyLocalToLocalFails() throws {
135+
let (_, _, _, status) = try run(arguments: [
136+
"copy",
137+
"/tmp/source.txt",
138+
"/tmp/dest.txt",
139+
])
140+
#expect(status != 0, "expected local-to-local copy to fail")
141+
}
142+
143+
@Test func testCopyContainerToContainerFails() throws {
144+
do {
145+
let name = getTestName()
146+
try doCreate(name: name)
147+
defer {
148+
try? doStop(name: name)
149+
}
150+
151+
let (_, _, _, status) = try run(arguments: [
152+
"copy",
153+
"\(name):/tmp/file.txt",
154+
"\(name):/tmp/file2.txt",
155+
])
156+
#expect(status != 0, "expected container-to-container copy to fail")
157+
} catch {
158+
Issue.record("failed test for container-to-container copy: \(error)")
159+
return
160+
}
161+
}
162+
163+
@Test func testCopyToNonRunningContainerFails() throws {
164+
do {
165+
let name = getTestName()
166+
try doCreate(name: name)
167+
defer {
168+
try? doStop(name: name)
169+
}
170+
171+
let tempFile = testDir.appendingPathComponent("norun.txt")
172+
try "test".write(to: tempFile, atomically: true, encoding: .utf8)
173+
174+
let (_, _, _, status) = try run(arguments: [
175+
"copy",
176+
tempFile.path,
177+
"\(name):/tmp/",
178+
])
179+
#expect(status != 0, "expected copy to non-running container to fail")
180+
} catch {
181+
Issue.record("failed test for copy to non-running container: \(error)")
182+
return
183+
}
184+
}
185+
186+
@Test func testCopyHostToContainerWithoutTrailingSlash() throws {
187+
do {
188+
let name = getTestName()
189+
try doCreate(name: name)
190+
defer {
191+
try? doStop(name: name)
192+
}
193+
try doStart(name: name)
194+
try waitForContainerRunning(name)
195+
196+
let tempFile = testDir.appendingPathComponent("noslash.txt")
197+
let content = "no trailing slash"
198+
try content.write(to: tempFile, atomically: true, encoding: .utf8)
199+
200+
let (_, _, error, status) = try run(arguments: [
201+
"copy",
202+
tempFile.path,
203+
"\(name):/tmp",
204+
])
205+
if status != 0 {
206+
throw CLIError.executionFailed("copy failed: \(error)")
207+
}
208+
209+
let catOutput = try doExec(name: name, cmd: ["cat", "/tmp/noslash.txt"])
210+
#expect(
211+
catOutput.trimmingCharacters(in: .whitespacesAndNewlines) == content,
212+
"expected file content to be '\(content)', got '\(catOutput.trimmingCharacters(in: .whitespacesAndNewlines))'"
213+
)
214+
215+
try doStop(name: name)
216+
} catch {
217+
Issue.record("failed to copy file without trailing slash: \(error)")
218+
return
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)