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