master
cpp 431 lines 20.8 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EInspectTests.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 "WSLCExecutor.h"
17 #include "WSLCE2EHelpers.h"
18 #include "TestImageRegistry.h"
19 #include <wslc_schema.h>
20 #include <JsonUtils.h>
21
22 namespace WSLCE2ETests {
23 using namespace wsl::shared;
24
25 class WSLCE2EInspectTests
26 {
27 WSLC_TEST_CLASS(WSLCE2EInspectTests)
28
29 TEST_CLASS_SETUP(ClassSetup)
30 {
31 TestImageRegistry::Instance().EnsureLoaded(DebianImage);
32 return true;
33 }
34
35 TEST_CLASS_CLEANUP(ClassCleanup)
36 {
37 EnsureContainerDoesNotExist(WslcContainerName);
38 EnsureContainerDoesNotExist(DebianImage.Name);
39 EnsureNetworkDoesNotExist(WslcNetworkName);
40 EnsureNetworkDoesNotExist(DebianImage.Name);
41 return true;
42 }
43
44 TEST_METHOD_SETUP(MethodSetup)
45 {
46 EnsureContainerDoesNotExist(WslcContainerName);
47 EnsureContainerDoesNotExist(DebianImage.Name);
48 EnsureNetworkDoesNotExist(WslcNetworkName);
49 EnsureNetworkDoesNotExist(DebianImage.Name);
50 return true;
51 }
52
53 WSLC_TEST_METHOD(WSLCE2E_Inspect_HelpCommand)
54 {
55 auto result = RunWslc(L"inspect --help");
56 result.Verify({.Stderr = L"", .ExitCode = 0});
57 VERIFY_IS_FALSE(result.Stdout.value().empty());
58 }
59
60 WSLC_TEST_METHOD(WSLCE2E_Inspect_MissingObjectId)
61 {
62 auto result = RunWslc(L"inspect");
63 result.Verify({.Stdout = L"", .ExitCode = 1});
64 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'object-id'"));
65 }
66
67 WSLC_TEST_METHOD(WSLCE2E_Inspect_ObjectNotFound)
68 {
69 auto result = RunWslc(std::format(L"inspect {}", InvalidImage.NameAndTag()));
70 result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
71 }
72
73 WSLC_TEST_METHOD(WSLCE2E_Inspect_SizeIgnoredForNonContainerTypes)
74 {
75 // --size on an object with no file sizes warns and inspects the object anyway.
76 auto result = RunWslc(std::format(L"inspect --size {}", DebianImage.NameAndTag()));
77 result.Verify({.ExitCode = 0});
78 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"WARNING: --size ignored for image"));
79
80 auto document = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
81 VERIFY_ARE_EQUAL(1u, document.size());
82 VERIFY_IS_FALSE(document[0].contains("SizeRw"));
83
84 // A plain inspect of the same image emits no warning.
85 auto plain = RunWslc(std::format(L"inspect {}", DebianImage.NameAndTag()));
86 plain.Verify({.Stderr = L"", .ExitCode = 0});
87 }
88
89 WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_Success)
90 {
91 auto result = RunWslc(std::format(L"inspect {}", DebianImage.NameAndTag()));
92 result.Verify({.Stderr = L"", .ExitCode = 0});
93 auto inspectData =
94 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
95 VERIFY_ARE_EQUAL(1u, inspectData.size());
96 VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
97 VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
98 VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
99 }
100
101 WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_WithTypeFlag)
102 {
103 auto result = RunWslc(std::format(L"inspect --type image {}", DebianImage.NameAndTag()));
104 result.Verify({.Stderr = L"", .ExitCode = 0});
105 auto inspectData =
106 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
107 VERIFY_ARE_EQUAL(1u, inspectData.size());
108 VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
109 VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
110 }
111
112 WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_TypeMismatch)
113 {
114 auto result = RunWslc(std::format(L"inspect --type container {}", DebianImage.NameAndTag()));
115 result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", DebianImage.NameAndTag()), .ExitCode = 1});
116 }
117
118 WSLC_TEST_METHOD(WSLCE2E_Inspect_FormatJson_IsSingleLine)
119 {
120 // The whole array is emitted on one compact line.
121 auto result = RunWslc(std::format(L"inspect --format json {}", DebianImage.NameAndTag()));
122 result.Verify({.Stderr = L"", .ExitCode = 0});
123
124 const auto document = VerifyCompactJsonOutput(result);
125 VERIFY_IS_TRUE(document.is_array());
126 VERIFY_ARE_EQUAL(1u, document.size());
127 VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(DebianImage.NameAndTag()), document[0]["RepoTags"][0].get<std::string>());
128
129 // The compact rendering must not contain the pretty-printer's indentation.
130 VERIFY_IS_FALSE(result.StdoutContainsSubstring(L"\n "));
131 }
132
133 WSLC_TEST_METHOD(WSLCE2E_Inspect_DefaultFormat_IsIndented)
134 {
135 // Without --format the array stays indented over several lines.
136 auto result = RunWslc(std::format(L"inspect {}", DebianImage.NameAndTag()));
137 result.Verify({.Stderr = L"", .ExitCode = 0});
138 VERIFY_IS_GREATER_THAN(result.GetStdoutLines().size(), 1u);
139 }
140
141 WSLC_TEST_METHOD(WSLCE2E_Inspect_InvalidFormatOption)
142 {
143 auto result = RunWslc(std::format(L"inspect --format invalid {}", DebianImage.NameAndTag()));
144 result.Verify({.Stdout = L"", .ExitCode = 1});
145 VERIFY_IS_TRUE(result.StderrContainsSubstring(
146 L"Invalid format value: invalid is not a recognized format type. Supported format types are: json."));
147
148 // json is the only rendering the inspect family has; `table` belongs to the list commands.
149 auto tableResult = RunWslc(std::format(L"inspect --format table {}", DebianImage.NameAndTag()));
150 tableResult.Verify({.Stdout = L"", .ExitCode = 1});
151 VERIFY_IS_TRUE(tableResult.StderrContainsSubstring(
152 L"Invalid format value: table is not a recognized format type. Supported format types are: json."));
153 }
154
155 WSLC_TEST_METHOD(WSLCE2E_Inspect_FormatJson_ObjectNotFound)
156 {
157 // An empty array is already compact, so both formats agree on "[]".
158 auto result = RunWslc(std::format(L"inspect --format json {}", InvalidImage.NameAndTag()));
159 result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
160 }
161
162 WSLC_TEST_METHOD(WSLCE2E_Inspect_Container_Success)
163 {
164 EnsureContainerDoesNotExist(WslcContainerName);
165 auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
166 createResult.Verify({.Stderr = L"", .ExitCode = 0});
167
168 auto result = RunWslc(std::format(L"inspect {}", WslcContainerName));
169 result.Verify({.Stderr = L"", .ExitCode = 0});
170 auto inspectData =
171 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
172 VERIFY_ARE_EQUAL(1u, inspectData.size());
173 VERIFY_ARE_EQUAL(std::format(L"/{}", WslcContainerName), wsl::shared::string::MultiByteToWide(inspectData[0].Name));
174
175 // Config.Labels must be present in the emitted JSON even when empty.
176 auto json = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
177 VERIFY_IS_TRUE(json.is_array() && !json.empty());
178 VERIFY_IS_TRUE(json[0].contains("Config") && json[0]["Config"].contains("Labels"));
179 VERIFY_IS_TRUE(json[0]["Config"].contains("Image"));
180 VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(DebianImage.NameAndTag()), json[0]["Config"]["Image"].get<std::string>());
181 }
182
183 WSLC_TEST_METHOD(WSLCE2E_Inspect_Container_InheritsImageLabels)
184 {
185 auto imageCleanup =
186 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { TestImageRegistry::Instance().Delete(LabelInheritImage); });
187 auto testRoot = std::filesystem::current_path() / L"wslc-e2e-inspect-inherit-labels";
188 auto cleanup = SetupTestDirectory(testRoot);
189
190 auto contextDir = testRoot / L"context";
191 std::error_code ec;
192 std::filesystem::create_directories(contextDir, ec);
193 THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
194
195 auto dockerfilePath = testRoot / L"Dockerfile";
196 WriteTestFileContent(
197 dockerfilePath,
198 "FROM debian:latest\n"
199 "LABEL com.microsoft.wsl.test.inherit-me=from-image\n"
200 "CMD [\"echo\", \"ok\"]\n");
201
202 auto buildResult = RunWslc(std::format(
203 L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), LabelInheritImage.NameAndTag()));
204 buildResult.Verify({.Stdout = L"", .ExitCode = 0});
205
206 EnsureContainerDoesNotExist(WslcContainerName);
207 auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, LabelInheritImage.NameAndTag()));
208 createResult.Verify({.Stderr = L"", .ExitCode = 0});
209
210 auto result = RunWslc(std::format(L"inspect {}", WslcContainerName));
211 result.Verify({.Stderr = L"", .ExitCode = 0});
212 auto inspectData =
213 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
214 VERIFY_ARE_EQUAL(1u, inspectData.size());
215
216 const auto& configLabels = inspectData[0].Config.Labels;
217 auto inheritedIt = configLabels.find("com.microsoft.wsl.test.inherit-me");
218 VERIFY_IS_TRUE(inheritedIt != configLabels.end());
219 VERIFY_ARE_EQUAL(std::string("from-image"), inheritedIt->second);
220
221 VERIFY_IS_TRUE(configLabels.find("com.microsoft.wsl.container.metadata") == configLabels.end());
222 }
223
224 WSLC_TEST_METHOD(WSLCE2E_Inspect_Volume_Success)
225 {
226 EnsureVolumeDoesNotExist(WslcVolumeName);
227
228 auto createResult = RunWslc(std::format(L"volume create {}", WslcVolumeName));
229 createResult.Verify({.Stderr = L"", .ExitCode = 0});
230 auto deleteVolume = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(WslcVolumeName); });
231
232 auto result = RunWslc(std::format(L"inspect {}", WslcVolumeName));
233 result.Verify({.Stderr = L"", .ExitCode = 0});
234
235 auto inspectData =
236 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
237 VERIFY_ARE_EQUAL(1u, inspectData.size());
238 VERIFY_ARE_EQUAL(WslcVolumeName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
239 }
240
241 WSLC_TEST_METHOD(WSLCE2E_Inspect_Container_PriorityOverImage)
242 {
243 // When a container and image share the same name and no --type is specified,
244 // the container should be returned (container is checked first in InspectTasks).
245 EnsureContainerDoesNotExist(DebianImage.Name);
246 auto createResult = RunWslc(std::format(L"container create --name {} {}", DebianImage.Name, DebianImage.NameAndTag()));
247 createResult.Verify({.Stderr = L"", .ExitCode = 0});
248
249 // No type specified
250 {
251 auto result = RunWslc(std::format(L"inspect {}", DebianImage.Name));
252 result.Verify({.Stderr = L"", .ExitCode = 0});
253 auto inspectData =
254 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
255 VERIFY_ARE_EQUAL(1u, inspectData.size());
256 VERIFY_ARE_EQUAL(std::format(L"/{}", DebianImage.Name), wsl::shared::string::MultiByteToWide(inspectData[0].Name));
257 }
258
259 // With --type container
260 {
261 auto result = RunWslc(std::format(L"inspect --type container {}", DebianImage.Name));
262 result.Verify({.Stderr = L"", .ExitCode = 0});
263 auto inspectData =
264 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
265 VERIFY_ARE_EQUAL(1u, inspectData.size());
266 VERIFY_ARE_EQUAL(std::format(L"/{}", DebianImage.Name), wsl::shared::string::MultiByteToWide(inspectData[0].Name));
267 }
268
269 // With --type image
270 {
271 auto result = RunWslc(std::format(L"inspect --type image {}", DebianImage.Name));
272 result.Verify({.Stderr = L"", .ExitCode = 0});
273 auto inspectData =
274 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
275 VERIFY_ARE_EQUAL(1u, inspectData.size());
276 VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
277 VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
278 VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
279 }
280 }
281
282 WSLC_TEST_METHOD(WSLCE2E_Inspect_Image_PriorityOverNetwork)
283 {
284 // When an image and network share the same name and no --type is specified,
285 // the image should be returned (image is checked before network in InspectTasks).
286 EnsureNetworkDoesNotExist(DebianImage.Name);
287 auto createResult = RunWslc(std::format(L"network create --driver bridge {}", DebianImage.Name));
288 createResult.Verify({.Stderr = L"", .ExitCode = 0});
289 auto deleteNetwork = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(DebianImage.Name); });
290
291 // No type specified
292 {
293 auto result = RunWslc(std::format(L"inspect {}", DebianImage.Name));
294 result.Verify({.Stderr = L"", .ExitCode = 0});
295
296 auto inspectData =
297 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
298 VERIFY_ARE_EQUAL(1u, inspectData.size());
299 VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
300 VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
301 VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
302 }
303
304 // With --type image
305 {
306 auto result = RunWslc(std::format(L"inspect --type image {}", DebianImage.Name));
307 result.Verify({.Stderr = L"", .ExitCode = 0});
308 auto inspectData =
309 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectImage>>(result.Stdout.value().c_str());
310 VERIFY_ARE_EQUAL(1u, inspectData.size());
311 VERIFY_IS_TRUE(inspectData[0].RepoTags.has_value());
312 VERIFY_ARE_EQUAL(1u, inspectData[0].RepoTags.value().size());
313 VERIFY_ARE_EQUAL(DebianImage.NameAndTag(), wsl::shared::string::MultiByteToWide(inspectData[0].RepoTags.value()[0]));
314 }
315
316 // With --type network
317 {
318 auto result = RunWslc(std::format(L"inspect --type network {}", DebianImage.Name));
319 result.Verify({.Stderr = L"", .ExitCode = 0});
320 auto inspectData =
321 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::Network>>(result.Stdout.value().c_str());
322 VERIFY_ARE_EQUAL(1u, inspectData.size());
323 VERIFY_ARE_EQUAL(DebianImage.Name, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
324 }
325 }
326
327 WSLC_TEST_METHOD(WSLCE2E_Inspect_Network_PriorityOverVolume)
328 {
329 // When a network and volume share the same name and no --type is specified,
330 // the network should be returned (network is checked before volume in InspectTasks).
331 EnsureNetworkDoesNotExist(WslcNetworkName);
332 EnsureVolumeDoesNotExist(WslcNetworkName);
333
334 auto createNetworkResult = RunWslc(std::format(L"network create --driver bridge {}", WslcNetworkName));
335 createNetworkResult.Verify({.Stderr = L"", .ExitCode = 0});
336 auto deleteNetwork = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(WslcNetworkName); });
337
338 auto createVolumeResult = RunWslc(std::format(L"volume create {}", WslcNetworkName));
339 createVolumeResult.Verify({.Stderr = L"", .ExitCode = 0});
340 auto deleteVolume = wil::scope_exit([&]() { EnsureVolumeDoesNotExist(WslcNetworkName); });
341
342 // No type specified — network takes priority over volume
343 {
344 auto result = RunWslc(std::format(L"inspect {}", WslcNetworkName));
345 result.Verify({.Stderr = L"", .ExitCode = 0});
346
347 auto inspectData =
348 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::Network>>(result.Stdout.value().c_str());
349 VERIFY_ARE_EQUAL(1u, inspectData.size());
350 VERIFY_ARE_EQUAL(WslcNetworkName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
351 }
352
353 // With --type network
354 {
355 auto result = RunWslc(std::format(L"inspect --type network {}", WslcNetworkName));
356 result.Verify({.Stderr = L"", .ExitCode = 0});
357 auto inspectData =
358 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::Network>>(result.Stdout.value().c_str());
359 VERIFY_ARE_EQUAL(1u, inspectData.size());
360 VERIFY_ARE_EQUAL(WslcNetworkName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
361 }
362
363 // With --type volume
364 {
365 auto result = RunWslc(std::format(L"inspect --type volume {}", WslcNetworkName));
366 result.Verify({.Stderr = L"", .ExitCode = 0});
367 auto inspectData =
368 wsl::shared::FromJson<std::vector<wsl::windows::common::wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
369 VERIFY_ARE_EQUAL(1u, inspectData.size());
370 VERIFY_ARE_EQUAL(WslcNetworkName, wsl::shared::string::MultiByteToWide(inspectData[0].Name));
371 }
372 }
373
374 WSLC_TEST_METHOD(WSLCE2E_Inspect_MultipleObjects)
375 {
376 EnsureContainerDoesNotExist(WslcContainerName);
377 auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
378 createResult.Verify({.Stderr = L"", .ExitCode = 0});
379
380 // Inspect both a container and an image in a single call
381 auto result = RunWslc(std::format(L"inspect {} {}", WslcContainerName, DebianImage.NameAndTag()));
382 result.Verify({.Stderr = L"", .ExitCode = 0});
383
384 // The result should be a JSON array with 2 entries
385 auto array = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
386 VERIFY_ARE_EQUAL(2u, array.size());
387 }
388
389 WSLC_TEST_METHOD(WSLCE2E_Inspect_MultipleObjects_PartialFailure)
390 {
391 // Inspect a valid image and an invalid object
392 auto result = RunWslc(std::format(L"inspect {} {}", DebianImage.NameAndTag(), InvalidImage.NameAndTag()));
393 result.Verify({.Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
394
395 // Stdout should still contain the valid result in a JSON array
396 auto array = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(result.Stdout.value()));
397 VERIFY_ARE_EQUAL(1u, array.size());
398 }
399
400 WSLC_TEST_METHOD(WSLCE2E_Inspect_InvalidTypeValue)
401 {
402 auto result = RunWslc(std::format(L"inspect --type invalid {}", DebianImage.NameAndTag()));
403 result.Verify({.Stdout = L"", .ExitCode = 1});
404 VERIFY_IS_TRUE(
405 result.StderrContainsSubstring(L"Invalid type value: invalid is not a recognized inspect type. Supported inspect "
406 L"types are: image, container, network, volume."));
407 }
408
409 WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidFormatError)
410 {
411 // Image name cannot be upper case, but root inspect command should skip this error and continue with the inspect instead of failing
412 auto result = RunWslc(L"inspect UPPER_CASE_INVALID_IMAGE");
413 result.Verify({.Stdout = L"[]\r\n", .Stderr = L"Object not found: UPPER_CASE_INVALID_IMAGE\r\n", .ExitCode = 1});
414 }
415
416 WSLC_TEST_METHOD(WSLCE2E_Inspect_SkipsInvalidTypeSpecifiedArgumentError)
417 {
418 // Container name cannot contain a colon, but root inspect command should skip this error and continue with the inspect instead of failing
419 auto result = RunWslc(std::format(L"inspect {}", InvalidImage.NameAndTag()));
420 result.Verify({.Stdout = L"[]\r\n", .Stderr = std::format(L"Object not found: {}\r\n", InvalidImage.NameAndTag()), .ExitCode = 1});
421 }
422
423 private:
424 const std::wstring WslcContainerName = L"wslc-inspect-test-container";
425 const TestImage& DebianImage = DebianTestImage();
426 const TestImage& InvalidImage = InvalidTestImage();
427 const std::wstring WslcVolumeName = L"wslc-inspect-test-volume";
428 const std::wstring WslcNetworkName = L"wslc-inspect-test-network";
429 const TestImage LabelInheritImage{L"wslc-e2e-inspect-inherit-labels", L"latest", L""};
430 };
431 } // namespace WSLCE2ETests