| 1 | // Copyright (C) Microsoft Corporation. All rights reserved. |
| 2 | |
| 3 | #include "precomp.h" |
| 4 | #include "windows/Common.h" |
| 5 | #include "WSLCExecutor.h" |
| 6 | #include "WSLCE2EHelpers.h" |
| 7 | #include "TestImageRegistry.h" |
| 8 | |
| 9 | namespace WSLCE2ETests { |
| 10 | using namespace wsl::shared; |
| 11 | |
| 12 | class WSLCE2EContainerCpTests |
| 13 | { |
| 14 | WSLC_TEST_CLASS(WSLCE2EContainerCpTests) |
| 15 | |
| 16 | TEST_CLASS_SETUP(ClassSetup) |
| 17 | { |
| 18 | TestImageRegistry::Instance().EnsureLoaded(DebianImage); |
| 19 | return true; |
| 20 | } |
| 21 | |
| 22 | TEST_CLASS_CLEANUP(ClassCleanup) |
| 23 | { |
| 24 | EnsureContainerDoesNotExist(WslcContainerName); |
| 25 | return true; |
| 26 | } |
| 27 | |
| 28 | TEST_METHOD_SETUP(MethodSetup) |
| 29 | { |
| 30 | EnsureContainerDoesNotExist(WslcContainerName); |
| 31 | TarPath = std::filesystem::current_path() / L"wslc-cp-test.tar"; |
| 32 | DeleteFileW(TarPath.c_str()); |
| 33 | return true; |
| 34 | } |
| 35 | |
| 36 | TEST_METHOD_CLEANUP(MethodCleanup) |
| 37 | { |
| 38 | EnsureContainerDoesNotExist(WslcContainerName); |
| 39 | DeleteFileW(TarPath.c_str()); |
| 40 | return true; |
| 41 | } |
| 42 | |
| 43 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_HelpCommand) |
| 44 | { |
| 45 | auto result = RunWslc(L"container cp --help"); |
| 46 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 47 | VERIFY_ARE_EQUAL(0u, result.ExitCode.value()); |
| 48 | VERIFY_IS_TRUE(result.Stdout.has_value()); |
| 49 | VERIFY_IS_TRUE(result.Stdout->find(L"container cp") != std::wstring::npos); |
| 50 | VERIFY_IS_TRUE(result.Stdout->find(L"source") != std::wstring::npos); |
| 51 | VERIFY_IS_TRUE(result.Stdout->find(L"target") != std::wstring::npos); |
| 52 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 53 | VERIFY_ARE_EQUAL(L"", result.Stderr.value()); |
| 54 | } |
| 55 | |
| 56 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingBothArgs) |
| 57 | { |
| 58 | const auto result = RunWslc(L"container cp"); |
| 59 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 60 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 61 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 62 | VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'source'") != std::wstring::npos); |
| 63 | } |
| 64 | |
| 65 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingTarget) |
| 66 | { |
| 67 | const auto result = RunWslc(L"container cp -"); |
| 68 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 69 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 70 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 71 | VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'target'") != std::wstring::npos); |
| 72 | } |
| 73 | |
| 74 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinIsTerminal) |
| 75 | { |
| 76 | // Running without piped stdin should fail with a terminal error. |
| 77 | // RunWslcAndRedirectToFile gives the child a real console stdout handle, |
| 78 | // and since RunWslc pipes NUL to stdin, we use RunWslcAndRedirectToFile |
| 79 | // with no output path to get a real console for the child. |
| 80 | const auto result = RunWslcAndRedirectToFile(L"container cp - fakecontainer:/path"); |
| 81 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 82 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 83 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 84 | VERIFY_IS_TRUE(result.Stderr->find(L"Cannot read tar data from terminal") != std::wstring::npos); |
| 85 | } |
| 86 | |
| 87 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_SourceNotStdin) |
| 88 | { |
| 89 | // A local file that doesn't exist should fail with a "source not found" error. |
| 90 | // Use RunWslc which pipes NUL to stdin (not a terminal). |
| 91 | const auto result = RunWslc(L"container cp somefile.tar fakecontainer:/path"); |
| 92 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 93 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 94 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 95 | VERIFY_IS_TRUE(result.Stderr->find(L"Source path not found: somefile.tar") != std::wstring::npos); |
| 96 | } |
| 97 | |
| 98 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_NoColon) |
| 99 | { |
| 100 | // Target must be CONTAINER:PATH — missing colon should fail. |
| 101 | const auto result = RunWslc(L"container cp - fakecontainer_nopath"); |
| 102 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 103 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 104 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 105 | VERIFY_IS_TRUE(result.Stderr->find(L"Invalid copy direction") != std::wstring::npos); |
| 106 | } |
| 107 | |
| 108 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyContainer) |
| 109 | { |
| 110 | // Target with empty container name (:path) should fail. |
| 111 | const auto result = RunWslc(L"container cp - :/path"); |
| 112 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 113 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 114 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 115 | VERIFY_IS_TRUE(result.Stderr->find(L"Invalid copy direction") != std::wstring::npos); |
| 116 | } |
| 117 | |
| 118 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyPath) |
| 119 | { |
| 120 | // Target with empty path (container:) should fail. |
| 121 | const auto result = RunWslc(L"container cp - fakecontainer:"); |
| 122 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 123 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 124 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 125 | VERIFY_IS_TRUE(result.Stderr->find(L"Invalid destination format") != std::wstring::npos); |
| 126 | } |
| 127 | |
| 128 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerNotFound) |
| 129 | { |
| 130 | // Create a valid tar file to pipe in, but target a nonexistent container. |
| 131 | CreateTestTarFile(); |
| 132 | |
| 133 | const auto result = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", InvalidContainerName), TarPath); |
| 134 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 135 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 136 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 137 | VERIFY_IS_TRUE(result.Stderr->find(L"not found") != std::wstring::npos); |
| 138 | } |
| 139 | |
| 140 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Success) |
| 141 | { |
| 142 | // Create and start a container with sleep infinity to keep it running. |
| 143 | auto runResult = |
| 144 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 145 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 146 | |
| 147 | // Create a test tar file with a known file inside. |
| 148 | CreateTestTarFile(); |
| 149 | |
| 150 | // Cp the tar into the running container. |
| 151 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath); |
| 152 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 153 | |
| 154 | // Verify the file was copied by running a command inside the container. |
| 155 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 156 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 157 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 158 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 159 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 160 | } |
| 161 | |
| 162 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinFromPipe) |
| 163 | { |
| 164 | // Validate that cp works when stdin is a pipe (no content-length available), |
| 165 | // as opposed to a file where GetFileSize can determine the length upfront. |
| 166 | auto runResult = |
| 167 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 168 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 169 | |
| 170 | CreateTestTarFile(); |
| 171 | |
| 172 | // Open the tar file and relay its content into a pipe on a background thread. |
| 173 | wil::unique_hfile tarFile( |
| 174 | CreateFileW(TarPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); |
| 175 | THROW_LAST_ERROR_IF(!tarFile); |
| 176 | |
| 177 | auto [pipeRead, pipeWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, false); |
| 178 | |
| 179 | std::thread relayThread([&tarFile, &pipeWrite] { |
| 180 | wsl::windows::common::relay::InterruptableRelay(tarFile.get(), pipeWrite.get()); |
| 181 | pipeWrite.reset(); |
| 182 | }); |
| 183 | |
| 184 | // Pass the pipe read end as stdin — wslc cannot determine content-length from a pipe. |
| 185 | const auto cpResult = RunWslc(std::format(L"container cp - {}:/tmp", WslcContainerName), ElevationType::Elevated, pipeRead.get()); |
| 186 | relayThread.join(); |
| 187 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 188 | |
| 189 | // Verify the file was copied. |
| 190 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 191 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 192 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 193 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 194 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 195 | } |
| 196 | |
| 197 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ToStoppedContainer) |
| 198 | { |
| 199 | // Create a stopped container (not started). |
| 200 | auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 201 | createResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 202 | |
| 203 | // Create a test tar file. |
| 204 | CreateTestTarFile(); |
| 205 | |
| 206 | // Attempt to cp into the stopped container — Docker should accept this. |
| 207 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath); |
| 208 | |
| 209 | // Docker's PUT /archive works on stopped containers too. |
| 210 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 211 | } |
| 212 | |
| 213 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlag) |
| 214 | { |
| 215 | // Create and start a container. |
| 216 | auto runResult = |
| 217 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 218 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 219 | |
| 220 | CreateTestTarFile(); |
| 221 | |
| 222 | // Cp with -a flag (archive mode preserves uid/gid). |
| 223 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a - {}:/tmp", WslcContainerName), TarPath); |
| 224 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 225 | |
| 226 | // Verify the file was copied. |
| 227 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 228 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 229 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 230 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 231 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 232 | } |
| 233 | |
| 234 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagLongForm) |
| 235 | { |
| 236 | // Create and start a container. |
| 237 | auto runResult = |
| 238 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 239 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 240 | |
| 241 | CreateTestTarFile(); |
| 242 | |
| 243 | // Cp with --archive flag (long form). |
| 244 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive - {}:/tmp", WslcContainerName), TarPath); |
| 245 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 246 | |
| 247 | // Verify the file was copied. |
| 248 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 249 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 250 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 251 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 252 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 253 | } |
| 254 | |
| 255 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsTrue) |
| 256 | { |
| 257 | // Test -a=true syntax. |
| 258 | auto runResult = |
| 259 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 260 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 261 | |
| 262 | CreateTestTarFile(); |
| 263 | |
| 264 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=true - {}:/tmp", WslcContainerName), TarPath); |
| 265 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 266 | |
| 267 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 268 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 269 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 270 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 271 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 272 | } |
| 273 | |
| 274 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsFalse) |
| 275 | { |
| 276 | // Test -a=false syntax (no archive mode). |
| 277 | auto runResult = |
| 278 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 279 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 280 | |
| 281 | CreateTestTarFile(); |
| 282 | |
| 283 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=false - {}:/tmp", WslcContainerName), TarPath); |
| 284 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 285 | |
| 286 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 287 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 288 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 289 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 290 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 291 | } |
| 292 | |
| 293 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsTrue) |
| 294 | { |
| 295 | // Test --archive=true syntax. |
| 296 | auto runResult = |
| 297 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 298 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 299 | |
| 300 | CreateTestTarFile(); |
| 301 | |
| 302 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=true - {}:/tmp", WslcContainerName), TarPath); |
| 303 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 304 | |
| 305 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 306 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 307 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 308 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 309 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 310 | } |
| 311 | |
| 312 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsFalse) |
| 313 | { |
| 314 | // Test --archive=false syntax (no archive mode). |
| 315 | auto runResult = |
| 316 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 317 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 318 | |
| 319 | CreateTestTarFile(); |
| 320 | |
| 321 | const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=false - {}:/tmp", WslcContainerName), TarPath); |
| 322 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 323 | |
| 324 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); |
| 325 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 326 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 327 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 328 | VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value()); |
| 329 | } |
| 330 | |
| 331 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagInvalidValue) |
| 332 | { |
| 333 | // Test -a=invalid should fail with an error. |
| 334 | CreateTestTarFile(); |
| 335 | |
| 336 | const auto result = RunWslcWithStdinFile(std::format(L"container cp -a=invalid - {}:/tmp", WslcContainerName), TarPath); |
| 337 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 338 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 339 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 340 | VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size()); |
| 341 | } |
| 342 | |
| 343 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileToContainer) |
| 344 | { |
| 345 | // Create and start a container. |
| 346 | auto runResult = |
| 347 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 348 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 349 | |
| 350 | // Create a local file to copy. |
| 351 | auto localFile = std::filesystem::current_path() / L"wslc-cp-local-test.txt"; |
| 352 | auto cleanupLocal = wil::scope_exit([&] { DeleteFileW(localFile.c_str()); }); |
| 353 | |
| 354 | { |
| 355 | wil::unique_hfile file(CreateFileW(localFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); |
| 356 | THROW_LAST_ERROR_IF(!file); |
| 357 | const std::string content = "local-file-content\n"; |
| 358 | DWORD written = 0; |
| 359 | THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast<DWORD>(content.size()), &written, nullptr)); |
| 360 | } |
| 361 | |
| 362 | // Copy local file to container. |
| 363 | const auto cpResult = RunWslc(std::format(L"container cp {} {}:/tmp/", localFile.wstring(), WslcContainerName)); |
| 364 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 365 | |
| 366 | // Verify the file was copied. |
| 367 | const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/wslc-cp-local-test.txt", WslcContainerName)); |
| 368 | VERIFY_IS_TRUE(execResult.ExitCode.has_value()); |
| 369 | VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); |
| 370 | VERIFY_IS_TRUE(execResult.Stdout.has_value()); |
| 371 | VERIFY_ARE_EQUAL(L"local-file-content\n", execResult.Stdout.value()); |
| 372 | } |
| 373 | |
| 374 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileNotFound) |
| 375 | { |
| 376 | // Copying a nonexistent local file should fail. |
| 377 | const auto result = RunWslc(std::format(L"container cp C:\\nonexistent_wslc_test_file.txt {}:/tmp/", WslcContainerName)); |
| 378 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 379 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 380 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 381 | VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size()); |
| 382 | } |
| 383 | |
| 384 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal) |
| 385 | { |
| 386 | // Create and start a container. |
| 387 | auto runResult = |
| 388 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 389 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 390 | |
| 391 | // Create a file inside the container using exec. |
| 392 | auto execResult = |
| 393 | RunWslc(std::format(L"container exec {} sh -c \"echo container-content > /tmp/fromcontainer.txt\"", WslcContainerName)); |
| 394 | execResult.Verify({.ExitCode = 0}); |
| 395 | |
| 396 | // Create a directory to download into. |
| 397 | auto downloadDir = std::filesystem::current_path() / L"wslc-cp-download-test"; |
| 398 | std::filesystem::create_directories(downloadDir); |
| 399 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); |
| 400 | |
| 401 | // Copy from container to local. |
| 402 | const auto cpResult = |
| 403 | RunWslc(std::format(L"container cp {}:/tmp/fromcontainer.txt {}", WslcContainerName, downloadDir.wstring())); |
| 404 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 405 | |
| 406 | // Verify the file was extracted locally. |
| 407 | auto extractedFile = downloadDir / L"fromcontainer.txt"; |
| 408 | VERIFY_IS_TRUE(std::filesystem::exists(extractedFile)); |
| 409 | } |
| 410 | |
| 411 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_TrailingBackslash) |
| 412 | { |
| 413 | // Regression test: trailing backslash on local path should not break tar extraction. |
| 414 | auto runResult = |
| 415 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 416 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 417 | |
| 418 | // Create the file inside the container using exec. |
| 419 | auto execResult = RunWslc(std::format(L"container exec {} sh -c \"echo backslash-test > /tmp/bstest.txt\"", WslcContainerName)); |
| 420 | execResult.Verify({.ExitCode = 0}); |
| 421 | |
| 422 | auto downloadDir = std::filesystem::current_path() / L"wslc-cp-backslash-test"; |
| 423 | std::filesystem::create_directories(downloadDir); |
| 424 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); |
| 425 | |
| 426 | // Copy with explicit trailing backslash in target path. |
| 427 | auto targetWithBackslash = downloadDir.wstring() + L"\\"; |
| 428 | const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/bstest.txt {}", WslcContainerName, targetWithBackslash)); |
| 429 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 430 | |
| 431 | auto extractedFile = downloadDir / L"bstest.txt"; |
| 432 | VERIFY_IS_TRUE(std::filesystem::exists(extractedFile)); |
| 433 | } |
| 434 | |
| 435 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_ThroughSymlink) |
| 436 | { |
| 437 | auto runResult = |
| 438 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 439 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 440 | |
| 441 | auto execResult = RunWslc(std::format(L"container exec {} touch /tmp/symfile.txt", WslcContainerName)); |
| 442 | execResult.Verify({.ExitCode = 0}); |
| 443 | |
| 444 | auto linkDir = std::filesystem::current_path() / L"wslc-cp-symlink-link"; |
| 445 | auto cleanup = wil::scope_exit([&] { |
| 446 | std::error_code ec; |
| 447 | std::filesystem::remove(linkDir, ec); |
| 448 | std::filesystem::remove(L"symfile.txt", ec); |
| 449 | }); |
| 450 | |
| 451 | // Creating a symlink requires Administrator privileges, which the E2E tests already run with. |
| 452 | THROW_LAST_ERROR_IF(!CreateSymbolicLinkW(linkDir.c_str(), std::filesystem::current_path().c_str(), SYMBOLIC_LINK_FLAG_DIRECTORY)); |
| 453 | |
| 454 | // Copy from the container into the symlink. |
| 455 | const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/symfile.txt {}", WslcContainerName, linkDir.wstring())); |
| 456 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 457 | |
| 458 | // The file must land in the real directory (and therefore be visible through the symlink). |
| 459 | VERIFY_IS_TRUE(std::filesystem::exists(std::filesystem::current_path() / L"symfile.txt")); |
| 460 | VERIFY_IS_TRUE(std::filesystem::exists(linkDir / L"symfile.txt")); |
| 461 | } |
| 462 | |
| 463 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_FileDestination) |
| 464 | { |
| 465 | // When the local target doesn't end with a separator and isn't an existing directory, |
| 466 | // it should be treated as a file destination (matching docker cp semantics). |
| 467 | auto runResult = |
| 468 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 469 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 470 | |
| 471 | // Create the file inside the container using exec. |
| 472 | auto execResult = RunWslc(std::format(L"container exec {} sh -c \"echo file-dest-test > /tmp/srcfile.txt\"", WslcContainerName)); |
| 473 | execResult.Verify({.ExitCode = 0}); |
| 474 | |
| 475 | auto targetFile = std::filesystem::current_path() / L"wslc-cp-file-dest-test" / L"renamed.txt"; |
| 476 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(targetFile.parent_path()); }); |
| 477 | |
| 478 | // Copy from container to a specific file path (not a directory). |
| 479 | const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/srcfile.txt {}", WslcContainerName, targetFile.wstring())); |
| 480 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 481 | |
| 482 | // The file should exist at the exact target path, not inside a directory named "renamed.txt". |
| 483 | VERIFY_IS_TRUE(std::filesystem::exists(targetFile)); |
| 484 | VERIFY_IS_TRUE(std::filesystem::is_regular_file(targetFile)); |
| 485 | } |
| 486 | |
| 487 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentPath) |
| 488 | { |
| 489 | // Regression test: DownloadArchive used to hang on 404 because the HTTP/1.1 keep-alive |
| 490 | // socket never closed. The fix shuts down the socket so the read sees EOF immediately. |
| 491 | auto runResult = |
| 492 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 493 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 494 | |
| 495 | auto downloadDir = std::filesystem::current_path() / L"wslc-cp-notfound-test"; |
| 496 | std::filesystem::create_directories(downloadDir); |
| 497 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); |
| 498 | |
| 499 | const auto cpResult = RunWslc(std::format(L"container cp {}:/nonexistent/file.txt {}", WslcContainerName, downloadDir.wstring())); |
| 500 | VERIFY_IS_TRUE(cpResult.ExitCode.has_value()); |
| 501 | VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value()); |
| 502 | VERIFY_IS_TRUE(cpResult.Stderr.has_value()); |
| 503 | VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size()); |
| 504 | } |
| 505 | |
| 506 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentDir) |
| 507 | { |
| 508 | // Regression test: DownloadArchive 404 for a nonexistent directory path. |
| 509 | auto runResult = |
| 510 | RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 511 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 512 | |
| 513 | auto downloadDir = std::filesystem::current_path() / L"wslc-cp-notfound-dir-test"; |
| 514 | std::filesystem::create_directories(downloadDir); |
| 515 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); |
| 516 | |
| 517 | const auto cpResult = RunWslc(std::format(L"container cp {}:/no/such/directory/ {}", WslcContainerName, downloadDir.wstring())); |
| 518 | VERIFY_IS_TRUE(cpResult.ExitCode.has_value()); |
| 519 | VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value()); |
| 520 | VERIFY_IS_TRUE(cpResult.Stderr.has_value()); |
| 521 | VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size()); |
| 522 | } |
| 523 | |
| 524 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Download_NonexistentContainer) |
| 525 | { |
| 526 | // Regression test: DownloadArchive error path when the container itself doesn't exist. |
| 527 | auto downloadDir = std::filesystem::current_path() / L"wslc-cp-no-container-test"; |
| 528 | std::filesystem::create_directories(downloadDir); |
| 529 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); |
| 530 | |
| 531 | const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/file.txt {}", InvalidContainerName, downloadDir.wstring())); |
| 532 | VERIFY_IS_TRUE(cpResult.ExitCode.has_value()); |
| 533 | VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value()); |
| 534 | VERIFY_IS_TRUE(cpResult.Stderr.has_value()); |
| 535 | VERIFY_IS_TRUE(cpResult.Stderr->find(L"not found") != std::wstring::npos); |
| 536 | } |
| 537 | |
| 538 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_FromStoppedContainer) |
| 539 | { |
| 540 | // Create a container, put a file in it, stop it, then copy out. |
| 541 | auto runResult = RunWslc(std::format( |
| 542 | L"container run --name {} {} sh -c \"echo stopped-content > /tmp/stopped.txt\"", WslcContainerName, DebianImage.NameAndTag())); |
| 543 | runResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 544 | |
| 545 | // Container has exited (ran a one-shot command). Copy from the stopped container. |
| 546 | auto downloadDir = std::filesystem::current_path() / L"wslc-cp-stopped-test"; |
| 547 | std::filesystem::create_directories(downloadDir); |
| 548 | auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); |
| 549 | |
| 550 | const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/stopped.txt {}", WslcContainerName, downloadDir.wstring())); |
| 551 | cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 552 | |
| 553 | auto extractedFile = downloadDir / L"stopped.txt"; |
| 554 | VERIFY_IS_TRUE(std::filesystem::exists(extractedFile)); |
| 555 | } |
| 556 | |
| 557 | WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidDirection_LocalToLocal) |
| 558 | { |
| 559 | // local → local is not a valid copy direction. |
| 560 | const auto result = RunWslc(L"container cp C:\\temp\\somefile.txt C:\\temp\\dest\\"); |
| 561 | VERIFY_IS_TRUE(result.ExitCode.has_value()); |
| 562 | VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); |
| 563 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 564 | VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size()); |
| 565 | } |
| 566 | |
| 567 | private: |
| 568 | const std::wstring WslcContainerName = L"wslc-test-container-cp"; |
| 569 | const std::wstring InvalidContainerName = L"wslc-nonexistent-container-for-cp"; |
| 570 | const TestImage& DebianImage = DebianTestImage(); |
| 571 | |
| 572 | std::filesystem::path TarPath{}; |
| 573 | |
| 574 | // Creates a tar file containing a single text file using tar.exe. |
| 575 | void CreateTestTarFile() |
| 576 | { |
| 577 | // Create a directory with a test file to archive. |
| 578 | auto tarSrcDir = std::filesystem::current_path() / L"wslc-cp-tar-src"; |
| 579 | std::filesystem::create_directories(tarSrcDir); |
| 580 | auto cleanupSrcDir = wil::scope_exit([&] { std::filesystem::remove_all(tarSrcDir); }); |
| 581 | |
| 582 | auto testFile = tarSrcDir / L"testfile.txt"; |
| 583 | { |
| 584 | wil::unique_hfile file(CreateFileW(testFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); |
| 585 | THROW_LAST_ERROR_IF(!file); |
| 586 | const std::string content = "wslc-cp-test-content\n"; |
| 587 | DWORD written = 0; |
| 588 | THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast<DWORD>(content.size()), &written, nullptr)); |
| 589 | } |
| 590 | |
| 591 | // Use tar.exe to create the archive. |
| 592 | auto tarCmd = std::format(L"tar.exe -cf \"{}\" -C \"{}\" testfile.txt", TarPath.wstring(), tarSrcDir.wstring()); |
| 593 | STARTUPINFOW si{sizeof(si)}; |
| 594 | PROCESS_INFORMATION pi{}; |
| 595 | THROW_LAST_ERROR_IF(!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)); |
| 596 | wil::unique_handle tarProcess(pi.hProcess); |
| 597 | wil::unique_handle tarThread(pi.hThread); |
| 598 | WaitForSingleObject(tarProcess.get(), INFINITE); |
| 599 | |
| 600 | DWORD exitCode = 0; |
| 601 | GetExitCodeProcess(tarProcess.get(), &exitCode); |
| 602 | THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode); |
| 603 | } |
| 604 | }; |
| 605 | } // namespace WSLCE2ETests |