1 package org.wikimedia.search.extra.superdetectnoop;
2
3 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
4 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
5 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertRequestBuilderThrows;
6 import static org.hamcrest.Matchers.anything;
7 import static org.hamcrest.Matchers.equalTo;
8 import static org.hamcrest.Matchers.hasEntry;
9 import static org.hamcrest.Matchers.instanceOf;
10 import static org.hamcrest.Matchers.not;
11
12 import java.io.IOException;
13 import java.util.Arrays;
14 import java.util.Iterator;
15 import java.util.Map;
16 import java.util.function.Function;
17
18 import org.elasticsearch.action.DocWriteResponse;
19 import org.elasticsearch.action.index.IndexResponse;
20 import org.elasticsearch.action.support.WriteRequest;
21 import org.elasticsearch.action.update.UpdateRequestBuilder;
22 import org.elasticsearch.action.update.UpdateResponse;
23 import org.elasticsearch.common.bytes.BytesReference;
24 import org.elasticsearch.common.xcontent.XContentBuilder;
25 import org.elasticsearch.common.xcontent.XContentHelper;
26 import org.elasticsearch.common.xcontent.XContentType;
27 import org.elasticsearch.rest.RestStatus;
28 import org.elasticsearch.script.Script;
29 import org.elasticsearch.script.ScriptType;
30 import org.hamcrest.Matcher;
31 import org.junit.Test;
32 import org.wikimedia.search.extra.AbstractPluginIntegrationTest;
33
34 import com.google.common.base.Splitter;
35 import com.google.common.collect.ImmutableList;
36 import com.google.common.collect.ImmutableMap;
37
38 public class SuperDetectNoopScriptIntegrationTest extends AbstractPluginIntegrationTest {
39 @Test
40 public void newField() throws IOException {
41 indexSeedData();
42 XContentBuilder b = x("bar", 2);
43 Map<String, Object> r = update(b, true);
44 assertThat(r, hasEntry("int", (Object) 3));
45 assertThat(r, hasEntry("bar", (Object) 2));
46 }
47
48 @Test
49 public void notModified() throws IOException {
50 indexSeedData();
51 XContentBuilder b = x("int", 3);
52 Map<String, Object> r = update(b, false);
53 assertThat(r, hasEntry("int", (Object) 3));
54 }
55
56 @Test
57 public void assignToNull() throws IOException {
58 indexSeedData();
59 XContentBuilder b = x("int", null);
60 Map<String, Object> r = update(b, true);
61 assertThat(r, not(hasEntry(equalTo("int"), anything())));
62 }
63
64 @Test
65 public void newValue() throws IOException {
66 indexSeedData();
67 XContentBuilder b = x("int", 2);
68 Map<String, Object> r = update(b, true);
69 assertThat(r, hasEntry("int", (Object) 2));
70 }
71
72 @Test
73 public void withinPercentage() throws IOException {
74 indexSeedData();
75 XContentBuilder b = x("int", 5, "within 200%");
76 Map<String, Object> r = update(b, false);
77 assertThat(r, hasEntry("int", (Object) 3));
78 }
79
80 @Test
81 public void withinPercentageNegative() throws IOException {
82 indexSeedData();
83 XContentBuilder b = x("int", -1, "within 200%");
84 Map<String, Object> r = update(b, false);
85 assertThat(r, hasEntry("int", (Object) 3));
86 }
87
88 @Test
89 public void outsidePercentage() throws IOException {
90 indexSeedData();
91 XContentBuilder b = x("int", 9, "within 200%");
92 Map<String, Object> r = update(b, true);
93 assertThat(r, hasEntry("int", (Object) 9));
94 }
95
96 @Test
97 public void outsidePercentageNegative() throws IOException {
98 indexSeedData();
99 XContentBuilder b = x("int", -3, "within 200%");
100 Map<String, Object> r = update(b, true);
101 assertThat(r, hasEntry("int", (Object) (-3)));
102 }
103
104 @Test
105 public void withinPercentageZeroMatch() throws IOException {
106 indexSeedData();
107 XContentBuilder b = x("zero", 0, "within 200%");
108 Map<String, Object> r = update(b, false);
109 assertThat(r, hasEntry("zero", (Object) 0));
110 }
111
112 @Test
113 public void withinPercentageZeroChanged() throws IOException {
114 indexSeedData();
115 XContentBuilder b = x("zero", 1, "within 200%");
116 Map<String, Object> r = update(b, true);
117 assertThat(r, hasEntry("zero", (Object) 1));
118 }
119
120 @Test
121 public void percentageOnString() throws IOException {
122 indexSeedData();
123 XContentBuilder b = x("string", "cat", "within 200%");
124 Map<String, Object> r = update(b, true);
125 assertThat(r, hasEntry("int", (Object) 3));
126 assertThat(r, hasEntry("string", (Object) "cat"));
127 }
128
129 @Test
130 public void withinAbsolute() throws IOException {
131 indexSeedData();
132 XContentBuilder b = x("int", 4, "within 2");
133 Map<String, Object> r = update(b, false);
134 assertThat(r, hasEntry("int", (Object) 3));
135 }
136
137 @Test
138 public void withinAbsoluteNegative() throws IOException {
139 indexSeedData();
140 XContentBuilder b = x("int", -1, "within 7");
141 Map<String, Object> r = update(b, false);
142 assertThat(r, hasEntry("int", (Object) 3));
143 }
144
145 @Test
146 public void outsideAbsolute() throws IOException {
147 indexSeedData();
148 XContentBuilder b = x("int", 5, "within 2");
149 Map<String, Object> r = update(b, true);
150 assertThat(r, hasEntry("int", (Object) 5));
151 }
152
153 @Test
154 public void outsideAbsoluteNegative() throws IOException {
155 indexSeedData();
156 XContentBuilder b = x("int", -4, "within 7");
157 Map<String, Object> r = update(b, true);
158 assertThat(r, hasEntry("int", (Object) (-4)));
159 }
160
161 @Test
162 public void absoluteOnString() throws IOException {
163 indexSeedData();
164 XContentBuilder b = x("string", "cat", "within 2");
165 Map<String, Object> r = update(b, true);
166 assertThat(r, hasEntry("int", (Object) 3));
167 assertThat(r, hasEntry("string", (Object) "cat"));
168 }
169
170 @Test
171 public void setNewField() throws IOException {
172 indexSeedData();
173 XContentBuilder b = x("another_set", ImmutableMap.of("add", ImmutableList.of("cat", "tree")), "set");
174 Map<String, Object> r = update(b, true);
175 assertThat(r, hasEntry("another_set", (Object) ImmutableList.of("cat", "tree")));
176 }
177
178 @Test
179 public void setNewFieldRemoveDoesntAddField() throws IOException {
180 indexSeedData();
181 XContentBuilder b = x("another_set", ImmutableMap.of("remove", "cat"), "set");
182 Map<String, Object> r = update(b, false);
183 assertThat(r, not(hasEntry(equalTo("another_set"), anything())));
184 }
185
186 @Test
187 public void setNullRemovesField() throws IOException {
188 indexSeedData();
189 XContentBuilder b = x("set", null, "set");
190 Map<String, Object> r = update(b, true);
191 assertThat(r, not(hasEntry(equalTo("set"), anything())));
192 }
193
194 @Test
195 public void setNoop() throws IOException {
196 indexSeedData();
197 XContentBuilder b = x("set", ImmutableMap.of("add", "cat"), "set");
198 Map<String, Object> r = update(b, false);
199 assertThat(r, hasEntry("set", (Object) ImmutableList.of("cat", "dog", "fish")));
200 }
201
202 @Test
203 public void setNoopFromRemove() throws IOException {
204 indexSeedData();
205 XContentBuilder b = x("set", ImmutableMap.of("remove", "tree"), "set");
206 Map<String, Object> r = update(b, false);
207 assertThat(r, hasEntry("set", (Object) ImmutableList.of("cat", "dog", "fish")));
208 }
209
210 @Test
211 public void setAdd() throws IOException {
212 indexSeedData();
213 XContentBuilder b = x("set", ImmutableMap.of("add", "cow"), "set");
214 Map<String, Object> r = update(b, true);
215 assertThat(r, hasEntry("set", (Object) ImmutableList.of("cat", "dog", "fish", "cow")));
216 }
217
218 @Test
219 public void setRemove() throws IOException {
220 indexSeedData();
221 XContentBuilder b = x("set", ImmutableMap.of("remove", "fish"), "set");
222 Map<String, Object> r = update(b, true);
223 assertThat(r, hasEntry("set", (Object) ImmutableList.of("cat", "dog")));
224 }
225
226 @Test
227 public void setAddAndRemove() throws IOException {
228 indexSeedData();
229 XContentBuilder b = x("set", ImmutableMap.of("add", "cow", "remove", "fish"), "set");
230 Map<String, Object> r = update(b, true);
231 assertThat(r, hasEntry("set", (Object) ImmutableList.of("cat", "dog", "cow")));
232 }
233
234 @Test
235 @SuppressWarnings({ "unchecked", "rawtypes" })
236 public void setNewFieldDeep() throws IOException {
237 indexSeedData();
238 XContentBuilder b = x("o.new_set", ImmutableMap.of("add", "cow", "remove", "fish"), "set");
239 Map<String, Object> r = update(b, true);
240 assertThat(r, hasEntry(equalTo("o"), (Matcher<Object>) (Matcher) hasEntry("new_set", ImmutableList.of("cow"))));
241 }
242
243 @Test
244 @SuppressWarnings({ "unchecked", "rawtypes" })
245 public void setAddFieldDeep() throws IOException {
246 indexSeedData();
247 XContentBuilder b = x("o.set", ImmutableMap.of("add", "cow", "remove", "fish"), "set");
248 Map<String, Object> r = update(b, true);
249 assertThat(r, hasEntry(equalTo("o"), (Matcher<Object>) (Matcher) hasEntry("set", ImmutableList.of("cow", "bat"))));
250 }
251
252 @Test
253 public void garbageDetector() throws IOException {
254 indexSeedData();
255 XContentBuilder b = x("int", "cat", "not a valid detector");
256 assertRequestBuilderThrows(toUpdateRequest(b), IllegalArgumentException.class, RestStatus.BAD_REQUEST);
257 }
258
259 @Test
260 public void noopDocumentWithLowerVersion() throws IOException {
261 indexSeedData();
262 XContentBuilder b = x("int", 1, "documentVersion");
263 update(b, false);
264 }
265
266 @Test
267 public void dontNoopDocumentWithEqualVersionAndDifferentData() throws IOException {
268 indexSeedData();
269 XContentBuilder b = jsonBuilder().startObject();
270 b.startObject("source");
271 {
272 b.field("string", "cheesecake");
273 b.field("int", 3);
274 }
275 b.endObject();
276 b.startObject("handlers");
277 {
278 b.field("int", "documentVersion");
279 }
280 b.endObject();
281 b.endObject();
282 update(b, true);
283 }
284
285 @Test
286 public void noopDocumentWithEqualVersionAndSameData() throws IOException {
287 indexSeedData();
288 XContentBuilder b = jsonBuilder().startObject();
289 b.startObject("source");
290 {
291 b.field("string", "cake");
292 b.field("int", 3);
293 }
294 b.endObject();
295 b.startObject("handlers");
296 {
297 b.field("int", "documentVersion");
298 }
299 b.endObject();
300 b.endObject();
301 update(b, false);
302 }
303
304
305 @Test
306 public void dontNoopDocumentWithMissingPrevVersion() throws IOException {
307 indexSeedData();
308 XContentBuilder b = x("nonexistent", 5, "documentVersion");
309 update(b, true);
310 }
311
312 @Test
313 public void dontNoopDocumentWithHigherVersion() throws IOException {
314 indexSeedData();
315 XContentBuilder b = x("int", 5, "documentVersion");
316 update(b, true);
317 }
318
319 @Test
320 public void dontNoopDocumentWithInvalidOldVersion() throws IOException {
321 indexSeedData();
322 XContentBuilder b = x("string", 5, "documentVersion");
323 update(b, true);
324 }
325
326 @Test
327 public void dontNoopDocumentWithMaximumVersion() throws IOException {
328 indexSeedData();
329 XContentBuilder b = x("int", 9223372036854775807L, "documentVersion");
330 update(b, true);
331 }
332
333 @Test
334 public void noopsDocumentWithOutOfBoundsVersion() throws IOException {
335 indexSeedData();
336 XContentBuilder b = x("int", 9223372036854775807L + 1L, "documentVersion");
337 update(b, false);
338 }
339
340 @Test
341 public void noopsEntireDocumentUpdate() throws IOException {
342 indexSeedData();
343 XContentBuilder b = jsonBuilder().startObject();
344 b.startObject("source");
345 {
346 b.field("string", "food");
347 b.field("int", 1);
348 }
349 b.endObject();
350 b.startObject("handlers");
351 {
352 b.field("int", "documentVersion");
353 }
354 b.endObject();
355 b.endObject();
356 update(b, false);
357 }
358
359 @Test
360 @SuppressWarnings("unchecked")
361 public void testReplaceMap() throws IOException {
362 indexSeedData();
363 Function<XContentBuilder, XContentBuilder> addSource = (builder) -> {
364 try {
365 return builder.startObject("source")
366 .startObject("labels")
367 .array("fr", "main", "poignet")
368 .endObject()
369 .endObject();
370 } catch (IOException ioe) {
371 throw new AssertionError(ioe);
372 }
373 };
374 XContentBuilder b = addSource.apply(jsonBuilder().startObject()).endObject();
375
376
377
378 update(b, false);
379
380
381 b = addSource.apply(jsonBuilder().startObject())
382 .startObject("handlers")
383 .field("labels", "equals")
384 .endObject()
385 .endObject();
386
387 Map<String, Object> updated = update(b, true);
388 assertThat(updated, hasEntry(equalTo("labels"), instanceOf(Map.class)));
389 Map<String, Object> labels = (Map<String, Object>) updated.get("labels");
390 assertFalse(labels.containsKey("en"));
391 assertTrue(labels.containsKey("fr"));
392 assertEquals(Arrays.asList("main", "poignet"), labels.get("fr"));
393
394 updated = update(b, false);
395 }
396
397 @Test
398 @SuppressWarnings("unchecked")
399 public void testReplaceMapEdgeCases() throws IOException {
400 indexSeedData();
401
402 XContentBuilder b = jsonBuilder().startObject()
403 .startObject("source")
404 .startObject("int")
405 .array("fr", "main", "poignet")
406 .endObject()
407 .endObject()
408 .startObject("handlers")
409 .field("int", "equals")
410 .endObject()
411 .endObject();
412 Map<String, Object> updated = update(b, true);
413 assertThat(updated, hasEntry(equalTo("int"), instanceOf(Map.class)));
414 Map<String, Object> labels = (Map<String, Object>) updated.get("int");
415 assertTrue(labels.containsKey("fr"));
416 assertEquals(Arrays.asList("main", "poignet"), labels.get("fr"));
417
418
419 b = jsonBuilder().startObject()
420 .startObject("source")
421 .field("int", 3)
422 .endObject()
423 .startObject("handlers")
424 .field("int", "equals")
425 .endObject()
426 .endObject();
427 updated = update(b, true);
428 assertThat(updated, hasEntry(equalTo("int"), instanceOf(Number.class)));
429 assertEquals(3, updated.get("int"));
430
431
432 b = jsonBuilder().startObject()
433 .startObject("source")
434 .startObject("unknown_field")
435 .array("fr", "main", "poignet")
436 .endObject()
437 .endObject()
438 .startObject("handlers")
439 .field("int", "equals")
440 .endObject()
441 .endObject();
442 updated = update(b, true);
443 assertThat(updated, hasEntry(equalTo("unknown_field"), instanceOf(Map.class)));
444 labels = (Map<String, Object>) updated.get("unknown_field");
445 assertTrue(labels.containsKey("fr"));
446 assertEquals(Arrays.asList("main", "poignet"), labels.get("fr"));
447 }
448
449
450
451
452 @Test
453 @SuppressWarnings({ "unchecked", "rawtypes" })
454 public void path() throws IOException {
455 indexSeedData();
456 XContentBuilder b = x("o.bar", 9, "within 10");
457 Map<String, Object> r = update(b, false);
458 assertThat(r, hasEntry(equalTo("o"), (Matcher<Object>) (Matcher) hasEntry("bar", 10)));
459 }
460
461 private XContentBuilder x(String field, Object value) throws IOException {
462 return x(field, value, null);
463 }
464
465
466
467
468 private XContentBuilder x(String field, Object value, String detector) throws IOException {
469 XContentBuilder b = jsonBuilder().startObject();
470 b.startObject("source");
471 xInPath(Splitter.on('.').split(field).iterator(), b, value);
472 b.endObject();
473 if (detector != null) {
474 b.startObject("handlers");
475 {
476 b.field(field, detector);
477 }
478 b.endObject();
479 }
480 b.endObject();
481 return b;
482 }
483
484 private void xInPath(Iterator<String> path, XContentBuilder b, Object value) throws IOException {
485 b.field(path.next());
486 if (path.hasNext()) {
487 b.startObject();
488 xInPath(path, b, value);
489 b.endObject();
490 } else {
491 b.value(value);
492 }
493 }
494
495 private void indexSeedData() throws IOException {
496 XContentBuilder mapping = jsonBuilder().startObject()
497
498
499
500 .startObject("test").field("dynamic", false).endObject()
501 .endObject();
502
503 assertAcked(prepareCreate("test").addMapping("test", mapping));
504 ensureGreen();
505
506 XContentBuilder b = jsonBuilder().startObject();
507 {
508 b.field("int", 3);
509 b.field("zero", 0);
510 b.field("string", "cake");
511 b.array("set", "cat", "dog", "fish");
512 b.startObject("o")
513 .field("bar", 10)
514 .array("set", "cow", "fish", "bat");
515 b.endObject();
516
517 b.startObject("labels")
518 .array("fr", "main", "poignet")
519 .array("en", "hand", "fist");
520 b.endObject();
521 }
522 b.endObject();
523
524 IndexResponse ir = client().prepareIndex("test", "test", "1").setSource(b).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
525 assertEquals("Test data is newly created", DocWriteResponse.Result.CREATED, ir.getResult());
526 }
527
528 private Map<String, Object> update(XContentBuilder b, boolean shouldUpdate) {
529 UpdateResponse resp = toUpdateRequest(b).get();
530 DocWriteResponse.Result expected = shouldUpdate ? DocWriteResponse.Result.UPDATED : DocWriteResponse.Result.NOOP;
531 assertEquals(expected, resp.getResult());
532 return client().prepareGet("test", "test", "1").get().getSource();
533 }
534
535 private UpdateRequestBuilder toUpdateRequest(XContentBuilder b) {
536 b.close();
537 Map<String, Object> m = XContentHelper.convertToMap(BytesReference.bytes(b), true, XContentType.JSON).v2();
538 Script script = new Script(ScriptType.INLINE, "super_detect_noop", "", m);
539 return client().prepareUpdate("test", "test", "1").setScript(script)
540 .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
541 }
542 }