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