master
cpp 380 lines 15.1 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 #include "precomp.h"
4 #include "Common.h"
5 #include "string.hpp"
6
7 using wsl::windows::common::string::c_reclaimedSpacePrecision;
8 using wsl::windows::common::string::Ellipsis;
9 using wsl::windows::common::string::FormatHumanReadableSize;
10 using wsl::windows::common::string::ParseStorageSize;
11 using wsl::windows::common::string::StorageSizeUnit;
12
13 namespace {
14
15 struct StorageSizeFormatCase
16 {
17 uint64_t Bytes;
18 StorageSizeUnit Unit;
19 uint32_t Precision;
20 std::wstring Expected;
21 };
22
23 struct StorageSizeTextRoundTripCase
24 {
25 std::wstring Text;
26 StorageSizeUnit Unit;
27 uint32_t Precision;
28 };
29
30 void VerifyDockerStorageSize(const std::string& Input, StorageSizeUnit Unit, std::optional<uint64_t> Expected)
31 {
32 const auto wideInput = wsl::shared::string::MultiByteToWide(Input);
33 VERIFY_ARE_EQUAL(Expected, ParseStorageSize(wideInput, Unit));
34 }
35
36 std::vector<std::string> DockerSuffixes(char Unit)
37 {
38 const auto UpperUnit = static_cast<char>(std::toupper(static_cast<unsigned char>(Unit)));
39 return {
40 {Unit},
41 {UpperUnit},
42 {Unit, 'b'},
43 {Unit, 'B'},
44 {UpperUnit, 'b'},
45 {UpperUnit, 'B'},
46 {Unit, 'i', 'b'},
47 {Unit, 'i', 'B'},
48 {Unit, 'I', 'b'},
49 {Unit, 'I', 'B'},
50 {UpperUnit, 'i', 'b'},
51 {UpperUnit, 'i', 'B'},
52 {UpperUnit, 'I', 'b'},
53 {UpperUnit, 'I', 'B'},
54 };
55 }
56
57 void VerifyDockerStorageUnits(StorageSizeUnit Unit, uint64_t Base)
58 {
59 uint64_t Factor = Base;
60 for (const auto UnitName : {'k', 'm', 'g', 't', 'p'})
61 {
62 for (const auto& Suffix : DockerSuffixes(UnitName))
63 {
64 VerifyDockerStorageSize("32" + Suffix, Unit, 32 * Factor);
65 }
66
67 Factor *= Base;
68 }
69 }
70
71 } // namespace
72
73 namespace StringUnitTests {
74 class StringUnitTests
75 {
76 WSL_TEST_CLASS(StringUnitTests)
77
78 TEST_METHOD(FormatUtf8StringAsWideString)
79 {
80 const std::string input{"安装依赖"};
81 const auto expected = wsl::shared::string::MultiByteToWide(input);
82
83 VERIFY_ARE_EQUAL(expected, std::format(L"{}", input));
84 }
85
86 TEST_METHOD(ParseMemorySize_LegacyForms)
87 {
88 const std::vector<std::pair<LPCSTR, std::optional<uint64_t>>> TestCases{
89 {"0", 0},
90 {"1", 1},
91 {" 1", 1},
92 {"1B", 1},
93 {"1K", 1024},
94 {"1KB", 1024},
95 {"2M", 2 * 1024 * 1024},
96 {"100MB", 100 * 1024 * 1024},
97 {"9G", 9 * 1024ULL * 1024ULL * 1024ULL},
98 {"44GB", 44 * 1024ULL * 1024ULL * 1024ULL},
99 {"1TB", 1ULL << 40},
100 {"2T", 2ULL << 40},
101 {"1 B", std::nullopt},
102 {nullptr, std::nullopt},
103 {"", std::nullopt},
104 {"foo", std::nullopt}};
105
106 for (const auto& [Input, Expected] : TestCases)
107 {
108 VERIFY_ARE_EQUAL(Expected, wsl::shared::string::ParseMemorySize(Input));
109
110 const auto wideInput = wsl::shared::string::MultiByteToWide(Input);
111 VERIFY_ARE_EQUAL(Expected, wsl::shared::string::ParseMemorySize(wideInput.c_str()));
112 }
113 }
114
115 TEST_METHOD(ParseStorageSize_DockerDecimalUnits)
116 {
117 VerifyDockerStorageUnits(StorageSizeUnit::Decimal, 1000);
118 }
119
120 TEST_METHOD(ParseStorageSize_DockerBinaryUnits)
121 {
122 VerifyDockerStorageUnits(StorageSizeUnit::Binary, 1024);
123 }
124
125 TEST_METHOD(ParseStorageSize_DockerNumericForms)
126 {
127 for (const auto Unit : {StorageSizeUnit::Decimal, StorageSizeUnit::Binary})
128 {
129 VerifyDockerStorageSize("0", Unit, 0);
130 VerifyDockerStorageSize("0b", Unit, 0);
131 VerifyDockerStorageSize("0B", Unit, 0);
132 VerifyDockerStorageSize("0 B", Unit, 0);
133 VerifyDockerStorageSize("32", Unit, 32);
134 VerifyDockerStorageSize("32b", Unit, 32);
135 VerifyDockerStorageSize("32B", Unit, 32);
136 VerifyDockerStorageSize("32.5 B", Unit, 32);
137 VerifyDockerStorageSize("0.", Unit, 0);
138 VerifyDockerStorageSize("0. ", Unit, 0);
139 VerifyDockerStorageSize("0.b", Unit, 0);
140 VerifyDockerStorageSize("0.B", Unit, 0);
141 VerifyDockerStorageSize("-0", Unit, 0);
142 VerifyDockerStorageSize("-0b", Unit, 0);
143 VerifyDockerStorageSize("-0B", Unit, 0);
144 VerifyDockerStorageSize("-0 b", Unit, 0);
145 VerifyDockerStorageSize("-0 B", Unit, 0);
146 VerifyDockerStorageSize("+32K", Unit, 32 * (Unit == StorageSizeUnit::Decimal ? 1000 : 1024));
147 VerifyDockerStorageSize("1e3K", Unit, 1000 * (Unit == StorageSizeUnit::Decimal ? 1000 : 1024));
148 VerifyDockerStorageSize("32.", Unit, 32);
149 VerifyDockerStorageSize("32.b", Unit, 32);
150 VerifyDockerStorageSize("32.B", Unit, 32);
151 VerifyDockerStorageSize("32. b", Unit, 32);
152 VerifyDockerStorageSize("32. B", Unit, 32);
153 VerifyDockerStorageSize("9007199254740991", Unit, 9'007'199'254'740'991);
154 VerifyDockerStorageSize("9007199254740992", Unit, 9'007'199'254'740'992);
155 VerifyDockerStorageSize("9007199254740993", Unit, 9'007'199'254'740'993);
156 VerifyDockerStorageSize("9223372036854775806", Unit, 9'223'372'036'854'775'806);
157 VerifyDockerStorageSize("9223372036854775807", Unit, 9'223'372'036'854'775'807);
158 VerifyDockerStorageSize("9223372036854775808", Unit, 9'223'372'036'854'775'808ULL);
159 VerifyDockerStorageSize("18446744073709551615", Unit, std::numeric_limits<uint64_t>::max());
160 }
161
162 VerifyDockerStorageSize("32.5kB", StorageSizeUnit::Decimal, 32'500);
163 VerifyDockerStorageSize("32.5 kB", StorageSizeUnit::Decimal, 32'500);
164 VerifyDockerStorageSize("0.3 K", StorageSizeUnit::Decimal, 300);
165 VerifyDockerStorageSize(".3kB", StorageSizeUnit::Decimal, 300);
166 VerifyDockerStorageSize("32.3 mb", StorageSizeUnit::Binary, 33'869'004);
167 VerifyDockerStorageSize("0.3MB", StorageSizeUnit::Binary, 314'572);
168 VerifyDockerStorageSize("18446744073709551K", StorageSizeUnit::Decimal, 18'446'744'073'709'551'000ULL);
169 VerifyDockerStorageSize("18446744073709552K", StorageSizeUnit::Decimal, std::nullopt);
170 VerifyDockerStorageSize("18014398509481983K", StorageSizeUnit::Binary, 18'446'744'073'709'550'592ULL);
171 VerifyDockerStorageSize("18014398509481984K", StorageSizeUnit::Binary, std::nullopt);
172 }
173
174 TEST_METHOD(ParseStorageSize_DockerInvalidForms)
175 {
176 const std::vector<std::string> InvalidSizes{
177 "", "hello",
178 ".", ". ",
179 " ", " ",
180 " .", " . ",
181 " 0", " 0b",
182 " 0B", " 0 B",
183 "0b ", "0B ",
184 "0 B ", "-32",
185 "-32b", "-32B",
186 "-32 b", "-32 B",
187 "32b.", "32B.",
188 "32 b.", "32 B.",
189 "32 bb", "32 BB",
190 "32 b b", "32 B B",
191 "32 b", "32 B",
192 " 32 ", "32m b",
193 "32bm", "1E",
194 "1EB", "1EiB",
195 "1e309", "18446744073709551616",
196 };
197
198 for (const auto Unit : {StorageSizeUnit::Decimal, StorageSizeUnit::Binary})
199 {
200 for (const auto& Input : InvalidSizes)
201 {
202 VerifyDockerStorageSize(Input, Unit, std::nullopt);
203 }
204 }
205 }
206
207 // Memory sizes are rendered in binary units with four significant digits and no space, matching
208 // docker's units.BytesSize.
209 TEST_METHOD(FormatHumanReadableSize_SupportsBinaryUnits)
210 {
211 const std::vector<StorageSizeFormatCase> TestCases{
212 {0, StorageSizeUnit::Binary, 4, L"0B"},
213 {1'023, StorageSizeUnit::Binary, 4, L"1023B"},
214 {1'024, StorageSizeUnit::Binary, 4, L"1KiB"},
215 {1'536, StorageSizeUnit::Binary, 4, L"1.5KiB"},
216 {44'000, StorageSizeUnit::Binary, 4, L"42.97KiB"},
217 {1'610'612'736, StorageSizeUnit::Binary, 4, L"1.5GiB"},
218 {8ULL << 30, StorageSizeUnit::Binary, 4, L"8GiB"},
219 {1ULL << 40, StorageSizeUnit::Binary, 4, L"1TiB"},
220 {1ULL << 50, StorageSizeUnit::Binary, 4, L"1PiB"},
221 {1'536, StorageSizeUnit::Binary, 3, L"1.5KiB"},
222 {1'000, StorageSizeUnit::Decimal, 4, L"1kB"},
223 {1'610'612'736, StorageSizeUnit::Decimal, 4, L"1.611GB"},
224 };
225
226 for (const auto& TestCase : TestCases)
227 {
228 VERIFY_ARE_EQUAL(TestCase.Expected, FormatHumanReadableSize(TestCase.Bytes, TestCase.Precision, TestCase.Unit));
229 }
230 }
231
232 // Image sizes are rendered with three significant digits, base 1000, no space, and "kB" rather
233 // than "KB".
234 TEST_METHOD(FormatHumanReadableSize_MatchesImageSizePrecision)
235 {
236 const std::vector<std::pair<uint64_t, std::wstring>> TestCases{
237 {0, L"0B"},
238 {999, L"999B"},
239 {1'000, L"1kB"},
240 {1'500, L"1.5kB"},
241 {7'050'000, L"7.05MB"},
242 {119'856'765, L"120MB"},
243 {1'090'000'000, L"1.09GB"},
244 {1'000'000'000'000ULL, L"1TB"},
245 };
246
247 for (const auto& [bytes, expected] : TestCases)
248 {
249 VERIFY_ARE_EQUAL(expected, FormatHumanReadableSize(bytes));
250 }
251
252 // Three significant digits switch to exponent form just below the next unit, matching Go's %g.
253 VERIFY_ARE_EQUAL(std::wstring{L"1e+03MB"}, FormatHumanReadableSize(999'900'000));
254 }
255
256 TEST_METHOD(FormatHumanReadableSize_SupportsReclaimedSpacePrecision)
257 {
258 const std::vector<std::pair<uint64_t, std::wstring>> TestCases{
259 {0, L"0B"},
260 {999, L"999B"},
261 {12'288, L"12.29kB"},
262 {119'856'765, L"119.9MB"},
263 {1'090'000'000, L"1.09GB"},
264 };
265
266 for (const auto& [bytes, expected] : TestCases)
267 {
268 VERIFY_ARE_EQUAL(expected, FormatHumanReadableSize(bytes, c_reclaimedSpacePrecision));
269 }
270 }
271
272 // Docker shortens display values with formatter.Ellipsis, which measures terminal columns rather than
273 // characters so East Asian wide and fullwidth code points count double.
274 TEST_METHOD(Ellipsis_NarrowCharacters_AreCountedAsOneColumn)
275 {
276 VERIFY_ARE_EQUAL(std::wstring{L""}, Ellipsis(L"", 20));
277 VERIFY_ARE_EQUAL(std::wstring{L"sleep 3600"}, Ellipsis(L"sleep 3600", 20));
278 VERIFY_ARE_EQUAL(std::wstring{L"12345678901234567890"}, Ellipsis(L"12345678901234567890", 20));
279 VERIFY_ARE_EQUAL(std::wstring{L"1234567890123456789\u2026"}, Ellipsis(L"123456789012345678901", 20));
280 VERIFY_ARE_EQUAL(std::wstring(19, L'\u00E0') + L"\u2026", Ellipsis(std::wstring(21, L'\u00E0'), 20));
281 }
282
283 TEST_METHOD(Ellipsis_WideCharacters_AreCountedAsTwoColumns)
284 {
285 // Ten wide characters fill the twenty columns exactly, so an eleventh forces the value to be shortened
286 // to the nine characters that leave room for the ellipsis.
287 VERIFY_ARE_EQUAL(std::wstring(10, L'\u65E5'), Ellipsis(std::wstring(10, L'\u65E5'), 20));
288 VERIFY_ARE_EQUAL(std::wstring(9, L'\u65E5') + L"\u2026", Ellipsis(std::wstring(11, L'\u65E5'), 20));
289 VERIFY_ARE_EQUAL(std::wstring(9, L'\uFF21') + L"\u2026", Ellipsis(std::wstring(11, L'\uFF21'), 20));
290 VERIFY_ARE_EQUAL(std::wstring{L"ab"} + std::wstring(8, L'\u65E5') + L"\u2026", Ellipsis(L"ab" + std::wstring(10, L'\u65E5'), 20));
291 }
292
293 TEST_METHOD(Ellipsis_SurrogatePairs_AreNotSplit)
294 {
295 // Emoji are wide and encoded as surrogate pairs, so both the column count and the code unit boundary
296 // have to be honored.
297 const std::wstring emoji{L"\U0001F600"};
298 std::wstring ten;
299 for (size_t index = 0; index < 10; ++index)
300 {
301 ten += emoji;
302 }
303
304 VERIFY_ARE_EQUAL(ten, Ellipsis(ten, 20));
305 VERIFY_ARE_EQUAL(ten.substr(0, 18) + L"\u2026", Ellipsis(ten + emoji, 20));
306 }
307
308 TEST_METHOD(Ellipsis_SmallWidths_MatchDocker)
309 {
310 VERIFY_ARE_EQUAL(std::wstring{L""}, Ellipsis(L"abc", 0));
311
312 // A width of one has no room for both content and an ellipsis, so the leading code point is kept even
313 // when it is wider than the limit.
314 VERIFY_ARE_EQUAL(std::wstring{L"a"}, Ellipsis(L"abc", 1));
315 VERIFY_ARE_EQUAL(std::wstring{L"\u65E5"}, Ellipsis(L"\u65E5\u65E5", 1));
316 VERIFY_ARE_EQUAL(std::wstring{L"\U0001F600"}, Ellipsis(L"\U0001F600\U0001F600", 1));
317
318 // A leading wide character leaves no room for the ellipsis at a width of two, and docker returns the
319 // value untouched in that case.
320 VERIFY_ARE_EQUAL(std::wstring{L"\u65E5\u65E5"}, Ellipsis(L"\u65E5\u65E5", 2));
321 VERIFY_ARE_EQUAL(std::wstring{L"a\u2026"}, Ellipsis(L"abc", 2));
322 VERIFY_ARE_EQUAL(std::wstring{L"\u65E5\u2026"}, Ellipsis(L"\u65E5\u65E5", 3));
323 }
324
325 TEST_METHOD(StorageSize_BytesToTextRoundTrips)
326 {
327 // The parser accepts suffixes up to peta, matching docker's unit map, so the round trip is
328 // only defined below one exabyte.
329 const auto VerifyRoundTrip = [](uint64_t Bytes, StorageSizeUnit Unit, uint32_t Precision) {
330 const auto text = FormatHumanReadableSize(Bytes, Precision, Unit);
331 VERIFY_ARE_EQUAL(std::optional<uint64_t>{Bytes}, ParseStorageSize(text, Unit));
332 };
333
334 VerifyRoundTrip(0, StorageSizeUnit::Decimal, 3);
335 VerifyRoundTrip(32, StorageSizeUnit::Decimal, 3);
336 VerifyRoundTrip(1'500, StorageSizeUnit::Decimal, 3);
337 VerifyRoundTrip(1'536, StorageSizeUnit::Binary, 3);
338 VerifyRoundTrip(1'250'000'000'000ULL, StorageSizeUnit::Decimal, 4);
339 VerifyRoundTrip(1ULL << 50, StorageSizeUnit::Binary, 4);
340
341 uint64_t decimalFactor = 1'000;
342 uint64_t binaryFactor = 1'024;
343 for (size_t index = 0; index < 5; ++index)
344 {
345 VerifyRoundTrip(32 * decimalFactor, StorageSizeUnit::Decimal, 3);
346 VerifyRoundTrip(32 * binaryFactor, StorageSizeUnit::Binary, 3);
347 decimalFactor *= 1'000;
348 binaryFactor *= 1'024;
349 }
350 }
351
352 TEST_METHOD(StorageSize_TextToBytesRoundTrips)
353 {
354 const std::vector<StorageSizeTextRoundTripCase> TestCases{
355 {L"0B", StorageSizeUnit::Decimal, 3},
356 {L"32B", StorageSizeUnit::Decimal, 3},
357 {L"32kB", StorageSizeUnit::Decimal, 3},
358 {L"32.5MB", StorageSizeUnit::Decimal, 3},
359 {L"1GB", StorageSizeUnit::Decimal, 3},
360 {L"1.25TB", StorageSizeUnit::Decimal, 3},
361 {L"1PB", StorageSizeUnit::Decimal, 3},
362 {L"32KiB", StorageSizeUnit::Binary, 3},
363 {L"1.5MiB", StorageSizeUnit::Binary, 3},
364 {L"1GiB", StorageSizeUnit::Binary, 3},
365 {L"1.25TiB", StorageSizeUnit::Binary, 3},
366 {L"1PiB", StorageSizeUnit::Binary, 3},
367 };
368
369 for (const auto& TestCase : TestCases)
370 {
371 const auto bytes = ParseStorageSize(TestCase.Text, TestCase.Unit);
372 VERIFY_IS_TRUE(bytes.has_value());
373
374 const auto text = FormatHumanReadableSize(bytes.value(), TestCase.Precision, TestCase.Unit);
375 VERIFY_ARE_EQUAL(TestCase.Text, text);
376 VERIFY_ARE_EQUAL(bytes, ParseStorageSize(text, TestCase.Unit));
377 }
378 }
379 };
380 } // namespace StringUnitTests