master
h 335 lines 11.5 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EHelpers.h
8
9 Abstract:
10
11 This file contains helper functions for WSLCE2E tests.
12 --*/
13
14 #pragma once
15
16 #include "WSLCExecutor.h"
17 #include <docker_schema.h>
18 #include <chrono>
19 #include <wslc_schema.h>
20 #include <ContainerModel.h>
21 #include <WSLCContainerLauncher.h>
22 #include "VTSupport.h"
23
24 namespace WSLCE2ETests {
25
26 namespace string = wsl::shared::string;
27
28 // VT sequence constants and helpers for TTY testing.
29 // Sequences are sourced from wsl::windows::common::vt (VTSupport.h).
30 namespace VT {
31 using namespace wsl::windows::common::vt;
32
33 inline const auto& B_START = Cursor::BracketedPasteOn;
34 inline const auto& B_END = Cursor::BracketedPasteOff;
35 inline const auto& RESET = Format::Default;
36 inline const auto& ERASE_LINE = Erase::LineForward;
37 inline const Sequence CR{L"\r"};
38
39 // The shell PS1 uses SGR 1;31 (bold + red) in a single sequence.
40 // Sgr({1, 31}) produces L"\x1b[1;31m" to match exactly.
41 inline const ConstructedSequence RED = Sgr({1, 31});
42
43 // Prompt pattern used in WSLC TTY sessions.
44 inline const std::string SESSION_PROMPT =
45 wsl::shared::string::WideToMultiByte(B_START + RED + L"root@ [ " + RESET + L"/" + RED + L" ]# ");
46
47 inline std::string BuildContainerPrompt(const std::string& prompt, bool withBracketedPaste = true)
48 {
49 if (withBracketedPaste)
50 {
51 return wsl::shared::string::WideToMultiByte(std::format(L"{}", B_START)) + prompt;
52 }
53 return prompt;
54 }
55
56 inline std::string BuildContainerAttachPrompt(const std::string& prompt)
57 {
58 return wsl::shared::string::WideToMultiByte(std::format(L"{}{}{}", CR, ERASE_LINE, CR)) + prompt;
59 }
60 } // namespace VT
61
62 // The shape emitted by "network list --format json"; every value is reported as a string.
63 struct NetworkListOutput
64 {
65 std::string CreatedAt;
66 std::string Driver;
67 std::string ID;
68 std::string IPv4;
69 std::string IPv6;
70 std::string Internal;
71 std::string Labels;
72 std::string Name;
73 std::string Scope;
74
75 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(NetworkListOutput, CreatedAt, Driver, ID, IPv4, IPv6, Internal, Labels, Name, Scope);
76 };
77
78 struct VolumeListOutput
79 {
80 std::string Availability;
81 std::string Driver;
82 std::string Group;
83 std::string Labels;
84 std::string Links;
85 std::string Mountpoint;
86 std::string Name;
87 std::string Scope;
88 std::string Size;
89 std::string Status;
90
91 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(VolumeListOutput, Availability, Driver, Group, Labels, Links, Mountpoint, Name, Scope, Size, Status);
92 };
93
94 struct TestImage
95 {
96 std::wstring Name;
97 std::wstring Tag;
98 std::filesystem::path Path;
99 std::wstring NameAndTag() const
100 {
101 return std::format(L"{}:{}", Name, Tag);
102 }
103 };
104
105 const TestImage& AlpineTestImage();
106 const TestImage& DebianTestImage();
107 const TestImage& HelloWorldTestImage();
108 const TestImage& PythonTestImage();
109 const TestImage& InvalidTestImage();
110
111 struct TestSession
112 {
113 static TestSession Create(const std::wstring& displayName, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone);
114
115 TestSession(std::wstring name, std::filesystem::path storagePath, wil::com_ptr<IWSLCSession> session) :
116 m_name(std::move(name)), m_storagePath(std::move(storagePath)), m_session(std::move(session))
117 {
118 }
119
120 ~TestSession();
121
122 NON_COPYABLE(TestSession);
123 NON_MOVABLE(TestSession);
124
125 const std::wstring& Name() const
126 {
127 return m_name;
128 }
129
130 const std::filesystem::path& StoragePath() const
131 {
132 return m_storagePath;
133 }
134
135 IWSLCSession& Session() const
136 {
137 return *m_session;
138 }
139
140 private:
141 std::wstring m_name;
142 std::filesystem::path m_storagePath;
143 wil::com_ptr<IWSLCSession> m_session;
144 };
145
146 void VerifyContainerIsListed(const std::wstring& containerName, const std::wstring& status, const std::wstring& sessionName = L"");
147 void VerifyImageIsUsed(const TestImage& image);
148 void VerifyImageIsNotUsed(const TestImage& image);
149 void VerifyImageIsListed(const TestImage& image);
150 void VerifyVolumeIsListed(const std::wstring& volumeName);
151 void VerifyVolumeIsNotListed(const std::wstring& volumeName);
152 void VerifyNetworkIsListed(const std::wstring& networkName);
153 void VerifyNetworkIsNotListed(const std::wstring& networkName);
154
155 std::string GetHashId(const std::string& id, bool fullId = false);
156 wsl::windows::common::wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName);
157 wsl::windows::common::wslc_schema::InspectImage InspectImage(const std::wstring& imageName);
158 wsl::windows::common::wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName);
159 wsl::windows::common::wslc_schema::Network InspectNetwork(const std::wstring& networkName);
160 std::vector<wsl::windows::wslc::models::ContainerOutputInformation> ListAllContainers();
161
162 void EnsureContainerDoesNotExist(const std::wstring& containerName);
163 void DeleteImagesWithRepositoryPrefix(const std::wstring& repositoryPrefix);
164 void EnsureImageContainersAreDeleted(const TestImage& image);
165 void EnsureNoUntaggedImages();
166 void EnsureSessionIsTerminated(const std::wstring& sessionName = L"");
167 void EnsureVolumeDoesNotExist(const std::wstring& volumeName);
168 void EnsureNetworkDoesNotExist(const std::wstring& networkName);
169
170 void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std::string>& envVariableLines);
171 void WriteTestFileContent(const std::filesystem::path& filePath, const std::string& content);
172
173 // Sets up a clean test directory and returns a scope_exit to remove it.
174 inline auto SetupTestDirectory(const std::filesystem::path& directory)
175 {
176 std::filesystem::remove_all(directory);
177 std::filesystem::create_directories(directory);
178
179 return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [directory]() {
180 std::error_code removeError;
181 std::filesystem::remove_all(directory, removeError);
182 });
183 }
184
185 std::wstring GetPythonHttpServerScript(uint16_t port);
186 std::wstring GetPythonUdpEchoServerScript(uint16_t port);
187
188 std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, const std::string& expectedReply, int family = AF_INET);
189
190 void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout = std::chrono::seconds(60));
191
192 wsl::windows::common::wslc_schema::Health WaitForContainerHealth(
193 const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout = std::chrono::seconds(120));
194
195 // Default timeout of 0 will execute once.
196 template <typename IntervalRep, typename IntervalPeriod, typename TimeoutRep, typename TimeoutPeriod>
197 void VerifyContainerIsNotListed(
198 const std::wstring& containerNameOrId,
199 std::chrono::duration<IntervalRep, IntervalPeriod> retryInterval,
200 std::chrono::duration<TimeoutRep, TimeoutPeriod> timeout)
201 {
202 try
203 {
204 wsl::shared::retry::RetryWithTimeout<void>(
205 [&containerNameOrId]() {
206 auto result = RunWslc(L"container list --no-trunc --all");
207 result.Verify({.Stderr = L"", .ExitCode = 0});
208
209 auto outputLines = result.GetStdoutLines();
210 for (const auto& line : outputLines)
211 {
212 if (line.find(containerNameOrId) != std::wstring::npos)
213 {
214 THROW_HR(E_FAIL);
215 }
216 }
217 },
218 retryInterval,
219 timeout);
220 }
221 catch (...)
222 {
223 HRESULT hr = wil::ResultFromCaughtException();
224 const bool hasTimeout = std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count() > 0;
225 const std::wstring message =
226 hr == E_FAIL
227 ? std::format(L"Container '{}' found in container list output{}", containerNameOrId, hasTimeout ? L" after timeout" : L" but it should not be listed")
228 : std::format(
229 L"Unexpected error while verifying container '{}' is not listed: 0x{:08X}",
230 containerNameOrId,
231 static_cast<unsigned int>(hr));
232 VERIFY_FAIL(message.c_str());
233 }
234 }
235
236 inline void VerifyContainerIsNotListed(const std::wstring& containerNameOrId)
237 {
238 VerifyContainerIsNotListed(containerNameOrId, std::chrono::milliseconds(0), std::chrono::milliseconds(0));
239 }
240
241 wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession();
242
243 void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows);
244
245 // Waits for a substring to appear in the session's pseudo console output.
246 void WaitForPseudoConsoleOutput(
247 const WSLCInteractiveSession& session, const std::string& expected, std::chrono::seconds timeout = std::chrono::seconds(60));
248
249 // Starts a local registry container using the COM API and returns the running container (holds it
250 // alive) plus the registry address. Host network for plain http, bridge network for tls enabled.
251 std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
252 IWSLCSession& session,
253 const std::string& username = "",
254 const std::string& password = "",
255 USHORT port = 5000,
256 const std::wstring& tlsCertDir = L"");
257
258 // Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest").
259 std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress);
260
261 // Verifies "--format json" output was emitted as a single compact line and returns the parsed document.
262 inline nlohmann::json VerifyCompactJsonOutput(const WSLCExecutionResult& result)
263 {
264 VERIFY_IS_TRUE(result.Stdout.has_value());
265
266 const auto lines = result.GetStdoutLines();
267 VERIFY_ARE_EQUAL(1u, lines.size(), L"'--format json' output must be a single line");
268
269 return nlohmann::json::parse(wsl::shared::string::WideToMultiByte(lines[0]));
270 }
271
272 // Parses list output emitted as one compact JSON object per line.
273 inline std::vector<nlohmann::json> ParseNdjsonOutput(const WSLCExecutionResult& result)
274 {
275 VERIFY_IS_TRUE(result.Stdout.has_value());
276
277 std::vector<nlohmann::json> entries;
278 for (const auto& line : result.GetStdoutLines())
279 {
280 if (line.empty())
281 {
282 continue;
283 }
284
285 auto entry = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(line));
286 if (!entry.is_object())
287 {
288 VERIFY_FAIL(std::format(L"Line is not a JSON object: '{}'", line).c_str());
289 }
290
291 entries.push_back(std::move(entry));
292 }
293
294 return entries;
295 }
296
297 // Typed form of ParseNdjsonOutput() that deserializes each line into T.
298 template <typename T>
299 std::vector<T> ParseNdjsonOutputAs(const WSLCExecutionResult& result)
300 {
301 std::vector<T> entries;
302 for (const auto& entry : ParseNdjsonOutput(result))
303 {
304 entries.push_back(entry.get<T>());
305 }
306
307 return entries;
308 }
309
310 // Verifies that a string is a valid hex ID output.
311 // truncated=true expects 12 hex chars, truncated=false expects 64 hex chars.
312 inline void VerifyIdOutput(const std::wstring& id, bool truncated)
313 {
314 constexpr size_t c_truncatedLength = 12;
315 constexpr size_t c_fullLength = 64;
316
317 const size_t expectedLength = truncated ? c_truncatedLength : c_fullLength;
318
319 VERIFY_ARE_EQUAL(id.size(), expectedLength);
320
321 bool allHex = true;
322 for (size_t i = 0; i < expectedLength; i++)
323 {
324 const auto ch = id[i];
325 if (!((ch >= L'0' && ch <= L'9') || (ch >= L'a' && ch <= L'f')))
326 {
327 allHex = false;
328 break;
329 }
330 }
331
332 VERIFY_IS_TRUE(allHex, WEX::Common::String().Format(L"ID is not a valid hex string: '%ls'", id.c_str()));
333 }
334
335 } // namespace WSLCE2ETests