View Javadoc
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         // default behavior is to only lookup map entry sent in the script
377         // here "en" entry is not sent and not detected.
378         update(b, false);
379 
380         // Now uses the equals handler to properly reset the map keys
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         // Calling a second time should be a noop
394         updated = update(b, false);
395     }
396 
397     @Test
398     @SuppressWarnings("unchecked")
399     public void testReplaceMapEdgeCases() throws IOException {
400         indexSeedData();
401         // Test that indexed content is not a map
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         // Inverse case, indexed content is a map, new doc is not
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         // Test that indexed content does not exist
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          * Tests path matching.
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      * Builds the xcontent for the request parameters for a single field.
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                 // We test doc updates we do not care about mapping and types
498                 // Disabling dynamic mapping so that we are free to experiment
499                 // with edge cases (changing types)
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 }