1 /*
2  * Copyright 2015-present Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may
5  * not use this file except in compliance with the License. You may obtain
6  * a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations
14  * under the License.
15  */
16 package com.facebook.watchman;
17 
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.nio.file.Paths;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.concurrent.CountDownLatch;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.atomic.AtomicReference;
30 
31 import com.facebook.watchman.bser.BserDeserializer;
32 
33 import com.google.common.base.Supplier;
34 import com.google.common.collect.ImmutableMap;
35 import org.hamcrest.Matchers;
36 import org.junit.Assert;
37 import org.junit.Before;
38 import org.junit.Rule;
39 import org.junit.Test;
40 import org.junit.rules.ExpectedException;
41 import org.mockito.Mockito;
42 
43 public class WatchmanClientTest extends WatchmanTestBase {
44 
45   private WatchmanClient mClient;
46   private Boolean withWatchProject;
47 
48   @Rule
49   public ExpectedException thrown = ExpectedException.none();
50 
51   /* @Before methods in subclasses are run AFTER @Before methods of the base class */
52   @Before
setClient()53   public void setClient() throws IOException {
54     withWatchProject = true;
55     mClient = new WatchmanClientImpl(
56         mIncomingMessageGetter,
57         mOutgoingMessageStream,
58         new Supplier<Boolean>() {
59           @Override
60           public Boolean get() {
61             return withWatchProject;
62           }
63         });
64     mClient.start();
65   }
66 
67   @SuppressWarnings("unchecked")
68   @Test
subscribeTriggersListenerTest()69   public void subscribeTriggersListenerTest() throws InterruptedException {
70     Map<String, Object> subscriptionReply = new HashMap<>();
71     subscriptionReply.put("version", "1.2.3");
72     // "subscribe" value should be private to WatchmanClient so we could use any mock string
73     subscriptionReply.put("subscribe", "sub-0");
74     mObjectQueue.put(subscriptionReply);
75 
76     Map<String, Object> subscribeEvent = new HashMap<>();
77     subscribeEvent.put("version", "1.2.3");
78     subscribeEvent.put("clock", "c:123:1234");
79     subscribeEvent.put("files", Arrays.asList("/foo/bar", "/foo/baz"));
80     subscribeEvent.put("root", "/foo");
81     subscribeEvent.put("subscription", "sub-0");
82     mObjectQueue.put(subscribeEvent);
83 
84     final CountDownLatch latch = new CountDownLatch(1);
85     final AtomicReference<Map<String, Object>> result = new AtomicReference<>();
86     mClient.subscribe(Paths.get("/foo"), null, new Callback() {
87       @Override
88       public void call(Map<String, Object> event) {
89         result.set(event);
90         latch.countDown();
91       }
92     });
93 
94     if (! latch.await(10, TimeUnit.SECONDS)) {
95       Assert.fail();
96     }
97 
98     deepObjectEquals(subscribeEvent, result.get());
99   }
100 
101   /**
102    * Test the case when we get a unilateral message from Watchman (a subscription update event)
103    * before the answer to the command we have just sent. We expect that the response to the watch
104    * request is delivered, and not the subscription update event.
105    */
106   @SuppressWarnings("unchecked")
107   @Test
watchProjectWithUnilateralTest()108   public void watchProjectWithUnilateralTest() throws ExecutionException, InterruptedException {
109     Map<String, Object> dummyUnilateralMessage = new HashMap<>();
110     dummyUnilateralMessage.put("version", "1.2.3");
111     dummyUnilateralMessage.put("clock", "c:123:1234");
112     dummyUnilateralMessage.put("files", Arrays.asList("/foo/bar", "/foo/baz"));
113     dummyUnilateralMessage.put("root", "/foo");
114     dummyUnilateralMessage.put("subscription", "sub-0");
115     mObjectQueue.put(dummyUnilateralMessage);
116 
117     Map<String, Object> mockResponse = new HashMap<>();
118     mockResponse.put("version", "1.2.3");
119     mockResponse.put("watch", "/foo/bar");
120     mockResponse.put("relative_path", "/foo");
121     mObjectQueue.put(mockResponse);
122 
123     Map<String, Object> receivedResponse = mClient.watch(Paths.get("/foo/bar")).get();
124     deepObjectEquals(mockResponse, receivedResponse);
125   }
126 
127   /**
128    * Test that the watch-project request sent by WatchmanClient respects the interface of Watchman
129    */
130   @SuppressWarnings("unchecked")
131   @Test
watchProjectRequestTest()132   public void watchProjectRequestTest() throws IOException, ExecutionException, InterruptedException {
133     String PATH = "/foo/bar";
134 
135     mObjectQueue.put(new HashMap<String, Object>()); // response irrelevant
136     mClient.watch(Paths.get(PATH)).get();
137 
138     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
139     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
140     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
141 
142     //noinspection RedundantCast
143     deepObjectEquals(
144         Arrays.<Object>asList("watch-project", PATH),
145         request);
146   }
147 
148   /**
149    * Test that the watch-del request sent by WatchmanClient respects the interface of Watchman
150    */
151   @SuppressWarnings("unchecked")
152   @Test
watchDelRequestTest()153   public void watchDelRequestTest() throws IOException, ExecutionException, InterruptedException {
154     String PATH = "/foo/bar";
155 
156     mObjectQueue.put(new HashMap<String, Object>()); // response irrelevant
157     mClient.watchDel(Paths.get(PATH)).get();
158 
159     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
160     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
161     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
162 
163     //noinspection RedundantCast
164     deepObjectEquals(
165         Arrays.<Object>asList("watch-del", PATH),
166         request);
167   }
168 
169   /**
170    * Test that requesting a watch when watch-project is unavailable throws a WatchmanException whose
171    * message mentions upgrading Watchman.
172    */
173   @SuppressWarnings("unchecked")
174   @Test
watchRequestTest()175   public void watchRequestTest() throws IOException, ExecutionException, InterruptedException {
176     String PATH = "/foo/bar";
177     thrown.expect(ExecutionException.class);
178     thrown.expectCause(Matchers.allOf(
179         Matchers.isA(WatchmanException.class),
180         Matchers.hasToString(
181             Matchers.containsString("upgrade"))));
182 
183     withWatchProject = false;
184     mClient.watch(Paths.get(PATH)).get(); // throws
185   }
186 
187   /**
188    * Test that the subscribe request sent by WatchmanClient respects the interface of Watchman
189    */
190   @SuppressWarnings("unchecked")
191   @Test
subscribeRequestTest()192   public void subscribeRequestTest() throws ExecutionException, InterruptedException, IOException {
193     final String PATH = "/foo/bar";
194     final String NAME = "sub-0";
195     Callback mockListener = Mockito.mock(Callback.class);
196 
197     Map<String, Object> response = new HashMap<>();
198     response.put("subscribe", "name");
199     mObjectQueue.put(response); // response irrelevant
200     mClient.subscribe(
201         Paths.get(PATH),
202         new HashMap<String, Object>(),
203         mockListener).get();
204 
205     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
206     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
207     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
208     deepObjectEquals(
209         Arrays.<Object>asList("subscribe", PATH, NAME, new HashMap<String, Object>()),
210         request);
211   }
212 
213   /**
214    * Test that the unsubscribe request sent by WatchmanClient respects the interface of Watchman
215    */
216   @SuppressWarnings("unchecked")
217   @Test
unsubscribeRequestTest()218   public void unsubscribeRequestTest()
219       throws ExecutionException, InterruptedException, IOException {
220     final String PATH = "/foo/bar";
221     final String NAME = "sub-0";
222 
223     Map<String, Object> response = new HashMap<>();
224     response.put("deleted", true);
225     mObjectQueue.put(response); // response irrelevant
226     mObjectQueue.put(response); // response irrelevant
227 
228     Callback mockListener = Mockito.mock(Callback.class);
229     WatchmanClient.SubscriptionDescriptor descriptor = mClient.subscribe(
230         Paths.get(PATH),
231         new HashMap<String, Object>(),
232         mockListener).get();
233     mOutgoingMessageStream.reset(); // ignore the subscribe command
234     mClient.unsubscribe(descriptor).get();
235 
236     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
237     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
238     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
239     deepObjectEquals(
240         Arrays.<Object>asList("unsubscribe", PATH, NAME),
241         request);
242   }
243 
244   /**
245    * Test that the clock request sent by WatchmanClient respects the interface of Watchman
246    */
247   @SuppressWarnings("unchecked")
248   @Test
clockRequestWithoutTimeoutTest()249   public void clockRequestWithoutTimeoutTest() throws ExecutionException, InterruptedException, IOException {
250     final String PATH = "/foo/bar";
251 
252     Map<String, Object> response = new HashMap<>();
253     response.put("clock", "some value");
254     mObjectQueue.put(response); // response irrelevant
255 
256     mClient.clock(Paths.get(PATH)).get();
257 
258     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
259     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
260     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
261     deepObjectEquals(
262         Arrays.<Object>asList("clock", PATH),
263         request);
264   }
265 
266   /**
267    * Test that the clock request sent by WatchmanClient respects the interface of Watchman, when
268    * sync_timeout is also required
269    */
270   @SuppressWarnings("unchecked")
271   @Test
clockRequestWithTimeoutTest()272   public void clockRequestWithTimeoutTest() throws ExecutionException, InterruptedException, IOException {
273     final String PATH = "/foo/bar";
274     final Short SYNC_TIMEOUT = 1500;
275 
276     Map<String, Object> response = new HashMap<>();
277     response.put("clock", "some value");
278     mObjectQueue.put(response); // response irrelevant
279 
280     mClient.clock(Paths.get(PATH), SYNC_TIMEOUT).get();
281 
282     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
283     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
284     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
285     deepObjectEquals(
286         Arrays.<Object>asList(
287             "clock",
288             PATH,
289             ImmutableMap.<String, Object>of("sync_timeout", SYNC_TIMEOUT)),
290         request);
291   }
292 
293 
294   /**
295    * Test that the version request sent by WatchmanClient respects the interface of Watchman
296    */
297   @SuppressWarnings("unchecked")
298   @Test
versionRequestTestNoCapabilities()299   public void versionRequestTestNoCapabilities()
300       throws ExecutionException, InterruptedException, IOException {
301     Map<String, Object> response = new HashMap<>();
302     response.put("version", "1.2.3");
303     mObjectQueue.put(response); // response irrelevant
304 
305     mClient.version().get();
306 
307     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
308     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
309     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
310     deepObjectEquals(
311         Arrays.<Object>asList("version"),
312         request);
313   }
314 
315   /**
316    * Test that the version request sent by WatchmanClient respects the interface of Watchman
317    */
318   @SuppressWarnings("unchecked")
319   @Test
versionRequestTestWithCapabilities()320   public void versionRequestTestWithCapabilities()
321       throws ExecutionException, InterruptedException, IOException {
322     Map<String, Object> response = new HashMap<>();
323     response.put("version", "1.2.3");
324     mObjectQueue.put(response); // response irrelevant
325 
326     List<String> optionalCapabilities = Collections.singletonList("optional1");
327     List<String> requiredCapabilities = Arrays.asList("required1", "required2");
328     mClient.version(optionalCapabilities, requiredCapabilities).get();
329 
330     Map<String, Object> expectedCapabilitiesMap = new HashMap<>();
331     expectedCapabilitiesMap.put("optional", optionalCapabilities);
332     expectedCapabilitiesMap.put("required", requiredCapabilities);
333 
334     ByteArrayInputStream in = new ByteArrayInputStream(mOutgoingMessageStream.toByteArray());
335     BserDeserializer deserializer = new BserDeserializer(BserDeserializer.KeyOrdering.UNSORTED);
336     List<Object> request = (List<Object>) deserializer.deserializeBserValue(in);
337     deepObjectEquals(
338         Arrays.<Object>asList("version", expectedCapabilitiesMap),
339         request);
340   }
341 }
342