64
65
WSLC_TEST_METHOD(WSLCE2E_Image_List_QuietOption_OutputsIdsOnly)
66
{
67
- // Get the expected image ID from JSON output.
68
- auto jsonResult = RunWslc(L"image list --format json");
67
+ // Get the expected image ID from JSON output. --no-trunc is required because json output
68
+ // truncates the id by default.
69
+ auto jsonResult = RunWslc(L"image list --format json --no-trunc");
70
jsonResult.Verify({.Stderr = L"", .ExitCode = 0});
70
- const auto images = ParseNdjsonOutputAs<ImageInformation>(jsonResult);
71
+ const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(jsonResult);
72
73
std::string debianId;
74
for (const auto& image : images)
76
if (image.Repository == wsl::shared::string::WideToMultiByte(DebianImage.Name) &&
77
image.Tag == wsl::shared::string::WideToMultiByte(DebianImage.Tag))
78
{
78
- debianId = image.Id;
79
+ debianId = image.ID;
80
break;
81
}
82
}
129
const auto result = RunWslc(L"image list --format json");
130
result.Verify({.Stderr = L"", .ExitCode = 0});
131
131
- const auto images = ParseNdjsonOutputAs<ImageInformation>(result);
132
+ const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(result);
133
134
VERIFY_IS_GREATER_THAN_OR_EQUAL(images.size(), 2u);
135
137
for (const auto& image : images)
138
{
139
auto nameAndTag = std::format(
139
- L"{}:{}",
140
- wsl::shared::string::MultiByteToWide(image.Repository.value_or("<untagged>")),
141
- wsl::shared::string::MultiByteToWide(image.Tag.value_or("<untagged>")));
140
+ L"{}:{}", wsl::shared::string::MultiByteToWide(image.Repository), wsl::shared::string::MultiByteToWide(image.Tag));
141
imageNames.push_back(nameAndTag);
142
}
143
145
VERIFY_ARE_NOT_EQUAL(imageNames.end(), std::find(imageNames.begin(), imageNames.end(), AlpineImage.NameAndTag()));
146
}
147
148
+ WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat_MatchesDockerShape)
149
+ {
150
+ const std::set<std::string> expectedKeys = {
151
+ "Containers", "CreatedAt", "CreatedSince", "Digest", "ID", "Repository", "SharedSize", "Size", "Tag", "UniqueSize"};
152
+
153
+ const auto result = RunWslc(L"image list --format json");
154
+ result.Verify({.Stderr = L"", .ExitCode = 0});
155
+
156
+ const auto entries = ParseNdjsonOutput(result);
157
+ VERIFY_IS_GREATER_THAN_OR_EQUAL(entries.size(), 2u);
158
+
159
+ for (const auto& entry : entries)
160
+ {
161
+ std::set<std::string> keys;
162
+ for (const auto& [key, value] : entry.items())
163
+ {
164
+ keys.insert(key);
165
+ VERIFY_IS_TRUE(value.is_string(), wsl::shared::string::MultiByteToWide(std::format("'{}' must be a string", key)).c_str());
166
+ }
167
+
168
+ VERIFY_ARE_EQUAL(expectedKeys, keys, L"json output must contain exactly docker's image fields");
169
+
170
+ VERIFY_ARE_NOT_EQUAL(std::string{}, entry["Repository"].get<std::string>());
171
+ VERIFY_ARE_NOT_EQUAL(std::string{}, entry["Tag"].get<std::string>());
172
+ VERIFY_ARE_EQUAL(std::string{c_none}, entry["Digest"].get<std::string>());
173
+
174
+ const auto containers = entry["Containers"].get<std::string>();
175
+ VERIFY_IS_FALSE(containers.empty());
176
+ VERIFY_IS_TRUE(
177
+ std::ranges::all_of(containers, [](char value) { return std::isdigit(static_cast<unsigned char>(value)) != 0; }),
178
+ L"'Containers' must be a container count");
179
+ }
180
+ }
181
+
182
+ WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat_ReportsContainerCount)
183
+ {
184
+ // Every container created from an image is counted, including containers that were never
185
+ // started, and the count is reported against every tag of that image.
186
+ constexpr auto containerName = L"wslc-image-list-container-count";
187
+ EnsureContainerDoesNotExist(containerName);
188
+
189
+ auto containerCount = [](const TestImage& image) {
190
+ const auto result = RunWslc(L"image list --format json");
191
+ result.Verify({.Stderr = L"", .ExitCode = 0});
192
+
193
+ for (const auto& entry : ParseNdjsonOutputAs<ImageOutputInformation>(result))
194
+ {
195
+ if (entry.Repository == wsl::shared::string::WideToMultiByte(image.Name) &&
196
+ entry.Tag == wsl::shared::string::WideToMultiByte(image.Tag))
197
+ {
198
+ return std::stoi(entry.Containers);
199
+ }
200
+ }
201
+
202
+ VERIFY_FAIL(std::format(L"Image '{}' not found in image list output", image.NameAndTag()).c_str());
203
+ return -1;
204
+ };
205
+
206
+ const auto alpineBaseline = containerCount(AlpineImage);
207
+ const auto debianBaseline = containerCount(DebianImage);
208
+
209
+ auto createResult = RunWslc(std::format(L"container create --name {} {}", containerName, AlpineImage.NameAndTag()));
210
+ createResult.Verify({.Stderr = L"", .ExitCode = 0});
211
+ auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(containerName); });
212
+
213
+ VERIFY_ARE_EQUAL(alpineBaseline + 1, containerCount(AlpineImage), L"a created container must be counted");
214
+ VERIFY_ARE_EQUAL(debianBaseline, containerCount(DebianImage), L"only the image the container was created from is counted");
215
+
216
+ cleanup.reset();
217
+
218
+ VERIFY_ARE_EQUAL(alpineBaseline, containerCount(AlpineImage), L"a removed container must no longer be counted");
219
+ }
220
+
221
+ WSLC_TEST_METHOD(WSLCE2E_Image_List_JsonFormat_TruncatesIdByDefault)
222
+ {
223
+ // The id is truncated to 12 hex characters unless --no-trunc is passed, in which case it
224
+ // keeps the sha256: prefix.
225
+ auto truncResult = RunWslc(L"image list --format json");
226
+ truncResult.Verify({.Stderr = L"", .ExitCode = 0});
227
+
228
+ for (const auto& image : ParseNdjsonOutputAs<ImageOutputInformation>(truncResult))
229
+ {
230
+ VERIFY_ARE_EQUAL(12u, image.ID.size(), L"json ids must be truncated to 12 characters by default");
231
+ VERIFY_IS_FALSE(image.ID.starts_with("sha256:"));
232
+ }
233
+
234
+ auto noTruncResult = RunWslc(L"image list --format json --no-trunc");
235
+ noTruncResult.Verify({.Stderr = L"", .ExitCode = 0});
236
+
237
+ for (const auto& image : ParseNdjsonOutputAs<ImageOutputInformation>(noTruncResult))
238
+ {
239
+ VERIFY_IS_TRUE(image.ID.starts_with("sha256:"), L"--no-trunc ids must keep the algorithm prefix");
240
+ VERIFY_IS_GREATER_THAN(image.ID.size(), 12u);
241
+ }
242
+ }
243
+
244
WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_HasExpectedColumns)
245
{
246
const auto result = RunWslc(L"image list");
261
VERIFY_IS_TRUE(foundHeader, L"Expected table header with REPOSITORY, TAG, IMAGE ID, CREATED, SIZE columns");
262
}
263
264
+ WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_MatchesJsonValues)
265
+ {
266
+ // The table and json output must report the same values, formatted the way docker formats
267
+ // them: SI sizes ("120MB", not "119.86 MB") and "<none>" for missing repository/tag data.
268
+ const auto jsonResult = RunWslc(L"image list --format json");
269
+ jsonResult.Verify({.Stderr = L"", .ExitCode = 0});
270
+
271
+ const auto tableResult = RunWslc(L"image list");
272
+ tableResult.Verify({.Stderr = L"", .ExitCode = 0});
273
+ const auto tableLines = tableResult.GetStdoutLines();
274
+
275
+ const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(jsonResult);
276
+ VERIFY_IS_GREATER_THAN_OR_EQUAL(images.size(), 2u);
277
+
278
+ for (const auto& image : images)
279
+ {
280
+ const auto row = std::format(
281
+ L"{} {} {} {}",
282
+ wsl::shared::string::MultiByteToWide(image.Repository),
283
+ wsl::shared::string::MultiByteToWide(image.Tag),
284
+ wsl::shared::string::MultiByteToWide(image.ID),
285
+ wsl::shared::string::MultiByteToWide(image.Size));
286
+
287
+ const bool found = std::ranges::any_of(tableLines, [&](const auto& line) {
288
+ // Columns are padded, so match on the individual values rather than the row text.
289
+ return line.find(wsl::shared::string::MultiByteToWide(image.ID)) != std::wstring::npos &&
290
+ line.find(wsl::shared::string::MultiByteToWide(image.Repository)) != std::wstring::npos &&
291
+ line.find(wsl::shared::string::MultiByteToWide(image.Tag)) != std::wstring::npos &&
292
+ line.find(wsl::shared::string::MultiByteToWide(image.Size)) != std::wstring::npos &&
293
+ line.find(wsl::shared::string::MultiByteToWide(image.CreatedSince)) != std::wstring::npos;
294
+ });
295
+
296
+ VERIFY_IS_TRUE(found, std::format(L"Table output has no row matching json values: {}", row).c_str());
297
+ }
298
+
299
+ // An untagged image is never labeled "<untagged>" in either format.
300
+ for (const auto& line : tableLines)
301
+ {
302
+ VERIFY_ARE_EQUAL(std::wstring::npos, line.find(L"<untagged>"), L"table must use the '<none>' placeholder");
303
+ }
304
+ }
305
+
306
+ WSLC_TEST_METHOD(WSLCE2E_Image_List_TableFormat_NoTruncKeepsAlgorithmPrefix)
307
+ {
308
+ // The --no-trunc table keeps the "sha256:" prefix on the image id.
309
+ const auto result = RunWslc(L"image list --no-trunc");
310
+ result.Verify({.Stderr = L"", .ExitCode = 0});
311
+
312
+ auto lines = result.GetStdoutLines();
313
+ VERIFY_IS_GREATER_THAN_OR_EQUAL(lines.size(), 2u);
314
+
315
+ for (size_t i = 1; i < lines.size(); i++)
316
+ {
317
+ if (!lines[i].empty())
318
+ {
319
+ VERIFY_ARE_NOT_EQUAL(
320
+ std::wstring::npos, lines[i].find(L"sha256:"), L"--no-trunc table ids must keep the algorithm prefix");
321
+ }
322
+ }
323
+ }
324
+
325
WSLC_TEST_METHOD(WSLCE2E_Image_List_Filter_MalformedValue)
326
{
327
// Filter values must be of the form key=value; bare keys are rejected by the CLI.
344
auto listNames = [&](const std::wstring& filterArgs) {
345
auto r = RunWslc(std::format(L"image list --format json {}", filterArgs));
346
r.Verify({.Stderr = L"", .ExitCode = 0});
191
- const auto images = ParseNdjsonOutputAs<ImageInformation>(r);
347
+ const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(r);
348
std::set<std::wstring> names;
349
for (const auto& image : images)
350
{
351
names.insert(std::format(
196
- L"{}:{}",
197
- wsl::shared::string::MultiByteToWide(image.Repository.value_or("<untagged>")),
198
- wsl::shared::string::MultiByteToWide(image.Tag.value_or("<untagged>"))));
352
+ L"{}:{}", wsl::shared::string::MultiByteToWide(image.Repository), wsl::shared::string::MultiByteToWide(image.Tag)));
353
}
354
return names;
355
};
388
auto result = RunWslc(L"image list --format json --filter dangling=false");
389
result.Verify({.Stderr = L"", .ExitCode = 0});
390
237
- auto images = ParseNdjsonOutputAs<ImageInformation>(result);
391
+ auto images = ParseNdjsonOutputAs<ImageOutputInformation>(result);
392
bool foundDebian = false;
393
for (const auto& image : images)
394
{
404
result = RunWslc(L"image list --format json --filter dangling=true");
405
result.Verify({.Stderr = L"", .ExitCode = 0});
406
253
- images = ParseNdjsonOutputAs<ImageInformation>(result);
407
+ images = ParseNdjsonOutputAs<ImageOutputInformation>(result);
408
for (const auto& image : images)
409
{
256
- VERIFY_IS_FALSE(
257
- image.Repository.has_value() && image.Repository.value() != "<none>",
410
+ VERIFY_ARE_EQUAL(
411
+ std::string{wsl::windows::wslc::models::c_none},
412
+ image.Repository,
413
L"dangling=true list should not contain tagged images");
414
}
415
}
422
RunWslc(std::format(L"image list --format json --filter reference={} --filter dangling=false", DebianImage.Name));
423
result.Verify({.Stderr = L"", .ExitCode = 0});
424
270
- const auto images = ParseNdjsonOutputAs<ImageInformation>(result);
425
+ const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(result);
426
bool foundDebian = false;
427
for (const auto& image : images)
428
{
274
- const auto repo = wsl::shared::string::MultiByteToWide(image.Repository.value_or(""));
429
+ const auto repo = wsl::shared::string::MultiByteToWide(image.Repository);
430
VERIFY_ARE_NOT_EQUAL(AlpineImage.Name, repo, L"alpine should not appear when filtering by reference=debian");
431
if (repo == DebianImage.Name)
432
{
438
439
WSLC_TEST_METHOD(WSLCE2E_Image_List_NoTrunc_ShowsFullImageId)
440
{
286
- // Pull the full image id from JSON output (always untruncated).
287
- auto jsonResult = RunWslc(L"image list --format json");
441
+ // Pull the full image id from JSON output. --no-trunc is required because json output
442
+ // truncates the id by default.
443
+ auto jsonResult = RunWslc(L"image list --format json --no-trunc");
444
jsonResult.Verify({.Stderr = L"", .ExitCode = 0});
289
- const auto images = ParseNdjsonOutputAs<ImageInformation>(jsonResult);
445
+ const auto images = ParseNdjsonOutputAs<ImageOutputInformation>(jsonResult);
446
447
std::string fullDebianId;
448
for (const auto& image : images)
449
{
450
if (image.Repository == wsl::shared::string::WideToMultiByte(DebianImage.Name))
451
{
296
- fullDebianId = image.Id;
452
+ fullDebianId = image.ID;
453
break;
454
}
455
}