| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCE2EImageListTests.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 "ImageModel.h" |
| 17 | #include "WSLCExecutor.h" |
| 18 | #include "WSLCE2EHelpers.h" |
| 19 | #include "TestImageRegistry.h" |
| 20 | |
| 21 | namespace WSLCE2ETests { |
| 22 | using namespace wsl::shared; |
| 23 | using namespace wsl::windows::common::string; |
| 24 | using namespace wsl::windows::wslc::models; |
| 25 | |
| 26 | class WSLCE2EImageListTests |
| 27 | { |
| 28 | WSLC_TEST_CLASS(WSLCE2EImageListTests) |
| 29 | |
| 30 | TEST_CLASS_SETUP(ClassSetup) |
| 31 | { |
| 32 | TestImageRegistry::Instance().EnsureLoaded(DebianImage); |
| 33 | TestImageRegistry::Instance().EnsureLoaded(AlpineImage); |
| 34 | return true; |
| 35 | } |
| 36 | |
| 37 | TEST_CLASS_CLEANUP(ClassCleanup) |
| 38 | { |
| 39 | return true; |
| 40 | } |
| 41 | |
| 42 | WSLC_TEST_METHOD(WSLCE2E_Image_List_HelpCommand) |
| 43 | { |
| 44 | const auto result = RunWslc(L"image list --help"); |
| 45 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 46 | VERIFY_IS_FALSE(result.Stdout.value().empty()); |
| 47 | } |
| 48 | |
| 49 | WSLC_TEST_METHOD(WSLCE2E_Image_List_DisplayLoadedImage) |
| 50 | { |
| 51 | const auto result = RunWslc(L"image list"); |
| 52 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 53 | for (const auto& line : result.GetStdoutLines()) |
| 54 | { |
| 55 | if (line.find(DebianImage.Name) != std::wstring::npos && line.find(DebianImage.Tag) != std::wstring::npos) |
| 56 | { |
| 57 | return; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | VERIFY_FAIL(L"Failed to find the loaded image in the output"); |
| 62 | } |
| 63 | |
| 64 | WSLC_TEST_METHOD(WSLCE2E_Image_List_QuietOption_OutputsIdsOnly) |
| 65 | { |
| 66 | // Get the expected image ID from JSON output. --no-trunc is required because json output |
| 67 | // truncates the id by default. |
| 68 | auto jsonResult = RunWslc(L"image list --format json --no-trunc"); |
| 69 | jsonResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 70 | const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(jsonResult); |
| 71 | |
| 72 | std::string debianId; |
| 73 | for (const auto& image : images) |
| 74 | { |
| 75 | if (image.Repository == wsl::shared::string::WideToMultiByte(DebianImage.Name) && |
| 76 | image.Tag == wsl::shared::string::WideToMultiByte(DebianImage.Tag)) |
| 77 | { |
| 78 | debianId = image.ID; |
| 79 | break; |
| 80 | } |
| 81 | } |
| 82 | VERIFY_ARE_NOT_EQUAL(std::string{}, debianId, L"Debian image was not present in `image list --format json` output"); |
| 83 | |
| 84 | const auto truncatedDebianId = wsl::shared::string::MultiByteToWide(TruncateId(debianId, true)); |
| 85 | const auto fullDebianIdW = wsl::shared::string::MultiByteToWide(debianId); |
| 86 | |
| 87 | // Default --quiet truncates to 12 chars. |
| 88 | auto truncResult = RunWslc(L"image list --quiet"); |
| 89 | truncResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 90 | |
| 91 | bool truncatedFound = false; |
| 92 | for (const auto& line : truncResult.GetStdoutLines()) |
| 93 | { |
| 94 | if (line == truncatedDebianId) |
| 95 | { |
| 96 | truncatedFound = true; |
| 97 | break; |
| 98 | } |
| 99 | } |
| 100 | VERIFY_IS_TRUE(truncatedFound, L"Truncated image ID not found in --quiet output"); |
| 101 | |
| 102 | // --quiet --no-trunc shows the full id with sha256: prefix. |
| 103 | auto noTruncResult = RunWslc(L"image list --quiet --no-trunc"); |
| 104 | noTruncResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 105 | |
| 106 | bool fullFound = false; |
| 107 | for (const auto& line : noTruncResult.GetStdoutLines()) |
| 108 | { |
| 109 | if (line == fullDebianIdW) |
| 110 | { |
| 111 | fullFound = true; |
| 112 | break; |
| 113 | } |
| 114 | } |
| 115 | VERIFY_IS_TRUE(fullFound, L"Full image ID not found in --quiet --no-trunc output"); |
| 116 | } |
| 117 | |
| 118 | WSLC_TEST_METHOD(WSLCE2E_Image_List_InvalidFormatOption) |
| 119 | { |
| 120 | const auto result = RunWslc(L"image list --format invalid"); |
| 121 | result.Verify({.Stdout = L"", .ExitCode = 1}); |
| 122 | VERIFY_IS_TRUE(result.StderrContainsSubstring( |
| 123 | L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.")); |
| 124 | } |
| 125 | |
| 126 | WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat) |
| 127 | { |
| 128 | const auto result = RunWslc(L"image list --format json"); |
| 129 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 130 | |
| 131 | const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(result); |
| 132 | |
| 133 | VERIFY_IS_GREATER_THAN_OR_EQUAL(images.size(), 2u); |
| 134 | |
| 135 | std::vector<std::wstring> imageNames; |
| 136 | for (const auto& image : images) |
| 137 | { |
| 138 | auto nameAndTag = std::format( |
| 139 | L"{}:{}", wsl::shared::string::MultiByteToWide(image.Repository), wsl::shared::string::MultiByteToWide(image.Tag)); |
| 140 | imageNames.push_back(nameAndTag); |
| 141 | } |
| 142 | |
| 143 | VERIFY_ARE_NOT_EQUAL(imageNames.end(), std::find(imageNames.begin(), imageNames.end(), DebianImage.NameAndTag())); |
| 144 | VERIFY_ARE_NOT_EQUAL(imageNames.end(), std::find(imageNames.begin(), imageNames.end(), AlpineImage.NameAndTag())); |
| 145 | } |
| 146 | |
| 147 | WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat_MatchesDockerShape) |
| 148 | { |
| 149 | const std::set<std::string> expectedKeys = { |
| 150 | "Containers", "CreatedAt", "CreatedSince", "Digest", "ID", "Repository", "SharedSize", "Size", "Tag", "UniqueSize"}; |
| 151 | |
| 152 | const auto result = RunWslc(L"image list --format json"); |
| 153 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 154 | |
| 155 | const auto entries = ParseNdjsonOutput(result); |
| 156 | VERIFY_IS_GREATER_THAN_OR_EQUAL(entries.size(), 2u); |
| 157 | |
| 158 | for (const auto& entry : entries) |
| 159 | { |
| 160 | std::set<std::string> keys; |
| 161 | for (const auto& [key, value] : entry.items()) |
| 162 | { |
| 163 | keys.insert(key); |
| 164 | VERIFY_IS_TRUE(value.is_string(), wsl::shared::string::MultiByteToWide(std::format("'{}' must be a string", key)).c_str()); |
| 165 | } |
| 166 | |
| 167 | VERIFY_ARE_EQUAL(expectedKeys, keys, L"json output must contain exactly docker's image fields"); |
| 168 | |
| 169 | VERIFY_ARE_NOT_EQUAL(std::string{}, entry["Repository"].get<std::string>()); |
| 170 | VERIFY_ARE_NOT_EQUAL(std::string{}, entry["Tag"].get<std::string>()); |
| 171 | VERIFY_ARE_EQUAL(std::string{c_none}, entry["Digest"].get<std::string>()); |
| 172 | |
| 173 | const auto containers = entry["Containers"].get<std::string>(); |
| 174 | VERIFY_IS_FALSE(containers.empty()); |
| 175 | VERIFY_IS_TRUE( |
| 176 | std::ranges::all_of(containers, [](char value) { return std::isdigit(static_cast<unsigned char>(value)) != 0; }), |
| 177 | L"'Containers' must be a container count"); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat_ReportsContainerCount) |
| 182 | { |
| 183 | // Every container created from an image is counted, including containers that were never |
| 184 | // started, and the count is reported against every tag of that image. |
| 185 | constexpr auto containerName = L"wslc-image-list-container-count"; |
| 186 | EnsureContainerDoesNotExist(containerName); |
| 187 | |
| 188 | auto containerCount = [](const TestImage& image) { |
| 189 | const auto result = RunWslc(L"image list --format json"); |
| 190 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 191 | |
| 192 | for (const auto& entry : ParseNdjsonOutputAs<ImageOutputInformation>(result)) |
| 193 | { |
| 194 | if (entry.Repository == wsl::shared::string::WideToMultiByte(image.Name) && |
| 195 | entry.Tag == wsl::shared::string::WideToMultiByte(image.Tag)) |
| 196 | { |
| 197 | return std::stoi(entry.Containers); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | VERIFY_FAIL(std::format(L"Image '{}' not found in image list output", image.NameAndTag()).c_str()); |
| 202 | return -1; |
| 203 | }; |
| 204 | |
| 205 | const auto alpineBaseline = containerCount(AlpineImage); |
| 206 | const auto debianBaseline = containerCount(DebianImage); |
| 207 | |
| 208 | auto createResult = RunWslc(std::format(L"container create --name {} {}", containerName, AlpineImage.NameAndTag())); |
| 209 | createResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 210 | auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(containerName); }); |
| 211 | |
| 212 | VERIFY_ARE_EQUAL(alpineBaseline + 1, containerCount(AlpineImage), L"a created container must be counted"); |
| 213 | VERIFY_ARE_EQUAL(debianBaseline, containerCount(DebianImage), L"only the image the container was created from is counted"); |
| 214 | |
| 215 | cleanup.reset(); |
| 216 | |
| 217 | VERIFY_ARE_EQUAL(alpineBaseline, containerCount(AlpineImage), L"a removed container must no longer be counted"); |
| 218 | } |
| 219 | |
| 220 | WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat_TruncatesIdByDefault) |
| 221 | { |
| 222 | // The id is truncated to 12 hex characters unless --no-trunc is passed, in which case it |
| 223 | // keeps the sha256: prefix. |
| 224 | auto truncResult = RunWslc(L"image list --format json"); |
| 225 | truncResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 226 | |
| 227 | for (const auto& image : ParseNdjsonOutputAs<ImageOutputInformation>(truncResult)) |
| 228 | { |
| 229 | VERIFY_ARE_EQUAL(12u, image.ID.size(), L"json ids must be truncated to 12 characters by default"); |
| 230 | VERIFY_IS_FALSE(image.ID.starts_with("sha256:")); |
| 231 | } |
| 232 | |
| 233 | auto noTruncResult = RunWslc(L"image list --format json --no-trunc"); |
| 234 | noTruncResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 235 | |
| 236 | for (const auto& image : ParseNdjsonOutputAs<ImageOutputInformation>(noTruncResult)) |
| 237 | { |
| 238 | VERIFY_IS_TRUE(image.ID.starts_with("sha256:"), L"--no-trunc ids must keep the algorithm prefix"); |
| 239 | VERIFY_IS_GREATER_THAN(image.ID.size(), 12u); |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_HasExpectedColumns) |
| 244 | { |
| 245 | const auto result = RunWslc(L"image list"); |
| 246 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 247 | |
| 248 | bool foundHeader = false; |
| 249 | for (const auto& line : result.GetStdoutLines()) |
| 250 | { |
| 251 | if (line.find(L"REPOSITORY") != std::wstring::npos && line.find(L"TAG") != std::wstring::npos && |
| 252 | line.find(L"IMAGE ID") != std::wstring::npos && line.find(L"CREATED") != std::wstring::npos && |
| 253 | line.find(L"SIZE") != std::wstring::npos) |
| 254 | { |
| 255 | foundHeader = true; |
| 256 | break; |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | VERIFY_IS_TRUE(foundHeader, L"Expected table header with REPOSITORY, TAG, IMAGE ID, CREATED, SIZE columns"); |
| 261 | } |
| 262 | |
| 263 | WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_MatchesJsonValues) |
| 264 | { |
| 265 | // The table and json output must report the same values, formatted the way docker formats |
| 266 | // them: SI sizes ("120MB", not "119.86 MB") and "<none>" for missing repository/tag data. |
| 267 | const auto jsonResult = RunWslc(L"image list --format json"); |
| 268 | jsonResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 269 | |
| 270 | const auto tableResult = RunWslc(L"image list"); |
| 271 | tableResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 272 | const auto tableLines = tableResult.GetStdoutLines(); |
| 273 | |
| 274 | const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(jsonResult); |
| 275 | VERIFY_IS_GREATER_THAN_OR_EQUAL(images.size(), 2u); |
| 276 | |
| 277 | for (const auto& image : images) |
| 278 | { |
| 279 | const auto row = std::format( |
| 280 | L"{} {} {} {}", |
| 281 | wsl::shared::string::MultiByteToWide(image.Repository), |
| 282 | wsl::shared::string::MultiByteToWide(image.Tag), |
| 283 | wsl::shared::string::MultiByteToWide(image.ID), |
| 284 | wsl::shared::string::MultiByteToWide(image.Size)); |
| 285 | |
| 286 | const bool found = std::ranges::any_of(tableLines, [&](const auto& line) { |
| 287 | // Columns are padded, so match on the individual values rather than the row text. |
| 288 | return line.find(wsl::shared::string::MultiByteToWide(image.ID)) != std::wstring::npos && |
| 289 | line.find(wsl::shared::string::MultiByteToWide(image.Repository)) != std::wstring::npos && |
| 290 | line.find(wsl::shared::string::MultiByteToWide(image.Tag)) != std::wstring::npos && |
| 291 | line.find(wsl::shared::string::MultiByteToWide(image.Size)) != std::wstring::npos && |
| 292 | line.find(wsl::shared::string::MultiByteToWide(image.CreatedSince)) != std::wstring::npos; |
| 293 | }); |
| 294 | |
| 295 | VERIFY_IS_TRUE(found, std::format(L"Table output has no row matching json values: {}", row).c_str()); |
| 296 | } |
| 297 | |
| 298 | // An untagged image is never labeled "<untagged>" in either format. |
| 299 | for (const auto& line : tableLines) |
| 300 | { |
| 301 | VERIFY_ARE_EQUAL(std::wstring::npos, line.find(L"<untagged>"), L"table must use the '<none>' placeholder"); |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_NoTruncKeepsAlgorithmPrefix) |
| 306 | { |
| 307 | // The --no-trunc table keeps the "sha256:" prefix on the image id. |
| 308 | const auto result = RunWslc(L"image list --no-trunc"); |
| 309 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 310 | |
| 311 | auto lines = result.GetStdoutLines(); |
| 312 | VERIFY_IS_GREATER_THAN_OR_EQUAL(lines.size(), 2u); |
| 313 | |
| 314 | for (size_t i = 1; i < lines.size(); i++) |
| 315 | { |
| 316 | if (!lines[i].empty()) |
| 317 | { |
| 318 | VERIFY_ARE_NOT_EQUAL( |
| 319 | std::wstring::npos, lines[i].find(L"sha256:"), L"--no-trunc table ids must keep the algorithm prefix"); |
| 320 | } |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_MalformedValue) |
| 325 | { |
| 326 | // Filter values must be of the form key=value; bare keys are rejected by the CLI. |
| 327 | const auto result = RunWslc(L"image list --filter dangling"); |
| 328 | result.Verify({.Stdout = L"", .ExitCode = 1}); |
| 329 | VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"dangling"))); |
| 330 | } |
| 331 | |
| 332 | WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_InvalidKey) |
| 333 | { |
| 334 | // Filter keys are validated by the Docker daemon, which rejects unknown keys. |
| 335 | const auto result = RunWslc(L"image list --filter color=blue"); |
| 336 | VERIFY_ARE_EQUAL(1, result.ExitCode); |
| 337 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 338 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter")); |
| 339 | } |
| 340 | |
| 341 | WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_Reference) |
| 342 | { |
| 343 | auto listNames = [&](const std::wstring& filterArgs) { |
| 344 | auto r = RunWslc(std::format(L"image list --format json {}", filterArgs)); |
| 345 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 346 | const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(r); |
| 347 | std::set<std::wstring> names; |
| 348 | for (const auto& image : images) |
| 349 | { |
| 350 | names.insert(std::format( |
| 351 | L"{}:{}", wsl::shared::string::MultiByteToWide(image.Repository), wsl::shared::string::MultiByteToWide(image.Tag))); |
| 352 | } |
| 353 | return names; |
| 354 | }; |
| 355 | |
| 356 | // reference=<name> matches only the matching image. |
| 357 | { |
| 358 | const auto names = listNames(std::format(L"--filter reference={}", DebianImage.Name)); |
| 359 | VERIFY_IS_TRUE(names.contains(DebianImage.NameAndTag())); |
| 360 | VERIFY_IS_FALSE(names.contains(AlpineImage.NameAndTag())); |
| 361 | } |
| 362 | |
| 363 | { |
| 364 | const auto names = listNames(std::format(L"--filter reference={}", AlpineImage.Name)); |
| 365 | VERIFY_IS_FALSE(names.contains(DebianImage.NameAndTag())); |
| 366 | VERIFY_IS_TRUE(names.contains(AlpineImage.NameAndTag())); |
| 367 | } |
| 368 | |
| 369 | // Multiple --filter reference= values are OR'd: both images should be returned. |
| 370 | { |
| 371 | const auto names = listNames(std::format(L"--filter reference={} --filter reference={}", DebianImage.Name, AlpineImage.Name)); |
| 372 | VERIFY_IS_TRUE(names.contains(DebianImage.NameAndTag())); |
| 373 | VERIFY_IS_TRUE(names.contains(AlpineImage.NameAndTag())); |
| 374 | } |
| 375 | |
| 376 | // A reference that matches nothing returns neither image. |
| 377 | { |
| 378 | const auto names = listNames(L"--filter reference=wslc-no-such-image-zzz"); |
| 379 | VERIFY_IS_FALSE(names.contains(DebianImage.NameAndTag())); |
| 380 | VERIFY_IS_FALSE(names.contains(AlpineImage.NameAndTag())); |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_Dangling) |
| 385 | { |
| 386 | // dangling=false should include normal (tagged) images. |
| 387 | auto result = RunWslc(L"image list --format json --filter dangling=false"); |
| 388 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 389 | |
| 390 | auto images = ParseNdjsonOutputAs<ImageOutputInformation>(result); |
| 391 | bool foundDebian = false; |
| 392 | for (const auto& image : images) |
| 393 | { |
| 394 | if (image.Repository == wsl::shared::string::WideToMultiByte(DebianImage.Name)) |
| 395 | { |
| 396 | foundDebian = true; |
| 397 | break; |
| 398 | } |
| 399 | } |
| 400 | VERIFY_IS_TRUE(foundDebian, L"Expected debian image to appear in dangling=false image list"); |
| 401 | |
| 402 | // dangling=true should exclude all tagged images. |
| 403 | result = RunWslc(L"image list --format json --filter dangling=true"); |
| 404 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 405 | |
| 406 | images = ParseNdjsonOutputAs<ImageOutputInformation>(result); |
| 407 | for (const auto& image : images) |
| 408 | { |
| 409 | VERIFY_ARE_EQUAL( |
| 410 | std::string{wsl::windows::wslc::models::c_none}, |
| 411 | image.Repository, |
| 412 | L"dangling=true list should not contain tagged images"); |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_MultipleKinds) |
| 417 | { |
| 418 | // Mixing different filter kinds should AND: reference=<debian> AND dangling=false |
| 419 | // narrows to just debian (alpine is excluded by the reference filter). |
| 420 | const auto result = |
| 421 | RunWslc(std::format(L"image list --format json --filter reference={} --filter dangling=false", DebianImage.Name)); |
| 422 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 423 | |
| 424 | const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(result); |
| 425 | bool foundDebian = false; |
| 426 | for (const auto& image : images) |
| 427 | { |
| 428 | const auto repo = wsl::shared::string::MultiByteToWide(image.Repository); |
| 429 | VERIFY_ARE_NOT_EQUAL(AlpineImage.Name, repo, L"alpine should not appear when filtering by reference=debian"); |
| 430 | if (repo == DebianImage.Name) |
| 431 | { |
| 432 | foundDebian = true; |
| 433 | } |
| 434 | } |
| 435 | VERIFY_IS_TRUE(foundDebian, L"Expected debian image when combining reference and dangling filters"); |
| 436 | } |
| 437 | |
| 438 | WSLC_TEST_METHOD(WSLCE2E_Image_List_NoTrunc_ShowsFullImageId) |
| 439 | { |
| 440 | // Pull the full image id from JSON output. --no-trunc is required because json output |
| 441 | // truncates the id by default. |
| 442 | auto jsonResult = RunWslc(L"image list --format json --no-trunc"); |
| 443 | jsonResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 444 | const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(jsonResult); |
| 445 | |
| 446 | std::string fullDebianId; |
| 447 | for (const auto& image : images) |
| 448 | { |
| 449 | if (image.Repository == wsl::shared::string::WideToMultiByte(DebianImage.Name)) |
| 450 | { |
| 451 | fullDebianId = image.ID; |
| 452 | break; |
| 453 | } |
| 454 | } |
| 455 | VERIFY_ARE_NOT_EQUAL(std::string{}, fullDebianId, L"Debian image was not present in `image list --format json` output"); |
| 456 | |
| 457 | fullDebianId = GetHashId(fullDebianId, true); |
| 458 | VERIFY_IS_GREATER_THAN(fullDebianId.size(), 12u); |
| 459 | const auto fullDebianIdW = wsl::shared::string::MultiByteToWide(fullDebianId); |
| 460 | const auto truncatedDebianIdW = fullDebianIdW.substr(0, 12); |
| 461 | |
| 462 | // Default table truncates IMAGE ID to 12 chars. |
| 463 | auto truncResult = RunWslc(L"image list"); |
| 464 | truncResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 465 | VERIFY_IS_TRUE(truncResult.StdoutContainsSubstring(truncatedDebianIdW)); |
| 466 | VERIFY_IS_FALSE(truncResult.StdoutContainsSubstring(fullDebianIdW)); |
| 467 | |
| 468 | // --no-trunc must show the full id. |
| 469 | auto noTruncResult = RunWslc(L"image list --no-trunc"); |
| 470 | noTruncResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 471 | VERIFY_IS_TRUE(noTruncResult.StdoutContainsSubstring(fullDebianIdW)); |
| 472 | } |
| 473 | |
| 474 | private: |
| 475 | const TestImage& DebianImage = DebianTestImage(); |
| 476 | const TestImage& AlpineImage = AlpineTestImage(); |
| 477 | }; |
| 478 | } // namespace WSLCE2ETests |