master
cpp 549 lines 26.9 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLIMountParserUnitTests.cpp
8
9 Abstract:
10
11 Unit tests for Docker-compatible --mount parsing.
12
13 --*/
14
15 #include "precomp.h"
16 #include "windows/Common.h"
17 #include "WSLCCLITestHelpers.h"
18 #include "ContainerModel.h"
19 #include "MountSpecParsing.h"
20
21 using namespace wsl::windows::common;
22 using namespace wsl::windows::wslc::models;
23 using namespace wsl::shared;
24 using namespace WEX::Logging;
25 using namespace WEX::Common;
26
27 namespace WSLCCLIMountParserUnitTests {
28
29 namespace {
30
31 mount::Spec ParseAndValidate(const std::wstring& value)
32 {
33 auto mountSpec = mount::ParseDockerMountString(value);
34 mount::ValidateMountSpec(mountSpec);
35 return mountSpec;
36 }
37
38 struct ValidMountCase
39 {
40 const wchar_t* Input;
41 mount::Type Type;
42 const wchar_t* Source;
43 const char* Target;
44 bool ReadOnly;
45 std::optional<int64_t> TmpfsSizeBytes;
46 std::optional<uint32_t> TmpfsMode;
47 const char* TmpfsOptions;
48 };
49
50 enum class ExpectedException
51 {
52 Parse,
53 Unsupported,
54 Validation,
55 };
56
57 struct InvalidMountCase
58 {
59 const wchar_t* Input;
60 std::wstring ExpectedReason;
61 ExpectedException Exception = ExpectedException::Parse;
62 };
63
64 constexpr ValidMountCase c_validMountCases[] = {
65 {L"type=volume,target=/data", WSLCMountTypeVolume, L"", "/data", false, {}, {}, ""},
66 {L"source=data-volume,target=/data", WSLCMountTypeVolume, L"data-volume", "/data", false, {}, {}, ""},
67 {L"type=volume,source=data-volume,target=/path:voldir",
68 WSLCMountTypeVolume,
69 L"data-volume",
70 "/path:voldir",
71 false,
72 {},
73 {},
74 ""},
75 {L"TYPE=VOLUME,SOURCE=data-volume,TARGET=/data", WSLCMountTypeVolume, L"data-volume", "/data", false, {}, {}, ""},
76 {L"type=VoLuMe,source=data-volume,target=/data", WSLCMountTypeVolume, L"data-volume", "/data", false, {}, {}, ""},
77 {L"type=volume,src=data-volume,dst=/data", WSLCMountTypeVolume, L"data-volume", "/data", false, {}, {}, ""},
78 {L"type=volume,src=data-volume,destination=/data", WSLCMountTypeVolume, L"data-volume", "/data", false, {}, {}, ""},
79 {L"type=volume,source=first,source=second,target=/data", WSLCMountTypeVolume, L"second", "/data", false, {}, {}, ""},
80 {L"type=volume,source=data-volume,target=/first,target=/second",
81 WSLCMountTypeVolume,
82 L"data-volume",
83 "/second",
84 false,
85 {},
86 {},
87 ""},
88 {L"type=volume,type=bind,source=C:\\data,target=/data", WSLCMountTypeBind, L"C:\\data", "/data", false, {}, {}, ""},
89 {L"type=volume,source=data-volume,target=/data,readonly", WSLCMountTypeVolume, L"data-volume", "/data", true, {}, {}, ""},
90 {L"type=volume,source=data-volume,target=/data,ro", WSLCMountTypeVolume, L"data-volume", "/data", true, {}, {}, ""},
91 {L"type=volume,source=data-volume,target=/data,readonly=1",
92 WSLCMountTypeVolume,
93 L"data-volume",
94 "/data",
95 true,
96 {},
97 {},
98 ""},
99 {L"type=volume,source=data-volume,target=/data,readonly=t",
100 WSLCMountTypeVolume,
101 L"data-volume",
102 "/data",
103 true,
104 {},
105 {},
106 ""},
107 {L"type=volume,source=data-volume,target=/data,readonly=T",
108 WSLCMountTypeVolume,
109 L"data-volume",
110 "/data",
111 true,
112 {},
113 {},
114 ""},
115 {L"type=volume,source=data-volume,target=/data,readonly=TRUE",
116 WSLCMountTypeVolume,
117 L"data-volume",
118 "/data",
119 true,
120 {},
121 {},
122 ""},
123 {L"type=volume,source=data-volume,target=/data,readonly=true",
124 WSLCMountTypeVolume,
125 L"data-volume",
126 "/data",
127 true,
128 {},
129 {},
130 ""},
131 {L"type=volume,source=data-volume,target=/data,readonly=True",
132 WSLCMountTypeVolume,
133 L"data-volume",
134 "/data",
135 true,
136 {},
137 {},
138 ""},
139 {L"type=volume,source=data-volume,target=/data,readonly=0",
140 WSLCMountTypeVolume,
141 L"data-volume",
142 "/data",
143 false,
144 {},
145 {},
146 ""},
147 {L"type=volume,source=data-volume,target=/data,readonly=f",
148 WSLCMountTypeVolume,
149 L"data-volume",
150 "/data",
151 false,
152 {},
153 {},
154 ""},
155 {L"type=volume,source=data-volume,target=/data,readonly=F",
156 WSLCMountTypeVolume,
157 L"data-volume",
158 "/data",
159 false,
160 {},
161 {},
162 ""},
163 {L"type=volume,source=data-volume,target=/data,readonly=FALSE",
164 WSLCMountTypeVolume,
165 L"data-volume",
166 "/data",
167 false,
168 {},
169 {},
170 ""},
171 {L"type=volume,source=data-volume,target=/data,readonly=false",
172 WSLCMountTypeVolume,
173 L"data-volume",
174 "/data",
175 false,
176 {},
177 {},
178 ""},
179 {L"type=volume,source=data-volume,target=/data,readonly=False",
180 WSLCMountTypeVolume,
181 L"data-volume",
182 "/data",
183 false,
184 {},
185 {},
186 ""},
187 {L"type=volume,source=data-volume,target=/data,readonly=true,readonly=false",
188 WSLCMountTypeVolume,
189 L"data-volume",
190 "/data",
191 false,
192 {},
193 {},
194 ""},
195 {L"type=volume,source=data-volume,target=/data,bind-recursive=enabled",
196 WSLCMountTypeVolume,
197 L"data-volume",
198 "/data",
199 false,
200 {},
201 {},
202 ""},
203 {L"type=bind,\"source=C:\\mount,a\",target=/data", WSLCMountTypeBind, L"C:\\mount,a", "/data", false, {}, {}, ""},
204 {L"type=bind,source=C:\\mount with spaces,target=/data",
205 WSLCMountTypeBind,
206 L"C:\\mount with spaces",
207 "/data",
208 false,
209 {},
210 {},
211 ""},
212 {L"type=bind,source=C:\\mount,target=/path:mntdir", WSLCMountTypeBind, L"C:\\mount", "/path:mntdir", false, {}, {}, ""},
213 {L"type=bind,source=C:\\,target=/data", WSLCMountTypeBind, L"C:\\", "/data", false, {}, {}, ""},
214 {L"type=bind,source=\\\\server\\share,target=/data", WSLCMountTypeBind, L"\\\\server\\share", "/data", false, {}, {}, ""},
215 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=enabled",
216 WSLCMountTypeBind,
217 L"C:\\mount",
218 "/data",
219 false,
220 {},
221 {},
222 ""},
223 {L"type=volume,source=A_,target=/data", WSLCMountTypeVolume, L"A_", "/data", false, {}, {}, ""},
224 {L"type=volume,source=data.volume-1,target=/data", WSLCMountTypeVolume, L"data.volume-1", "/data", false, {}, {}, ""},
225 {L"type=tmpfs,target=/tmp", WSLCMountTypeTmpfs, L"", "/tmp", false, {}, {}, ""},
226 {L"type=tmpfs,target=/path:tmpfs", WSLCMountTypeTmpfs, L"", "/path:tmpfs", false, {}, {}, ""},
227 {L"type=tmpfs,target=/tmp,readonly", WSLCMountTypeTmpfs, L"", "/tmp", true, {}, {}, "ro"},
228 {L"type=tmpfs,target=/tmp,tmpfs-size=0", WSLCMountTypeTmpfs, L"", "/tmp", false, 0, {}, ""},
229 {L"type=tmpfs,target=/tmp,tmpfs-size=1", WSLCMountTypeTmpfs, L"", "/tmp", false, 1, {}, "size=1"},
230 {L"type=tmpfs,target=/tmp,tmpfs-size=1024", WSLCMountTypeTmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
231 {L"type=tmpfs,target=/tmp,tmpfs-size=1536", WSLCMountTypeTmpfs, L"", "/tmp", false, 1536, {}, "size=1536"},
232 {L"type=tmpfs,target=/tmp,tmpfs-size=1k", WSLCMountTypeTmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
233 {L"type=tmpfs,target=/tmp,tmpfs-size=1KB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
234 {L"type=tmpfs,target=/tmp,tmpfs-size=1KiB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1024, {}, "size=1k"},
235 {L"type=tmpfs,target=/tmp,tmpfs-size=1MB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
236 {L"type=tmpfs,target=/tmp,tmpfs-size=1MiB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
237 {L"type=tmpfs,target=/tmp,tmpfs-size=1GB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1LL << 30, {}, "size=1g"},
238 {L"type=tmpfs,target=/tmp,tmpfs-size=1.5MB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1536LL << 10, {}, "size=1536k"},
239 {L"type=tmpfs,target=/tmp,tmpfs-size=+1MB", WSLCMountTypeTmpfs, L"", "/tmp", false, 1LL << 20, {}, "size=1m"},
240 {L"type=tmpfs,target=/tmp,tmpfs-size=1e3", WSLCMountTypeTmpfs, L"", "/tmp", false, 1000, {}, "size=1000"},
241 {L"type=tmpfs,target=/tmp,tmpfs-mode=0000", WSLCMountTypeTmpfs, L"", "/tmp", false, {}, 0, ""},
242 {L"type=tmpfs,target=/tmp,tmpfs-mode=0700", WSLCMountTypeTmpfs, L"", "/tmp", false, {}, 0700, "mode=700"},
243 {L"type=tmpfs,target=/tmp,tmpfs-size=1MB,tmpfs-mode=0700,readonly",
244 WSLCMountTypeTmpfs,
245 L"",
246 "/tmp",
247 true,
248 1LL << 20,
249 0700,
250 "ro,mode=700,size=1m"},
251 {L"type=tmpfs,target=/tmp,tmpfs-size=0,tmpfs-mode=0000,readonly=false", WSLCMountTypeTmpfs, L"", "/tmp", false, 0, 0, ""},
252 };
253
254 const InvalidMountCase c_invalidMountCases[] = {
255 {L"", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
256 {L",", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
257 {L"type=volume,source=data-volume,target=/data,", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
258 {L",type=volume,source=data-volume,target=/data", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"")},
259 {L"type=bind,\"source=C:\\mount,target=/data", Localization::WSLCCLI_MountMalformedCsvError()},
260 {L"type=volume,bogus", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"bogus")},
261 {L"type=volume,bogus=value", Localization::WSLCCLI_MountUnexpectedKeyError(L"bogus", L"bogus=value")},
262 {L"type", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"type")},
263 {L"source", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"source")},
264 {L"target", Localization::WSLCCLI_MountFieldKeyValueRequiredError(L"target")},
265 {L"type=,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeRequiredError()},
266 {L"type=volume,source=data-volume", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
267 {L"type=volume,source=data-volume,target=", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
268 {L"type=volume,source=data-volume,dst=", Localization::WSLCCLI_MountTargetRequiredError(), ExpectedException::Validation},
269 {L"type=cluster,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"cluster"), ExpectedException::Unsupported},
270 {L"type=npipe,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"npipe"), ExpectedException::Unsupported},
271 {L"type=bogus,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"bogus"), ExpectedException::Unsupported},
272 {L"type=CLUSTER,source=data-volume,target=/data", Localization::WSLCCLI_MountTypeUnsupportedError(L"cluster"), ExpectedException::Unsupported},
273 {L"type=bind,target=/data", Localization::WSLCCLI_MountSourceRequiredError(), ExpectedException::Validation},
274 {L"type=bind,source=relative,target=/data", Localization::WSLCCLI_MountBindSourceAbsoluteError(), ExpectedException::Validation},
275 {L"type=volume,source=a,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
276 {L"type=volume,source=data/volume,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
277 {L"type=volume,source=C:\\mount,target=/data", Localization::WSLCCLI_MountVolumeSourceInvalidError(), ExpectedException::Validation},
278 {L"type=tmpfs,source=data-volume,target=/data", Localization::WSLCCLI_MountTmpfsSourceUnsupportedError(), ExpectedException::Validation},
279 {L"type=volume,source=data-volume,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
280 {L"type=volume,source=data-volume,dst=.", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
281 {L"type=volume,source=data-volume,destination=\\data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
282 {L"type=bind,source=C:\\mount,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
283 {L"type=tmpfs,target=data", Localization::WSLCCLI_MountTargetAbsoluteError(), ExpectedException::Validation},
284 {L"type=volume,source=data-volume,target=/data,readonly=no",
285 Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"no")},
286 {L"type=volume,source=data-volume,target=/data,readonly=yes",
287 Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"yes")},
288 {L"type=volume,source=data-volume,target=/data,readonly=", Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"")},
289 {L"type=volume,source=data-volume,target=/data,readonly=2",
290 Localization::WSLCCLI_MountInvalidValueError(L"readonly", L"2")},
291 {L"type=volume,source=data-volume,target=/data,volume-nocopy=no",
292 Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", L"no")},
293 {L"type=volume,source=data-volume,target=/data,volume-nocopy=",
294 Localization::WSLCCLI_MountInvalidValueError(L"volume-nocopy", L"")},
295 {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive=no",
296 Localization::WSLCCLI_MountInvalidValueError(L"bind-nonrecursive", L"no")},
297 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=",
298 Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"")},
299 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=Enabled",
300 Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"Enabled")},
301 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=bogus",
302 Localization::WSLCCLI_MountInvalidBindRecursiveValueError(L"bind-recursive", L"bogus")},
303 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=writable",
304 Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=writable")},
305 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly",
306 Localization::WSLCCLI_MountOptionRequiresReadonlyError(L"bind-recursive=readonly")},
307 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly,readonly",
308 Localization::WSLCCLI_MountBindRecursiveReadonlyRequiresPropagationError()},
309 {L"type=bind,source=C:\\mount,target=/data,consistency=cached",
310 Localization::WSLCCLI_MountOptionUnsupportedError(L"consistency"),
311 ExpectedException::Unsupported},
312 {L"type=bind,source=C:\\mount,target=/data,bind-propagation=rprivate",
313 Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-propagation"),
314 ExpectedException::Unsupported},
315 {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive",
316 Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-nonrecursive"),
317 ExpectedException::Unsupported},
318 {L"type=bind,source=C:\\mount,target=/data,bind-nonrecursive=true",
319 Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-nonrecursive"),
320 ExpectedException::Unsupported},
321 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=disabled",
322 Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
323 ExpectedException::Unsupported},
324 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=writable,readonly",
325 Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
326 ExpectedException::Unsupported},
327 {L"type=bind,source=C:\\mount,target=/data,bind-recursive=readonly,readonly,bind-propagation=rprivate",
328 Localization::WSLCCLI_MountOptionUnsupportedError(L"bind-recursive"),
329 ExpectedException::Unsupported},
330 {L"type=volume,source=data-volume,target=/data,volume-nocopy",
331 Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-nocopy"),
332 ExpectedException::Unsupported},
333 {L"type=volume,source=data-volume,target=/data,volume-nocopy=true",
334 Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-nocopy"),
335 ExpectedException::Unsupported},
336 {L"type=volume,source=data-volume,target=/data,volume-label=a=b",
337 Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-label"),
338 ExpectedException::Unsupported},
339 {L"type=volume,source=data-volume,target=/data,volume-driver=local",
340 Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-driver"),
341 ExpectedException::Unsupported},
342 {L"type=volume,source=data-volume,target=/data,volume-opt=a=b",
343 Localization::WSLCCLI_MountOptionUnsupportedError(L"volume-opt"),
344 ExpectedException::Unsupported},
345 {L"type=bind,source=C:\\mount,target=/data,volume-nocopy=true",
346 Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", L"bind")},
347 {L"type=volume,source=data-volume,target=/data,bind-propagation=rprivate",
348 Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"volume")},
349 {L"type=volume,source=data-volume,target=/data,tmpfs-size=1m",
350 Localization::WSLCCLI_MountOptionFamilyMismatchError(L"tmpfs-*", L"volume")},
351 {L"type=tmpfs,target=/tmp,volume-label=a=b", Localization::WSLCCLI_MountOptionFamilyMismatchError(L"volume-*", L"tmpfs")},
352 {L"type=tmpfs,target=/tmp,bind-nonrecursive", Localization::WSLCCLI_MountOptionFamilyMismatchError(L"bind-*", L"tmpfs")},
353 {L"type=tmpfs,target=/tmp,tmpfs-size=", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"")},
354 {L"type=tmpfs,target=/tmp,tmpfs-size=bad", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"bad")},
355 {L"type=tmpfs,target=/tmp,tmpfs-size=-1", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"-1")},
356 {L"type=tmpfs,target=/tmp,\"tmpfs-size=1,5MB\"", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1,5MB")},
357 {L"type=tmpfs,target=/tmp,tmpfs-size=1XB", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1XB")},
358 {L"type=tmpfs,target=/tmp,tmpfs-size=1Ki", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1Ki")},
359 {L"type=tmpfs,target=/tmp,tmpfs-size=1BB", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"1BB")},
360 {L"type=tmpfs,target=/tmp,tmpfs-size=nan", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"nan")},
361 {L"type=tmpfs,target=/tmp,tmpfs-size=inf", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"inf")},
362 {L"type=tmpfs,target=/tmp,tmpfs-size=9223372036854775808",
363 Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-size", L"9223372036854775808")},
364 {L"type=tmpfs,target=/tmp,tmpfs-mode=", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"")},
365 {L"type=tmpfs,target=/tmp,tmpfs-mode=+0700", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"+0700")},
366 {L"type=tmpfs,target=/tmp,tmpfs-mode=-1", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"-1")},
367 {L"type=tmpfs,target=/tmp,tmpfs-mode=8", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"8")},
368 {L"type=tmpfs,target=/tmp,tmpfs-mode=0899", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"0899")},
369 {L"type=tmpfs,target=/tmp,tmpfs-mode=0x700", Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"0x700")},
370 {L"type=tmpfs,target=/tmp,tmpfs-mode=40000000000",
371 Localization::WSLCCLI_MountInvalidValueError(L"tmpfs-mode", L"40000000000")},
372 };
373
374 } // namespace
375
376 class WSLCCLIMountParserUnitTests
377 {
378 WSLC_TEST_CLASS(WSLCCLIMountParserUnitTests)
379
380 TEST_METHOD(Mount_ValidCases)
381 {
382 for (const auto& testCase : c_validMountCases)
383 {
384 Log::Comment(String().Format(L"Accepting: %ls", testCase.Input));
385
386 const auto actual = ParseAndValidate(testCase.Input);
387 VERIFY_ARE_EQUAL(static_cast<int>(testCase.Type), static_cast<int>(actual.MountType));
388 VERIFY_ARE_EQUAL(std::wstring(testCase.Source), actual.Source);
389 VERIFY_ARE_EQUAL(std::string(testCase.Target), actual.Target);
390 VERIFY_ARE_EQUAL(testCase.ReadOnly, actual.ReadOnly);
391 VERIFY_ARE_EQUAL(testCase.TmpfsSizeBytes.has_value(), actual.TmpfsSizeBytes.has_value());
392 if (testCase.TmpfsSizeBytes.has_value() && actual.TmpfsSizeBytes.has_value())
393 {
394 VERIFY_ARE_EQUAL(testCase.TmpfsSizeBytes.value(), actual.TmpfsSizeBytes.value());
395 }
396
397 VERIFY_ARE_EQUAL(testCase.TmpfsMode.has_value(), actual.TmpfsMode.has_value());
398 if (testCase.TmpfsMode.has_value() && actual.TmpfsMode.has_value())
399 {
400 VERIFY_ARE_EQUAL(testCase.TmpfsMode.value(), actual.TmpfsMode.value());
401 }
402
403 const auto actualTmpfsOptions = actual.MountType == WSLCMountTypeTmpfs ? mount::FormatTmpfsOptions(actual) : std::string{};
404 VERIFY_ARE_EQUAL(std::string(testCase.TmpfsOptions), actualTmpfsOptions);
405 }
406 }
407
408 TEST_METHOD(Mount_InvalidCases)
409 {
410 for (const auto& testCase : c_invalidMountCases)
411 {
412 Log::Comment(String().Format(L"Rejecting: %ls", testCase.Input));
413
414 try
415 {
416 (void)ParseAndValidate(testCase.Input);
417 VERIFY_FAIL(L"Expected MountException for invalid mount spec");
418 }
419 catch (const mount::MountException& ex)
420 {
421 VERIFY_ARE_EQUAL(testCase.ExpectedReason, ex.Reason());
422 switch (testCase.Exception)
423 {
424 case ExpectedException::Parse:
425 VERIFY_IS_TRUE(dynamic_cast<const mount::MountParseException*>(&ex) != nullptr);
426 break;
427
428 case ExpectedException::Unsupported:
429 VERIFY_IS_TRUE(dynamic_cast<const mount::MountUnsupportedException*>(&ex) != nullptr);
430 break;
431
432 case ExpectedException::Validation:
433 VERIFY_IS_TRUE(dynamic_cast<const mount::MountValidationException*>(&ex) != nullptr);
434 break;
435 }
436 }
437 }
438 }
439
440 TEST_METHOD(Volume_ValidCases)
441 {
442 const auto bind = mount::ParseDockerVolumeString(LR"(C:\hostPath:/data:ro)");
443 VERIFY_ARE_EQUAL(static_cast<int>(WSLCMountTypeBind), static_cast<int>(bind.MountType));
444 VERIFY_ARE_EQUAL(std::wstring(LR"(C:\hostPath)"), bind.Source);
445 VERIFY_ARE_EQUAL(std::string("/data"), bind.Target);
446 VERIFY_IS_TRUE(bind.ReadOnly);
447 VERIFY_ARE_EQUAL(static_cast<int>(mount::BindSourcePolicy::CreateIfMissing), static_cast<int>(bind.BindSource));
448
449 const auto relativeBind = mount::ParseDockerVolumeString(L".\\mount:/data");
450 VERIFY_ARE_EQUAL(std::filesystem::weakly_canonical(std::filesystem::current_path() / L"mount").wstring(), relativeBind.Source);
451
452 const auto volume = mount::ParseDockerVolumeString(L"named-volume:/data");
453 VERIFY_ARE_EQUAL(static_cast<int>(WSLCMountTypeVolume), static_cast<int>(volume.MountType));
454 VERIFY_ARE_EQUAL(std::wstring(L"named-volume"), volume.Source);
455 VERIFY_ARE_EQUAL(std::string("/data"), volume.Target);
456 VERIFY_IS_FALSE(volume.ReadOnly);
457 VERIFY_ARE_EQUAL(static_cast<int>(mount::BindSourcePolicy::RequireExisting), static_cast<int>(volume.BindSource));
458 }
459
460 TEST_METHOD(Volume_InvalidHostPath)
461 {
462 try
463 {
464 (void)mount::ParseDockerVolumeString(L"::/container:ro");
465 VERIFY_FAIL(L"Expected MountParseException for an invalid host path");
466 }
467 catch (const mount::MountParseException& ex)
468 {
469 VERIFY_ARE_EQUAL(Localization::WSLCCLI_VolumeHostPathInvalid(L"::/container:ro", L":"), ex.Reason());
470 }
471 }
472
473 TEST_METHOD(Mount_DotRelativeBindSourceUsesCurrentDirectory)
474 {
475 const auto expected = (std::filesystem::current_path() / L"mount").lexically_normal().wstring();
476 const auto actual = ParseAndValidate(L"type=bind,source=.\\mount,target=/data");
477 VERIFY_ARE_EQUAL(expected, actual.Source);
478 }
479
480 TEST_METHOD(Mount_TypedSpecsAreValidated)
481 {
482 const mount::Spec relativeBind{
483 .MountType = WSLCMountTypeBind,
484 .Source = L"relative",
485 .Target = "/data",
486 };
487 VERIFY_THROWS(mount::ValidateMountSpec(relativeBind), mount::MountValidationException);
488
489 const mount::Spec relativeTarget{
490 .MountType = WSLCMountTypeVolume,
491 .Source = L"data-volume",
492 .Target = "data",
493 };
494 VERIFY_THROWS(mount::ValidateMountSpec(relativeTarget), mount::MountValidationException);
495
496 const mount::Spec tmpfsWithSource{
497 .MountType = WSLCMountTypeTmpfs,
498 .Source = L"data-volume",
499 .Target = "/data",
500 };
501 VERIFY_THROWS(mount::ValidateMountSpec(tmpfsWithSource), mount::MountValidationException);
502
503 const mount::Spec bindWithTmpfsOptions{
504 .MountType = WSLCMountTypeBind,
505 .Source = L"C:\\data",
506 .Target = "/data",
507 .TmpfsSizeBytes = 1024,
508 };
509 VERIFY_THROWS(mount::ValidateMountSpec(bindWithTmpfsOptions), mount::MountValidationException);
510
511 const mount::Spec negativeTmpfsSize{
512 .MountType = WSLCMountTypeTmpfs,
513 .Target = "/data",
514 .TmpfsSizeBytes = -1,
515 };
516 VERIFY_THROWS(mount::ValidateMountSpec(negativeTmpfsSize), mount::MountValidationException);
517
518 const mount::Spec tmpfs{
519 .MountType = WSLCMountTypeTmpfs,
520 .Target = "/data",
521 .TmpfsSizeBytes = 1024,
522 .TmpfsMode = 0700,
523 };
524 VERIFY_NO_THROW(mount::ValidateMountSpec(tmpfs));
525
526 const mount::Spec duplicateMounts[] = {
527 {.MountType = WSLCMountTypeTmpfs, .Target = "/data"},
528 {.MountType = WSLCMountTypeVolume, .Source = L"data-volume", .Target = "/data/"},
529 };
530 try
531 {
532 mount::ValidateMountCollection(duplicateMounts);
533 VERIFY_FAIL(L"Expected MountValidationException for duplicate destinations");
534 }
535 catch (const mount::MountValidationException& ex)
536 {
537 VERIFY_ARE_EQUAL(static_cast<int>(mount::ValidationError::DuplicateDestination), static_cast<int>(ex.Error()));
538 VERIFY_ARE_EQUAL(std::string("/data"), ex.Destination());
539 }
540
541 const mount::Spec distinctBackslashMounts[] = {
542 {.MountType = WSLCMountTypeTmpfs, .Target = "/data\\cache"},
543 {.MountType = WSLCMountTypeVolume, .Source = L"data-volume", .Target = "/data/cache"},
544 };
545 VERIFY_NO_THROW(mount::ValidateMountCollection(distinctBackslashMounts));
546 }
547 };
548
549 } // namespace WSLCCLIMountParserUnitTests