1 | //===-- PathMappingTests.cpp ------------------------*- C++ -*-----------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "PathMapping.h" |
10 | #include "llvm/Support/JSON.h" |
11 | #include "gmock/gmock.h" |
12 | #include "gtest/gtest.h" |
13 | #include <optional> |
14 | #include <string> |
15 | namespace clang { |
16 | namespace clangd { |
17 | namespace { |
18 | using ::testing::ElementsAre; |
19 | MATCHER_P2(Mapping, ClientPath, ServerPath, "" ) { |
20 | return arg.ClientPath == ClientPath && arg.ServerPath == ServerPath; |
21 | } |
22 | |
23 | bool failedParse(llvm::StringRef RawMappings) { |
24 | llvm::Expected<PathMappings> Mappings = parsePathMappings(RawPathMappings: RawMappings); |
25 | if (!Mappings) { |
26 | consumeError(Err: Mappings.takeError()); |
27 | return true; |
28 | } |
29 | return false; |
30 | } |
31 | |
32 | TEST(ParsePathMappingTests, WindowsPath) { |
33 | // Relative path to C drive |
34 | EXPECT_TRUE(failedParse(R"(C:a=/root)" )); |
35 | EXPECT_TRUE(failedParse(R"(\C:a=/root)" )); |
36 | // Relative path to current drive. |
37 | EXPECT_TRUE(failedParse(R"(\a=/root)" )); |
38 | // Absolute paths |
39 | llvm::Expected<PathMappings> ParsedMappings = |
40 | parsePathMappings(RawPathMappings: R"(C:\a=/root)" ); |
41 | ASSERT_TRUE(bool(ParsedMappings)); |
42 | EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("/C:/a" , "/root" ))); |
43 | // Absolute UNC path |
44 | ParsedMappings = parsePathMappings(RawPathMappings: R"(\\Server\C$=/root)" ); |
45 | ASSERT_TRUE(bool(ParsedMappings)); |
46 | EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("//Server/C$" , "/root" ))); |
47 | } |
48 | |
49 | TEST(ParsePathMappingTests, UnixPath) { |
50 | // Relative unix path |
51 | EXPECT_TRUE(failedParse("a/b=/root" )); |
52 | // Absolute unix path |
53 | llvm::Expected<PathMappings> ParsedMappings = parsePathMappings(RawPathMappings: "/A/b=/root" ); |
54 | ASSERT_TRUE(bool(ParsedMappings)); |
55 | EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("/A/b" , "/root" ))); |
56 | // Absolute unix path w/ backslash |
57 | ParsedMappings = parsePathMappings(RawPathMappings: R"(/a/b\\ar=/root)" ); |
58 | ASSERT_TRUE(bool(ParsedMappings)); |
59 | EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping(R"(/a/b\\ar)" , "/root" ))); |
60 | } |
61 | |
62 | TEST(ParsePathMappingTests, ImproperFormat) { |
63 | // uneven mappings |
64 | EXPECT_TRUE(failedParse("/home/myuser1=" )); |
65 | // mappings need to be absolute |
66 | EXPECT_TRUE(failedParse("home/project=/workarea/project" )); |
67 | // duplicate delimiter |
68 | EXPECT_TRUE(failedParse("/home==/workarea" )); |
69 | // no delimiter |
70 | EXPECT_TRUE(failedParse("/home" )); |
71 | // improper delimiter |
72 | EXPECT_TRUE(failedParse("/home,/workarea" )); |
73 | } |
74 | |
75 | TEST(ParsePathMappingTests, ParsesMultiple) { |
76 | std::string RawPathMappings = |
77 | "/home/project=/workarea/project,/home/project/.includes=/opt/include" ; |
78 | auto Parsed = parsePathMappings(RawPathMappings); |
79 | ASSERT_TRUE(bool(Parsed)); |
80 | EXPECT_THAT(*Parsed, |
81 | ElementsAre(Mapping("/home/project" , "/workarea/project" ), |
82 | Mapping("/home/project/.includes" , "/opt/include" ))); |
83 | } |
84 | |
85 | bool mapsProperly(llvm::StringRef Orig, llvm::StringRef Expected, |
86 | llvm::StringRef RawMappings, PathMapping::Direction Dir) { |
87 | llvm::Expected<PathMappings> Mappings = parsePathMappings(RawPathMappings: RawMappings); |
88 | if (!Mappings) |
89 | return false; |
90 | std::optional<std::string> MappedPath = doPathMapping(S: Orig, Dir, Mappings: *Mappings); |
91 | std::string Actual = MappedPath ? *MappedPath : Orig.str(); |
92 | EXPECT_STREQ(Expected.str().c_str(), Actual.c_str()); |
93 | return Expected == Actual; |
94 | } |
95 | |
96 | TEST(DoPathMappingTests, PreservesOriginal) { |
97 | // Preserves original path when no mapping |
98 | EXPECT_TRUE(mapsProperly("file:///home" , "file:///home" , "" , |
99 | PathMapping::Direction::ClientToServer)); |
100 | } |
101 | |
102 | TEST(DoPathMappingTests, UsesFirstMatch) { |
103 | EXPECT_TRUE(mapsProperly("file:///home/foo.cpp" , "file:///workarea1/foo.cpp" , |
104 | "/home=/workarea1,/home=/workarea2" , |
105 | PathMapping::Direction::ClientToServer)); |
106 | } |
107 | |
108 | TEST(DoPathMappingTests, IgnoresSubstrings) { |
109 | // Doesn't map substrings that aren't a proper path prefix |
110 | EXPECT_TRUE(mapsProperly("file://home/foo-bar.cpp" , "file://home/foo-bar.cpp" , |
111 | "/home/foo=/home/bar" , |
112 | PathMapping::Direction::ClientToServer)); |
113 | } |
114 | |
115 | TEST(DoPathMappingTests, MapsOutgoingPaths) { |
116 | // When IsIncoming is false (i.e.a response), map the other way |
117 | EXPECT_TRUE(mapsProperly("file:///workarea/foo.cpp" , "file:///home/foo.cpp" , |
118 | "/home=/workarea" , |
119 | PathMapping::Direction::ServerToClient)); |
120 | } |
121 | |
122 | TEST(DoPathMappingTests, OnlyMapFileUris) { |
123 | EXPECT_TRUE(mapsProperly("test:///home/foo.cpp" , "test:///home/foo.cpp" , |
124 | "/home=/workarea" , |
125 | PathMapping::Direction::ClientToServer)); |
126 | } |
127 | |
128 | TEST(DoPathMappingTests, RespectsCaseSensitivity) { |
129 | EXPECT_TRUE(mapsProperly("file:///HOME/foo.cpp" , "file:///HOME/foo.cpp" , |
130 | "/home=/workarea" , |
131 | PathMapping::Direction::ClientToServer)); |
132 | } |
133 | |
134 | TEST(DoPathMappingTests, MapsWindowsPaths) { |
135 | // Maps windows properly |
136 | EXPECT_TRUE(mapsProperly("file:///C:/home/foo.cpp" , |
137 | "file:///C:/workarea/foo.cpp" , R"(C:\home=C:\workarea)" , |
138 | PathMapping::Direction::ClientToServer)); |
139 | } |
140 | |
141 | TEST(DoPathMappingTests, MapsWindowsUnixInterop) { |
142 | // Path mappings with a windows-style client path and unix-style server path |
143 | EXPECT_TRUE(mapsProperly( |
144 | "file:///C:/home/foo.cpp" , "file:///workarea/foo.cpp" , |
145 | R"(C:\home=/workarea)" , PathMapping::Direction::ClientToServer)); |
146 | } |
147 | |
148 | TEST(ApplyPathMappingTests, PreservesOriginalParams) { |
149 | auto Params = llvm::json::parse(JSON: R"({ |
150 | "textDocument": {"uri": "file:///home/foo.cpp"}, |
151 | "position": {"line": 0, "character": 0} |
152 | })" ); |
153 | ASSERT_TRUE(bool(Params)); |
154 | llvm::json::Value ExpectedParams = *Params; |
155 | PathMappings Mappings; |
156 | applyPathMappings(Params&: *Params, Dir: PathMapping::Direction::ClientToServer, Mappings); |
157 | EXPECT_EQ(*Params, ExpectedParams); |
158 | } |
159 | |
160 | TEST(ApplyPathMappingTests, MapsAllMatchingPaths) { |
161 | // Handles nested objects and array values |
162 | auto Params = llvm::json::parse(JSON: R"({ |
163 | "rootUri": {"uri": "file:///home/foo.cpp"}, |
164 | "workspaceFolders": ["file:///home/src", "file:///tmp"] |
165 | })" ); |
166 | auto ExpectedParams = llvm::json::parse(JSON: R"({ |
167 | "rootUri": {"uri": "file:///workarea/foo.cpp"}, |
168 | "workspaceFolders": ["file:///workarea/src", "file:///tmp"] |
169 | })" ); |
170 | auto Mappings = parsePathMappings(RawPathMappings: "/home=/workarea" ); |
171 | ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); |
172 | applyPathMappings(Params&: *Params, Dir: PathMapping::Direction::ClientToServer, Mappings: *Mappings); |
173 | EXPECT_EQ(*Params, *ExpectedParams); |
174 | } |
175 | |
176 | TEST(ApplyPathMappingTests, MapsOutbound) { |
177 | auto Params = llvm::json::parse(JSON: R"({ |
178 | "id": 1, |
179 | "result": [ |
180 | {"uri": "file:///opt/include/foo.h"}, |
181 | {"uri": "file:///workarea/src/foo.cpp"}] |
182 | })" ); |
183 | auto ExpectedParams = llvm::json::parse(JSON: R"({ |
184 | "id": 1, |
185 | "result": [ |
186 | {"uri": "file:///home/.includes/foo.h"}, |
187 | {"uri": "file:///home/src/foo.cpp"}] |
188 | })" ); |
189 | auto Mappings = |
190 | parsePathMappings(RawPathMappings: "/home=/workarea,/home/.includes=/opt/include" ); |
191 | ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); |
192 | applyPathMappings(Params&: *Params, Dir: PathMapping::Direction::ServerToClient, Mappings: *Mappings); |
193 | EXPECT_EQ(*Params, *ExpectedParams); |
194 | } |
195 | |
196 | TEST(ApplyPathMappingTests, MapsKeys) { |
197 | auto Params = llvm::json::parse(JSON: R"({ |
198 | "changes": { |
199 | "file:///home/foo.cpp": {"newText": "..."}, |
200 | "file:///home/src/bar.cpp": {"newText": "..."} |
201 | } |
202 | })" ); |
203 | auto ExpectedParams = llvm::json::parse(JSON: R"({ |
204 | "changes": { |
205 | "file:///workarea/foo.cpp": {"newText": "..."}, |
206 | "file:///workarea/src/bar.cpp": {"newText": "..."} |
207 | } |
208 | })" ); |
209 | auto Mappings = parsePathMappings(RawPathMappings: "/home=/workarea" ); |
210 | ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); |
211 | applyPathMappings(Params&: *Params, Dir: PathMapping::Direction::ClientToServer, Mappings: *Mappings); |
212 | EXPECT_EQ(*Params, *ExpectedParams); |
213 | } |
214 | |
215 | } // namespace |
216 | } // namespace clangd |
217 | } // namespace clang |
218 | |