| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCE2EContainerListTests.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains end-to-end tests for WSLC. |
| 12 | --*/ |
| 13 | |
| 14 | #include "precomp.h" |
| 15 | #include "windows/Common.h" |
| 16 | #include "ContainerModel.h" |
| 17 | #include "WSLCExecutor.h" |
| 18 | #include "WSLCE2EHelpers.h" |
| 19 | #include "TestImageRegistry.h" |
| 20 | |
| 21 | namespace WSLCE2ETests { |
| 22 | using namespace wsl::shared; |
| 23 | |
| 24 | using namespace wsl::windows::wslc::models; |
| 25 | using namespace wsl::windows::common::string; |
| 26 | |
| 27 | class WSLCE2EContainerListTests |
| 28 | { |
| 29 | WSLC_TEST_CLASS(WSLCE2EContainerListTests) |
| 30 | |
| 31 | TEST_CLASS_SETUP(ClassSetup) |
| 32 | { |
| 33 | TestImageRegistry::Instance().EnsureLoaded(DebianImage); |
| 34 | return true; |
| 35 | } |
| 36 | |
| 37 | TEST_CLASS_CLEANUP(ClassCleanup) |
| 38 | { |
| 39 | EnsureContainerDoesNotExist(WslcContainerName); |
| 40 | EnsureContainerDoesNotExist(WslcContainerName2); |
| 41 | return true; |
| 42 | } |
| 43 | |
| 44 | TEST_METHOD_SETUP(TestMethodSetup) |
| 45 | { |
| 46 | EnsureContainerDoesNotExist(WslcContainerName); |
| 47 | EnsureContainerDoesNotExist(WslcContainerName2); |
| 48 | return true; |
| 49 | } |
| 50 | |
| 51 | WSLC_TEST_METHOD(WSLCE2E_Container_List_HelpCommand) |
| 52 | { |
| 53 | auto result = RunWslc(L"container list --help"); |
| 54 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 55 | VERIFY_IS_FALSE(result.Stdout.value().empty()); |
| 56 | } |
| 57 | |
| 58 | WSLC_TEST_METHOD(WSLCE2E_Container_List_AllOption) |
| 59 | { |
| 60 | VerifyContainerIsNotListed(WslcContainerName); |
| 61 | |
| 62 | // Create a container |
| 63 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 64 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 65 | auto containerId = result.GetStdoutOneLine(); |
| 66 | VERIFY_IS_FALSE(containerId.empty()); |
| 67 | |
| 68 | // Find container in list output |
| 69 | result = RunWslc(L"container list --no-trunc --all"); |
| 70 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 71 | auto outputLines = result.GetStdoutLines(); |
| 72 | std::optional<std::wstring> foundContainerLine{}; |
| 73 | for (const auto& line : outputLines) |
| 74 | { |
| 75 | if (line.find(containerId) != std::wstring::npos) |
| 76 | { |
| 77 | foundContainerLine = line; |
| 78 | break; |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // Verify we found the container in the list output |
| 83 | VERIFY_IS_TRUE(foundContainerLine.has_value()); |
| 84 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, foundContainerLine->find(L"Created")); |
| 85 | } |
| 86 | |
| 87 | WSLC_TEST_METHOD(WSLCE2E_Container_List_NoOptions_RunningContainers) |
| 88 | { |
| 89 | VerifyContainerIsNotListed(WslcContainerName); |
| 90 | |
| 91 | // Run a container in the background |
| 92 | auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); |
| 93 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 94 | auto containerId = TruncateId(result.GetStdoutOneLine()); |
| 95 | VERIFY_IS_FALSE(containerId.empty()); |
| 96 | |
| 97 | // Find container in list output with no options |
| 98 | result = RunWslc(L"container list"); |
| 99 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 100 | auto outputLines = result.GetStdoutLines(); |
| 101 | std::optional<std::wstring> foundContainerLine{}; |
| 102 | for (const auto& line : outputLines) |
| 103 | { |
| 104 | if (line.find(containerId) != std::wstring::npos) |
| 105 | { |
| 106 | foundContainerLine = line; |
| 107 | break; |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // Verify we found the container in the list output |
| 112 | VERIFY_IS_TRUE(foundContainerLine.has_value()); |
| 113 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, foundContainerLine->find(L"Up ")); |
| 114 | } |
| 115 | |
| 116 | WSLC_TEST_METHOD(WSLCE2E_Container_List_NoOptions_ExcludesCreatedContainers) |
| 117 | { |
| 118 | VerifyContainerIsNotListed(WslcContainerName); |
| 119 | |
| 120 | // Create (but do not start) a container. |
| 121 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 122 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 123 | const auto containerId = TruncateId(result.GetStdoutOneLine()); |
| 124 | VERIFY_IS_FALSE(containerId.empty()); |
| 125 | |
| 126 | // Default list should only show running containers. |
| 127 | result = RunWslc(L"container list"); |
| 128 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 129 | bool isListed = false; |
| 130 | for (const auto& line : result.GetStdoutLines()) |
| 131 | { |
| 132 | if (line.find(containerId) != std::wstring::npos) |
| 133 | { |
| 134 | isListed = true; |
| 135 | break; |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | VERIFY_IS_FALSE(isListed); |
| 140 | } |
| 141 | |
| 142 | // The table layout must match `docker container list` so users can rely on column order. |
| 143 | WSLC_TEST_METHOD(WSLCE2E_Container_List_TableFormat_MatchesDockerColumnOrder) |
| 144 | { |
| 145 | const auto result = RunWslc(L"container list --all"); |
| 146 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 147 | |
| 148 | const auto outputLines = result.GetStdoutLines(); |
| 149 | VERIFY_IS_FALSE(outputLines.empty()); |
| 150 | |
| 151 | const auto& header = outputLines.front(); |
| 152 | size_t position = 0; |
| 153 | for (const auto& column : |
| 154 | {Localization::WSLCCLI_TableHeaderContainerId(), |
| 155 | Localization::WSLCCLI_TableHeaderImage(), |
| 156 | Localization::WSLCCLI_TableHeaderCommand(), |
| 157 | Localization::WSLCCLI_TableHeaderCreated(), |
| 158 | Localization::WSLCCLI_TableHeaderStatus(), |
| 159 | Localization::WSLCCLI_TableHeaderPorts(), |
| 160 | Localization::WSLCCLI_TableHeaderNames()}) |
| 161 | { |
| 162 | const auto found = header.find(column, position); |
| 163 | VERIFY_ARE_NOT_EQUAL( |
| 164 | std::wstring::npos, found, std::format(L"Column '{}' missing or out of order in '{}'", column, header).c_str()); |
| 165 | position = found + column.size(); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | WSLC_TEST_METHOD(WSLCE2E_Container_List_QuietOption_OutputsIdsOnly) |
| 170 | { |
| 171 | VerifyContainerIsNotListed(WslcContainerName); |
| 172 | |
| 173 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 174 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 175 | const auto containerId = result.GetStdoutOneLine(); |
| 176 | VERIFY_IS_FALSE(containerId.empty()); |
| 177 | |
| 178 | result = RunWslc(L"container list --all --quiet"); |
| 179 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 180 | |
| 181 | const auto truncatedId = wsl::shared::string::MultiByteToWide(TruncateId(WideToMultiByte(containerId))); |
| 182 | VERIFY_ARE_EQUAL(12u, truncatedId.size()); |
| 183 | VERIFY_IS_TRUE(result.StdoutContainsLine(truncatedId)); |
| 184 | |
| 185 | result = RunWslc(L"container list --all --quiet --no-trunc"); |
| 186 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 187 | VERIFY_IS_TRUE(result.StdoutContainsLine(containerId)); |
| 188 | } |
| 189 | |
| 190 | WSLC_TEST_METHOD(WSLCE2E_Container_List_InvalidFormatOption) |
| 191 | { |
| 192 | const auto result = RunWslc(L"container list --format invalid"); |
| 193 | result.Verify({.Stdout = L"", .ExitCode = 1}); |
| 194 | VERIFY_IS_TRUE(result.StderrContainsSubstring( |
| 195 | L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.")); |
| 196 | } |
| 197 | |
| 198 | WSLC_TEST_METHOD(WSLCE2E_Container_List_JsonFormat) |
| 199 | { |
| 200 | VerifyContainerIsNotListed(WslcContainerName); |
| 201 | |
| 202 | // Create a container |
| 203 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 204 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 205 | const auto containerId = result.GetStdoutOneLine(); |
| 206 | VERIFY_IS_FALSE(containerId.empty()); |
| 207 | |
| 208 | // List containers with json format |
| 209 | result = RunWslc(L"container list --all --format json --no-trunc"); |
| 210 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 211 | // Parse json and verify we got the expected container information back |
| 212 | auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(result); |
| 213 | VERIFY_IS_GREATER_THAN_OR_EQUAL(containers.size(), 1U); |
| 214 | VERIFY_ARE_EQUAL(containers.size(), result.GetStdoutLines().size()); |
| 215 | |
| 216 | auto findContainer = [](const std::vector<ContainerOutputInformation>& list, const std::wstring& id) { |
| 217 | return std::ranges::any_of(list, [&](const auto& c) { return wsl::shared::string::MultiByteToWide(c.ID) == id; }); |
| 218 | }; |
| 219 | |
| 220 | VERIFY_IS_TRUE(findContainer(containers, containerId)); |
| 221 | |
| 222 | // Create another container |
| 223 | result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag())); |
| 224 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 225 | const auto containerId2 = result.GetStdoutOneLine(); |
| 226 | VERIFY_IS_FALSE(containerId2.empty()); |
| 227 | |
| 228 | // List containers with json format again |
| 229 | result = RunWslc(L"container list --all --format json --no-trunc"); |
| 230 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 231 | // Parse json and verify we got both containers back |
| 232 | containers = ParseNdjsonOutputAs<ContainerOutputInformation>(result); |
| 233 | VERIFY_IS_GREATER_THAN_OR_EQUAL(containers.size(), 2U); |
| 234 | |
| 235 | VERIFY_IS_TRUE(findContainer(containers, containerId)); |
| 236 | VERIFY_IS_TRUE(findContainer(containers, containerId2)); |
| 237 | } |
| 238 | |
| 239 | WSLC_TEST_METHOD(WSLCE2E_Container_List_JsonFormat_MatchesDockerShape) |
| 240 | { |
| 241 | const std::set<std::string> expectedKeys = { |
| 242 | "Command", |
| 243 | "CreatedAt", |
| 244 | "HealthStatus", |
| 245 | "ID", |
| 246 | "Image", |
| 247 | "Labels", |
| 248 | "LocalVolumes", |
| 249 | "Mounts", |
| 250 | "Names", |
| 251 | "Networks", |
| 252 | "Platform", |
| 253 | "Ports", |
| 254 | "RunningFor", |
| 255 | "Size", |
| 256 | "State", |
| 257 | "Status"}; |
| 258 | |
| 259 | VerifyContainerIsNotListed(WslcContainerName); |
| 260 | |
| 261 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 262 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 263 | |
| 264 | result = RunWslc(L"container list --all --format json"); |
| 265 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 266 | |
| 267 | const auto entries = ParseNdjsonOutput(result); |
| 268 | VERIFY_IS_GREATER_THAN_OR_EQUAL(entries.size(), 1u); |
| 269 | |
| 270 | for (const auto& entry : entries) |
| 271 | { |
| 272 | std::set<std::string> keys; |
| 273 | for (const auto& [key, value] : entry.items()) |
| 274 | { |
| 275 | keys.insert(key); |
| 276 | |
| 277 | if (key == "Platform") |
| 278 | { |
| 279 | VERIFY_IS_TRUE(value.is_object()); |
| 280 | } |
| 281 | else |
| 282 | { |
| 283 | VERIFY_IS_TRUE( |
| 284 | value.is_string(), wsl::shared::string::MultiByteToWide(std::format("'{}' must be a string", key)).c_str()); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | VERIFY_ARE_EQUAL(expectedKeys, keys, L"json output must contain exactly docker's container fields"); |
| 289 | |
| 290 | VERIFY_ARE_EQUAL(12u, entry["ID"].get<std::string>().size()); |
| 291 | VERIFY_ARE_NOT_EQUAL(std::string{}, entry["Names"].get<std::string>()); |
| 292 | VERIFY_ARE_NOT_EQUAL(std::string{}, entry["State"].get<std::string>()); |
| 293 | VERIFY_ARE_EQUAL(std::string{"linux"}, entry["Platform"]["os"].get<std::string>()); |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidKey) |
| 298 | { |
| 299 | // Filter keys are validated by the Docker daemon, which rejects unknown keys. |
| 300 | const auto result = RunWslc(L"container list --filter color=blue"); |
| 301 | VERIFY_ARE_EQUAL(1, result.ExitCode); |
| 302 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 303 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter 'color'")); |
| 304 | } |
| 305 | |
| 306 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_MalformedValue) |
| 307 | { |
| 308 | // Filter values must be of the form key=value; bare keys are rejected by the CLI. |
| 309 | const auto result = RunWslc(L"container list --filter status"); |
| 310 | result.Verify({.Stdout = L"", .ExitCode = 1}); |
| 311 | VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"status"))); |
| 312 | } |
| 313 | |
| 314 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidStatusValue) |
| 315 | { |
| 316 | // Status values are validated by the Docker daemon, which rejects unknown values. |
| 317 | const auto result = RunWslc(L"container list --filter status=bogus"); |
| 318 | VERIFY_ARE_EQUAL(1, result.ExitCode); |
| 319 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 320 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter 'status=bogus'")); |
| 321 | } |
| 322 | |
| 323 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Name) |
| 324 | { |
| 325 | VerifyContainerIsNotListed(WslcContainerName); |
| 326 | VerifyContainerIsNotListed(WslcContainerName2); |
| 327 | |
| 328 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 329 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 330 | const auto containerId = result.GetStdoutOneLine(); |
| 331 | |
| 332 | result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag())); |
| 333 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 334 | const auto containerId2 = result.GetStdoutOneLine(); |
| 335 | |
| 336 | result = RunWslc(std::format(L"container list --all --format json --filter name={}", WslcContainerName2)); |
| 337 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 338 | |
| 339 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(result); |
| 340 | VERIFY_ARE_EQUAL(1U, containers.size()); |
| 341 | VERIFY_ARE_EQUAL(WideToMultiByte(WslcContainerName2), std::string(containers[0].Names)); |
| 342 | } |
| 343 | |
| 344 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Status) |
| 345 | { |
| 346 | VerifyContainerIsNotListed(WslcContainerName); |
| 347 | VerifyContainerIsNotListed(WslcContainerName2); |
| 348 | |
| 349 | // Created (never started) container. |
| 350 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 351 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 352 | |
| 353 | // Running container. |
| 354 | result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName2, DebianImage.NameAndTag())); |
| 355 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 356 | |
| 357 | auto listNames = [&](const std::wstring& filterArgs) { |
| 358 | auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs)); |
| 359 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 360 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(r); |
| 361 | std::set<std::string> names; |
| 362 | for (const auto& c : containers) |
| 363 | { |
| 364 | names.insert(c.Names); |
| 365 | } |
| 366 | return names; |
| 367 | }; |
| 368 | |
| 369 | // status=created |
| 370 | { |
| 371 | const auto names = listNames(L"--filter status=created"); |
| 372 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 373 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 374 | } |
| 375 | |
| 376 | // status=running |
| 377 | { |
| 378 | const auto names = listNames(L"--filter status=running"); |
| 379 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName))); |
| 380 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 381 | } |
| 382 | |
| 383 | // Multiple --filter status= values are OR'd. |
| 384 | { |
| 385 | const auto names = listNames(L"--filter status=created --filter status=running"); |
| 386 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 387 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Label) |
| 392 | { |
| 393 | VerifyContainerIsNotListed(WslcContainerName); |
| 394 | VerifyContainerIsNotListed(WslcContainerName2); |
| 395 | |
| 396 | // First container has both labels; second has only one of them. |
| 397 | auto result = RunWslc(std::format( |
| 398 | L"container create --name {} --label filter.test=yes --label filter.role=primary {}", WslcContainerName, DebianImage.NameAndTag())); |
| 399 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 400 | |
| 401 | result = RunWslc(std::format(L"container create --name {} --label filter.test=yes {}", WslcContainerName2, DebianImage.NameAndTag())); |
| 402 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 403 | |
| 404 | auto listNames = [&](const std::wstring& filterArgs) { |
| 405 | auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs)); |
| 406 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 407 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(r); |
| 408 | std::set<std::string> names; |
| 409 | for (const auto& c : containers) |
| 410 | { |
| 411 | names.insert(c.Names); |
| 412 | } |
| 413 | return names; |
| 414 | }; |
| 415 | |
| 416 | // label=<key> matches both since both have filter.test set (any value). |
| 417 | { |
| 418 | const auto names = listNames(L"--filter label=filter.test"); |
| 419 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 420 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 421 | } |
| 422 | |
| 423 | // label=<key>=<value> matches only the first. |
| 424 | { |
| 425 | const auto names = listNames(L"--filter label=filter.role=primary"); |
| 426 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 427 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 428 | } |
| 429 | |
| 430 | // Multiple --filter label= entries are AND'd. |
| 431 | { |
| 432 | const auto names = listNames(L"--filter label=filter.test --filter label=filter.role=primary"); |
| 433 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 434 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 435 | } |
| 436 | } |
| 437 | |
| 438 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Id) |
| 439 | { |
| 440 | VerifyContainerIsNotListed(WslcContainerName); |
| 441 | VerifyContainerIsNotListed(WslcContainerName2); |
| 442 | |
| 443 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 444 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 445 | const auto containerId = result.GetStdoutOneLine(); |
| 446 | |
| 447 | result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag())); |
| 448 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 449 | |
| 450 | // Filter by id (full id) should return exactly one container. |
| 451 | result = RunWslc(std::format(L"container list --all --format json --no-trunc --filter id={}", containerId)); |
| 452 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 453 | |
| 454 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(result); |
| 455 | VERIFY_ARE_EQUAL(1U, containers.size()); |
| 456 | VERIFY_ARE_EQUAL(WideToMultiByte(containerId), std::string(containers[0].ID)); |
| 457 | } |
| 458 | |
| 459 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Exited) |
| 460 | { |
| 461 | VerifyContainerIsNotListed(WslcContainerName); |
| 462 | VerifyContainerIsNotListed(WslcContainerName2); |
| 463 | |
| 464 | // First container exits with code 0. |
| 465 | auto result = RunWslc(std::format(L"container run --name {} {} true", WslcContainerName, DebianImage.NameAndTag())); |
| 466 | result.Verify({.ExitCode = 0}); |
| 467 | |
| 468 | // Second container exits with non-zero code. |
| 469 | result = RunWslc(std::format(L"container run --name {} {} sh -c \"exit 7\"", WslcContainerName2, DebianImage.NameAndTag())); |
| 470 | // run with non-zero container exit code is allowed; we don't assert exit here. |
| 471 | |
| 472 | auto listNames = [&](const std::wstring& filterArgs) { |
| 473 | auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs)); |
| 474 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 475 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(r); |
| 476 | std::set<std::string> names; |
| 477 | for (const auto& c : containers) |
| 478 | { |
| 479 | names.insert(c.Names); |
| 480 | } |
| 481 | return names; |
| 482 | }; |
| 483 | |
| 484 | // exited=0 should match the first container only. |
| 485 | { |
| 486 | const auto names = listNames(L"--filter exited=0"); |
| 487 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 488 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 489 | } |
| 490 | |
| 491 | // exited=7 should match the second container only. |
| 492 | { |
| 493 | const auto names = listNames(L"--filter exited=7"); |
| 494 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName))); |
| 495 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 496 | } |
| 497 | } |
| 498 | |
| 499 | WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_BeforeSince) |
| 500 | { |
| 501 | VerifyContainerIsNotListed(WslcContainerName); |
| 502 | VerifyContainerIsNotListed(WslcContainerName2); |
| 503 | |
| 504 | // Create the first container, then the second so that ordering is deterministic. |
| 505 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 506 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 507 | |
| 508 | result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag())); |
| 509 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 510 | |
| 511 | auto listNames = [&](const std::wstring& filterArgs) { |
| 512 | auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs)); |
| 513 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 514 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(r); |
| 515 | std::set<std::string> names; |
| 516 | for (const auto& c : containers) |
| 517 | { |
| 518 | names.insert(c.Names); |
| 519 | } |
| 520 | return names; |
| 521 | }; |
| 522 | |
| 523 | // before=<container2 name> -> only container1 (created earlier) is visible. |
| 524 | { |
| 525 | const auto names = listNames(std::format(L"--filter before={}", WslcContainerName2)); |
| 526 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName))); |
| 527 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 528 | } |
| 529 | |
| 530 | // since=<container1 name> -> only container2 (created later) is visible. |
| 531 | { |
| 532 | const auto names = listNames(std::format(L"--filter since={}", WslcContainerName)); |
| 533 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName))); |
| 534 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2))); |
| 535 | } |
| 536 | } |
| 537 | |
| 538 | WSLC_TEST_METHOD(WSLCE2E_Container_List_LastAndLatest) |
| 539 | { |
| 540 | VerifyContainerIsNotListed(WslcContainerName); |
| 541 | VerifyContainerIsNotListed(WslcContainerName2); |
| 542 | |
| 543 | // Create container1 then container2 (deterministic order). |
| 544 | auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); |
| 545 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 546 | |
| 547 | result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag())); |
| 548 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 549 | |
| 550 | // --latest is shorthand for --last 1; should return only container2 (most recent). |
| 551 | // Implies --all so created-but-not-running containers are visible. |
| 552 | { |
| 553 | result = RunWslc(L"container list --latest --format json"); |
| 554 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 555 | |
| 556 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(result); |
| 557 | VERIFY_ARE_EQUAL(1U, containers.size()); |
| 558 | VERIFY_ARE_EQUAL(WideToMultiByte(WslcContainerName2), std::string(containers[0].Names)); |
| 559 | } |
| 560 | |
| 561 | // --last 2 should cap output at 2 containers. |
| 562 | { |
| 563 | result = RunWslc(L"container list --last 2 --format json"); |
| 564 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 565 | |
| 566 | const auto containers = ParseNdjsonOutputAs<ContainerOutputInformation>(result); |
| 567 | VERIFY_IS_TRUE(containers.size() <= 2u); |
| 568 | } |
| 569 | |
| 570 | // --last and --latest are mutually exclusive. |
| 571 | { |
| 572 | result = RunWslc(L"container list --last 1 --latest"); |
| 573 | VERIFY_ARE_EQUAL(1, result.ExitCode); |
| 574 | } |
| 575 | |
| 576 | // --last requires an integer. |
| 577 | { |
| 578 | result = RunWslc(L"container list --last bogus"); |
| 579 | VERIFY_ARE_EQUAL(1, result.ExitCode); |
| 580 | } |
| 581 | } |
| 582 | |
| 583 | private: |
| 584 | const std::wstring WslcContainerName = L"wslc-test-container"; |
| 585 | const std::wstring WslcContainerName2 = L"wslc-test-container-2"; |
| 586 | const TestImage& DebianImage = DebianTestImage(); |
| 587 | }; |
| 588 | } // namespace WSLCE2ETests |