master
cpp 271 lines 10.7 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 #include "precomp.h"
4 #include "windows/Common.h"
5 #include "WSLCCLITestHelpers.h"
6
7 #include "ContainerService.h"
8
9 using namespace wsl::windows::wslc;
10 using namespace wsl::windows::wslc::services;
11 using namespace WSLCTestHelpers;
12 using namespace WEX::Logging;
13 using namespace WEX::Common;
14 using namespace WEX::TestExecution;
15
16 namespace WSLCCLIContainerCommandUnitTests {
17
18 class WSLCCLIContainerCommandUnitTests
19 {
20 WSLC_TEST_CLASS(WSLCCLIContainerCommandUnitTests)
21
22 TEST_CLASS_SETUP(TestClassSetup)
23 {
24 return true;
25 }
26
27 TEST_CLASS_CLEANUP(TestClassCleanup)
28 {
29 return true;
30 }
31
32 static std::wstring Truncated(const std::string& command)
33 {
34 return ContainerService::FormatCommand(command, true);
35 }
36
37 TEST_METHOD(FormatCommand_Empty_ReturnsEmptyQuotes)
38 {
39 VERIFY_ARE_EQUAL(std::wstring{LR"("")"}, Truncated(""));
40 }
41
42 TEST_METHOD(FormatCommand_ShortCommand_IsQuotedUnchanged)
43 {
44 VERIFY_ARE_EQUAL(std::wstring{LR"("sleep 3600")"}, Truncated("sleep 3600"));
45 }
46
47 TEST_METHOD(FormatCommand_ExactlyTwentyCharacters_IsNotShortened)
48 {
49 VERIFY_ARE_EQUAL(std::wstring{LR"("12345678901234567890")"}, Truncated("12345678901234567890"));
50 }
51
52 TEST_METHOD(FormatCommand_TwentyOneCharacters_KeepsNineteenAndAppendsEllipsis)
53 {
54 VERIFY_ARE_EQUAL(std::wstring{L"\"1234567890123456789\u2026\""}, Truncated("123456789012345678901"));
55 }
56
57 TEST_METHOD(FormatCommand_LongCommand_MatchesDockerOutput)
58 {
59 VERIFY_ARE_EQUAL(std::wstring{L"\"sh -c 'echo this is\u2026\""}, Truncated("sh -c 'echo this is a very long command that should be truncated by docker'"));
60 }
61
62 TEST_METHOD(FormatCommand_NoTruncate_KeepsFullCommand)
63 {
64 const std::string command = "sh -c 'echo this is a very long command that should be truncated by docker'";
65 VERIFY_ARE_EQUAL(
66 std::wstring{L"\"" + wsl::shared::string::MultiByteToWide(command) + L"\""}, ContainerService::FormatCommand(command, false));
67 }
68
69 TEST_METHOD(FormatCommand_EmbeddedQuotesAndBackslashes_AreEscaped)
70 {
71 // Raw string literals are avoided here: the compiler mangles them when the verify macro
72 // stringizes its arguments.
73 VERIFY_ARE_EQUAL(std::wstring{L"\"say \\\"hi\\\"\""}, Truncated("say \"hi\""));
74 VERIFY_ARE_EQUAL(std::wstring{L"\"c:\\\\temp\""}, Truncated("c:\\temp"));
75 }
76
77 TEST_METHOD(FormatCommand_NarrowMultiByteCharacters_CountedAsOneColumn)
78 {
79 const std::string accented = "\xC3\xA0";
80 std::string twenty;
81 for (int i = 0; i < 20; ++i)
82 {
83 twenty += accented;
84 }
85
86 VERIFY_ARE_EQUAL(std::wstring(20, L'\u00E0').insert(0, L"\"") + L"\"", Truncated(twenty));
87 VERIFY_ARE_EQUAL(std::wstring(19, L'\u00E0').insert(0, L"\"") + L"\u2026\"", Truncated(twenty + accented));
88 }
89
90 TEST_METHOD(FormatCommand_WideCharacters_CountedAsTwoColumns)
91 {
92 // Docker measures display columns, so an East Asian wide character consumes two of the twenty
93 // available columns and only ten of them fit.
94 const std::string wide = "\xE6\x97\xA5";
95 std::string ten;
96 for (int i = 0; i < 10; ++i)
97 {
98 ten += wide;
99 }
100
101 VERIFY_ARE_EQUAL(std::wstring(10, L'\u65E5').insert(0, L"\"") + L"\"", Truncated(ten));
102 VERIFY_ARE_EQUAL(std::wstring(9, L'\u65E5').insert(0, L"\"") + L"\u2026\"", Truncated(ten + wide));
103 }
104
105 TEST_METHOD(FormatCommand_MixedWidthCharacters_AreShortenedByColumn)
106 {
107 // Two narrow characters leave eighteen columns, so eight wide characters fit before the ellipsis.
108 const std::string wide = "\xE6\x97\xA5";
109 std::string command = "ab";
110 for (int i = 0; i < 10; ++i)
111 {
112 command += wide;
113 }
114
115 VERIFY_ARE_EQUAL(std::wstring{L"\"ab"} + std::wstring(8, L'\u65E5') + L"\u2026\"", Truncated(command));
116 }
117
118 TEST_METHOD(FormatCommand_SurrogatePairs_AreNotSplit)
119 {
120 // Emoji are wide and encoded as surrogate pairs, so a shortened value has to stop on a code point
121 // boundary as well as a column boundary.
122 const std::string emoji = "\xF0\x9F\x98\x80";
123 std::string ten;
124 std::wstring expected;
125 for (int i = 0; i < 10; ++i)
126 {
127 ten += emoji;
128 expected += L"\U0001F600";
129 }
130
131 VERIFY_ARE_EQUAL(L"\"" + expected + L"\"", Truncated(ten));
132 VERIFY_ARE_EQUAL(L"\"" + expected.substr(0, 18) + L"\u2026\"", Truncated(ten + emoji));
133 }
134
135 TEST_METHOD(FormatCommand_ControlCharacters_AreEscaped)
136 {
137 // Docker quotes this field with Go's strconv.Quote, which renders control characters as escape sequences
138 // rather than emitting them raw and breaking the table row.
139 VERIFY_ARE_EQUAL(std::wstring{L"\"line1\\nline2\""}, Truncated("line1\nline2"));
140 VERIFY_ARE_EQUAL(std::wstring{L"\"col1\\tcol2\""}, Truncated("col1\tcol2"));
141 VERIFY_ARE_EQUAL(std::wstring{L"\"a\\rb\""}, Truncated("a\rb"));
142 VERIFY_ARE_EQUAL(std::wstring{L"\"a\\vb\""}, Truncated("a\vb"));
143 VERIFY_ARE_EQUAL(std::wstring{L"\"a\\fb\""}, Truncated("a\fb"));
144 VERIFY_ARE_EQUAL(std::wstring{L"\"a\\bb\""}, Truncated("a\bb"));
145 VERIFY_ARE_EQUAL(std::wstring{L"\"a\\ab\""}, Truncated("a\ab"));
146 }
147
148 TEST_METHOD(FormatCommand_NonPrintableCharacters_AreEscapedAsHex)
149 {
150 // Control characters without a dedicated escape use \x, matching strconv.Quote.
151 VERIFY_ARE_EQUAL(
152 std::wstring{L"\"a\\x1bb\""},
153 Truncated("a\x1b"
154 "b"));
155 VERIFY_ARE_EQUAL(
156 std::wstring{L"\"a\\x7fb\""},
157 Truncated("a\x7f"
158 "b"));
159 }
160
161 TEST_METHOD(FormatCommand_PrintableUnicode_IsNotEscaped)
162 {
163 // Letters and symbols stay verbatim, so only genuinely unprintable values are expanded.
164 VERIFY_ARE_EQUAL(std::wstring{L"\"caf\u00E9\""}, Truncated("caf\xC3\xA9"));
165 }
166
167 TEST_METHOD(FormatCommand_EscapesAreCountedAfterTruncation)
168 {
169 // Docker truncates before quoting, so escape expansion does not consume the display budget and the
170 // quoted result is wider than the limit.
171 VERIFY_ARE_EQUAL(std::wstring{L"\"\\n\\n\\n\\n\\n\""}, Truncated("\n\n\n\n\n"));
172 }
173
174 TEST_METHOD(FormatMounts_LongNames_AreShortenedIndependently)
175 {
176 // Docker shortens every mount name to fifteen columns rather than the list as a whole
177 // (ContainerContext.Mounts in cli/command/formatter/container.go).
178 const auto mounts = "/var/lib/docker/volumes/data,logs,/mnt/c/users/test/source";
179 VERIFY_ARE_EQUAL(std::wstring{L"/var/lib/docke\u2026,logs,/mnt/c/users/t\u2026"}, ContainerService::FormatMounts(mounts, true));
180 VERIFY_ARE_EQUAL(wsl::shared::string::MultiByteToWide(mounts), ContainerService::FormatMounts(mounts, false));
181 }
182
183 TEST_METHOD(FormatMounts_ShortNames_AreUnchanged)
184 {
185 VERIFY_ARE_EQUAL(std::wstring{L""}, ContainerService::FormatMounts("", true));
186 VERIFY_ARE_EQUAL(std::wstring{L"data-volume"}, ContainerService::FormatMounts("data-volume", true));
187 VERIFY_ARE_EQUAL(std::wstring{L"123456789012345"}, ContainerService::FormatMounts("123456789012345", true));
188 VERIFY_ARE_EQUAL(std::wstring{L"12345678901234\u2026"}, ContainerService::FormatMounts("1234567890123456", true));
189 }
190
191 TEST_METHOD(FormatMounts_WideCharacters_CountedAsTwoColumns)
192 {
193 // Seven wide characters occupy fourteen columns, so an eighth exceeds the fifteen column budget.
194 std::string wide;
195 for (int i = 0; i < 8; ++i)
196 {
197 wide += "\xE6\x97\xA5";
198 }
199
200 VERIFY_ARE_EQUAL(std::wstring(7, L'\u65E5') + L"\u2026", ContainerService::FormatMounts(wide, true));
201 }
202
203 TEST_METHOD(FormatStatus_RuntimeStatus_IsPreferred)
204 {
205 VERIFY_ARE_EQUAL(std::wstring{L"Up 5 minutes"}, ContainerService::FormatStatus("Up 5 minutes", WslcContainerStateRunning, 0));
206 }
207
208 TEST_METHOD(FormatStatus_EmptyRuntimeStatus_FallsBackToState)
209 {
210 VERIFY_ARE_EQUAL(std::wstring{L"created"}, ContainerService::FormatStatus("", WslcContainerStateCreated, 0));
211 }
212
213 // The fallback is built locally, so json has to render it in invariant English rather than in the
214 // machine's display language.
215 TEST_METHOD(FormatStatus_EmptyRuntimeStatus_JsonFallbackIsInvariant)
216 {
217 const auto twoHoursAgo = static_cast<LONGLONG>(std::time(nullptr)) - (2 * 60 * 60);
218
219 VERIFY_ARE_EQUAL(
220 std::wstring{L"exited 2 hours ago"},
221 ContainerService::FormatStatus("", WslcContainerStateExited, twoHoursAgo, models::FormatType::Json));
222 VERIFY_ARE_EQUAL(std::wstring{L"created"}, ContainerService::FormatStatus("", WslcContainerStateCreated, 0, models::FormatType::Json));
223 }
224
225 // A status supplied by the runtime is already invariant, so it is passed through unchanged for
226 // both formats.
227 TEST_METHOD(FormatStatus_RuntimeStatus_IsFormatIndependent)
228 {
229 VERIFY_ARE_EQUAL(
230 std::wstring{L"Up 5 minutes"},
231 ContainerService::FormatStatus("Up 5 minutes", WslcContainerStateRunning, 0, models::FormatType::Json));
232 VERIFY_ARE_EQUAL(
233 std::wstring{L"Up 5 minutes"},
234 ContainerService::FormatStatus("Up 5 minutes", WslcContainerStateRunning, 0, models::FormatType::Table));
235 }
236
237 TEST_METHOD(FormatHealthStatus_Healthy_IsExtracted)
238 {
239 VERIFY_ARE_EQUAL(std::string{"healthy"}, ContainerService::FormatHealthStatus("Up 2 minutes (healthy)"));
240 }
241
242 TEST_METHOD(FormatHealthStatus_Unhealthy_IsExtracted)
243 {
244 VERIFY_ARE_EQUAL(std::string{"unhealthy"}, ContainerService::FormatHealthStatus("Up 2 minutes (unhealthy)"));
245 }
246
247 TEST_METHOD(FormatHealthStatus_Starting_DropsHealthPrefix)
248 {
249 VERIFY_ARE_EQUAL(std::string{"starting"}, ContainerService::FormatHealthStatus("Up 2 seconds (health: starting)"));
250 }
251
252 TEST_METHOD(FormatHealthStatus_NoHealthCheck_IsEmpty)
253 {
254 VERIFY_ARE_EQUAL(std::string{}, ContainerService::FormatHealthStatus("Up 2 minutes"));
255 VERIFY_ARE_EQUAL(std::string{}, ContainerService::FormatHealthStatus(""));
256 VERIFY_ARE_EQUAL(std::string{}, ContainerService::FormatHealthStatus("Created"));
257 }
258
259 TEST_METHOD(FormatHealthStatus_NoneIsNotReported)
260 {
261 VERIFY_ARE_EQUAL(std::string{}, ContainerService::FormatHealthStatus("Up 2 minutes (none)"));
262 }
263
264 TEST_METHOD(FormatHealthStatus_UnrelatedParentheses_AreIgnored)
265 {
266 VERIFY_ARE_EQUAL(std::string{}, ContainerService::FormatHealthStatus("Exited (0) 8 days ago"));
267 VERIFY_ARE_EQUAL(std::string{}, ContainerService::FormatHealthStatus("Up 2 minutes (Paused)"));
268 }
269 };
270
271 } // namespace WSLCCLIContainerCommandUnitTests