1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ FilesystemUnitTests.cpp
8
+
9
+Abstract:
10
+
11
+ This file contains unit tests for the helpers in src/windows/common/filesystem.cpp.
12
+ These tests only read from the local filesystem so they do not require an installed distribution.
13
+
14
+--*/
15
+
16
+#include "precomp.h"
17
+#include "Common.h"
18
+
19
+using wsl::windows::common::filesystem::GetCanonicalPath;
20
+
21
+namespace {
22
+
23
+// Returns a file name that does not exist in the given directory.
24
+std::wstring UniqueMissingName(const std::filesystem::path& Directory)
25
+{
26
+ static int counter = 0;
27
+ const auto name = std::format(L"wsl_ut_canonical_{}_{}.txt", GetCurrentProcessId(), ++counter);
28
+ VERIFY_IS_FALSE(std::filesystem::exists(Directory / name));
29
+
30
+ return name;
31
+}
32
+
33
+// The canonical form of the current directory, which is what a relative path is expected to resolve
34
+// against. std::filesystem::canonical is used rather than weakly_canonical so the expected value is
35
+// computed independently of the API under test.
36
+std::filesystem::path CanonicalCurrentDirectory()
37
+{
38
+ return std::filesystem::canonical(std::filesystem::current_path());
39
+}
40
+
41
+// A path that std::filesystem::absolute is guaranteed to reject, because it exceeds the longest path
42
+// Win32 can express. An empty path is not used: whether absolute rejects one is implementation
43
+// defined, and some standard library versions accept it.
44
+std::filesystem::path UnresolvablePath()
45
+{
46
+ return {L"C:\\" + std::wstring(40000, L'a')};
47
+}
48
+
49
+// A file that is known to exist, used to cover paths that resolve to a real filesystem entry. The
50
+// test module itself is used so that no file has to be created.
51
+std::filesystem::path ExistingFile()
52
+{
53
+ return {wil::GetModuleFileNameW<std::wstring>(wil::GetModuleInstanceHandle())};
54
+}
55
+
56
+} // namespace
57
+
58
+namespace FilesystemUnitTests {
59
+class FilesystemUnitTests
60
+{
61
+ WSL_TEST_CLASS(FilesystemUnitTests)
62
+
63
+ // A relative path naming a file that does not exist must still resolve to an absolute path.
64
+ // std::filesystem::weakly_canonical cannot do this on its own: it builds its result from the
65
+ // longest leading sequence of elements that exist, so a bare missing file name has nothing to
66
+ // canonicalize and is returned unchanged.
67
+ TEST_METHOD(GetCanonicalPath_RelativeMissingPathIsMadeAbsolute)
68
+ {
69
+ const auto name = UniqueMissingName(std::filesystem::current_path());
70
+ VERIFY_IS_FALSE(std::filesystem::weakly_canonical(name).is_absolute());
71
+
72
+ const auto result = GetCanonicalPath(name);
73
+
74
+ VERIFY_IS_TRUE(result.is_absolute());
75
+ VERIFY_ARE_EQUAL((CanonicalCurrentDirectory() / name).wstring(), result.wstring());
76
+ }
77
+
78
+ // The same resolution must happen for a relative path whose target already exists.
79
+ TEST_METHOD(GetCanonicalPath_RelativeExistingPathIsMadeAbsolute)
80
+ {
81
+ const auto existing = ExistingFile();
82
+ const auto relativePath = std::filesystem::relative(existing, std::filesystem::current_path());
83
+ VERIFY_IS_FALSE(relativePath.empty());
84
+ VERIFY_IS_FALSE(relativePath.is_absolute());
85
+
86
+ const auto result = GetCanonicalPath(relativePath);
87
+
88
+ VERIFY_IS_TRUE(result.is_absolute());
89
+ VERIFY_ARE_EQUAL(std::filesystem::canonical(existing).wstring(), result.wstring());
90
+ }
91
+
92
+ // '.' and '..' components must be collapsed even when the intermediate directory does not exist.
93
+ TEST_METHOD(GetCanonicalPath_CollapsesDotSegments)
94
+ {
95
+ const auto name = UniqueMissingName(std::filesystem::current_path());
96
+
97
+ const auto result = GetCanonicalPath(L".\\nonexistent\\..\\" + name);
98
+
99
+ VERIFY_ARE_EQUAL((CanonicalCurrentDirectory() / name).wstring(), result.wstring());
100
+ }
101
+
102
+ // An already absolute path must be returned unchanged.
103
+ TEST_METHOD(GetCanonicalPath_AbsolutePathIsUnchanged)
104
+ {
105
+ const auto expected = CanonicalCurrentDirectory() / UniqueMissingName(std::filesystem::current_path());
106
+
107
+ VERIFY_ARE_EQUAL(expected.wstring(), GetCanonicalPath(expected).wstring());
108
+ }
109
+
110
+ // A failure from std::filesystem::absolute must be reported. absolute returns an empty path when
111
+ // it fails, and weakly_canonical succeeds on an empty path and clears the error_code, so calling
112
+ // the two in sequence without checking in between silently turns the failure into success.
113
+ TEST_METHOD(GetCanonicalPath_ErrorOverloadReportsFailure)
114
+ {
115
+ std::error_code error;
116
+ const auto result = GetCanonicalPath(UnresolvablePath(), error);
117
+
118
+ VERIFY_ARE_NOT_EQUAL(std::error_code{}, error);
119
+ VERIFY_IS_TRUE(result.empty());
120
+ }
121
+
122
+ // Error must be cleared when the call succeeds so callers can reuse the same variable.
123
+ TEST_METHOD(GetCanonicalPath_ErrorOverloadClearsErrorOnSuccess)
124
+ {
125
+ const auto expected = CanonicalCurrentDirectory() / UniqueMissingName(std::filesystem::current_path());
126
+
127
+ auto error = std::make_error_code(std::errc::permission_denied);
128
+ const auto result = GetCanonicalPath(expected, error);
129
+
130
+ VERIFY_ARE_EQUAL(std::error_code{}, error);
131
+ VERIFY_ARE_EQUAL(expected.wstring(), result.wstring());
132
+ }
133
+
134
+ // The throwing overload must surface the same failure the non-throwing overload reports.
135
+ TEST_METHOD(GetCanonicalPath_ThrowingOverloadSurfacesFailure)
136
+ {
137
+ std::error_code error;
138
+ (void)GetCanonicalPath(UnresolvablePath(), error);
139
+ VERIFY_ARE_NOT_EQUAL(std::error_code{}, error);
140
+
141
+ const auto expectedResult = HRESULT_FROM_WIN32(error.value());
142
+ VERIFY_THROWS_SPECIFIC(GetCanonicalPath(UnresolvablePath()), wil::ResultException, [&](const wil::ResultException& e) {
143
+ return e.GetErrorCode() == expectedResult;
144
+ });
145
+ }
146
+};
147
+} // namespace FilesystemUnitTests