/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.index.mapper; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.index.fielddata.FieldDataType; import org.elasticsearch.index.similarity.BM25SimilarityProvider; import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** Base test case for subclasses of MappedFieldType */ public abstract class FieldTypeTestCase extends ESTestCase { /** Abstraction for mutating a property of a MappedFieldType */ public static abstract class Modifier { /** The name of the property that is being modified. Used in test failure messages. */ public final String property; /** true if this modifier only makes types incompatible in strict mode, false otherwise */ public final boolean strictOnly; /** true if reversing the order of checkCompatibility arguments should result in the same conflicts, false otherwise **/ public final boolean symmetric; public Modifier(String property, boolean strictOnly, boolean symmetric) { this.property = property; this.strictOnly = strictOnly; this.symmetric = symmetric; } /** Modifies the property */ public abstract void modify(MappedFieldType ft); /** * Optional method to implement that allows the field type that will be compared to be modified, * so that it does not have the default value for the property being modified. */ public void normalizeOther(MappedFieldType other) {} } private final List<Modifier> modifiers = new ArrayList<>(Arrays.asList( new Modifier("boost", true, true) { @Override public void modify(MappedFieldType ft) { ft.setBoost(1.1f); } }, new Modifier("doc_values", false, false) { @Override public void modify(MappedFieldType ft) { ft.setHasDocValues(ft.hasDocValues() == false); } }, new Modifier("analyzer", false, true) { @Override public void modify(MappedFieldType ft) { ft.setIndexAnalyzer(new NamedAnalyzer("bar", new StandardAnalyzer())); } }, new Modifier("analyzer", false, true) { @Override public void modify(MappedFieldType ft) { ft.setIndexAnalyzer(new NamedAnalyzer("bar", new StandardAnalyzer())); } @Override public void normalizeOther(MappedFieldType other) { other.setIndexAnalyzer(new NamedAnalyzer("foo", new StandardAnalyzer())); } }, new Modifier("search_analyzer", true, true) { @Override public void modify(MappedFieldType ft) { ft.setSearchAnalyzer(new NamedAnalyzer("bar", new StandardAnalyzer())); } }, new Modifier("search_analyzer", true, true) { @Override public void modify(MappedFieldType ft) { ft.setSearchAnalyzer(new NamedAnalyzer("bar", new StandardAnalyzer())); } @Override public void normalizeOther(MappedFieldType other) { other.setSearchAnalyzer(new NamedAnalyzer("foo", new StandardAnalyzer())); } }, new Modifier("search_quote_analyzer", true, true) { @Override public void modify(MappedFieldType ft) { ft.setSearchQuoteAnalyzer(new NamedAnalyzer("bar", new StandardAnalyzer())); } }, new Modifier("search_quote_analyzer", true, true) { @Override public void modify(MappedFieldType ft) { ft.setSearchQuoteAnalyzer(new NamedAnalyzer("bar", new StandardAnalyzer())); } @Override public void normalizeOther(MappedFieldType other) { other.setSearchQuoteAnalyzer(new NamedAnalyzer("foo", new StandardAnalyzer())); } }, new Modifier("similarity", false, true) { @Override public void modify(MappedFieldType ft) { ft.setSimilarity(new BM25SimilarityProvider("foo", Settings.EMPTY)); } }, new Modifier("similarity", false, true) { @Override public void modify(MappedFieldType ft) { ft.setSimilarity(new BM25SimilarityProvider("foo", Settings.EMPTY)); } @Override public void normalizeOther(MappedFieldType other) { other.setSimilarity(new BM25SimilarityProvider("bar", Settings.EMPTY)); } }, new Modifier("norms.loading", true, true) { @Override public void modify(MappedFieldType ft) { ft.setNormsLoading(MappedFieldType.Loading.LAZY); } }, new Modifier("fielddata", true, true) { @Override public void modify(MappedFieldType ft) { ft.setFieldDataType(new FieldDataType("foo", Settings.builder().put("loading", "eager").build())); } }, new Modifier("null_value", true, true) { @Override public void modify(MappedFieldType ft) { ft.setNullValue(dummyNullValue); } } )); /** * Add a mutation that will be tested for all expected semantics of equality and compatibility. * These should be added in an @Before method. */ protected void addModifier(Modifier modifier) { modifiers.add(modifier); } private Object dummyNullValue = "dummyvalue"; /** Sets the null value used by the modifier for null value testing. This should be set in an @Before method. */ protected void setDummyNullValue(Object value) { dummyNullValue = value; } /** Create a default constructed fieldtype */ protected abstract MappedFieldType createDefaultFieldType(); MappedFieldType createNamedDefaultFieldType() { MappedFieldType fieldType = createDefaultFieldType(); fieldType.setNames(new MappedFieldType.Names("foo")); return fieldType; } // TODO: remove this once toString is no longer final on FieldType... protected void assertFieldTypeEquals(String property, MappedFieldType ft1, MappedFieldType ft2) { if (ft1.equals(ft2) == false) { fail("Expected equality, testing property " + property + "\nexpected: " + toString(ft1) + "; \nactual: " + toString(ft2) + "\n"); } } protected void assertFieldTypeNotEquals(String property, MappedFieldType ft1, MappedFieldType ft2) { if (ft1.equals(ft2)) { fail("Expected inequality, testing property " + property + "\nfirst: " + toString(ft1) + "; \nsecond: " + toString(ft2) + "\n"); } } protected void assertCompatible(String msg, MappedFieldType ft1, MappedFieldType ft2, boolean strict) { List<String> conflicts = new ArrayList<>(); ft1.checkCompatibility(ft2, conflicts, strict); assertTrue("Found conflicts for " + msg + ": " + conflicts, conflicts.isEmpty()); } protected void assertNotCompatible(String msg, MappedFieldType ft1, MappedFieldType ft2, boolean strict, String... messages) { assert messages.length != 0; List<String> conflicts = new ArrayList<>(); ft1.checkCompatibility(ft2, conflicts, strict); for (String message : messages) { boolean found = false; for (String conflict : conflicts) { if (conflict.contains(message)) { found = true; } } assertTrue("Missing conflict for " + msg + ": [" + message + "] in conflicts " + conflicts, found); } } protected String toString(MappedFieldType ft) { return "MappedFieldType{" + "names=" + ft.names() + ", boost=" + ft.boost() + ", docValues=" + ft.hasDocValues() + ", indexAnalyzer=" + ft.indexAnalyzer() + ", searchAnalyzer=" + ft.searchAnalyzer() + ", searchQuoteAnalyzer=" + ft.searchQuoteAnalyzer() + ", similarity=" + ft.similarity() + ", normsLoading=" + ft.normsLoading() + ", fieldDataType=" + ft.fieldDataType() + ", nullValue=" + ft.nullValue() + ", nullValueAsString='" + ft.nullValueAsString() + "'" + "} " + super.toString(); } public void testClone() { MappedFieldType fieldType = createNamedDefaultFieldType(); MappedFieldType clone = fieldType.clone(); assertNotSame(clone, fieldType); assertEquals(clone.getClass(), fieldType.getClass()); assertEquals(clone, fieldType); assertEquals(clone, clone.clone()); // transitivity for (Modifier modifier : modifiers) { fieldType = createNamedDefaultFieldType(); modifier.modify(fieldType); clone = fieldType.clone(); assertNotSame(clone, fieldType); assertFieldTypeEquals(modifier.property, clone, fieldType); } } public void testEquals() { MappedFieldType ft1 = createNamedDefaultFieldType(); MappedFieldType ft2 = createNamedDefaultFieldType(); assertEquals(ft1, ft1); // reflexive assertEquals(ft1, ft2); // symmetric assertEquals(ft2, ft1); assertEquals(ft1.hashCode(), ft2.hashCode()); for (Modifier modifier : modifiers) { ft1 = createNamedDefaultFieldType(); ft2 = createNamedDefaultFieldType(); modifier.modify(ft2); assertFieldTypeNotEquals(modifier.property, ft1, ft2); assertNotEquals("hash code for modified property " + modifier.property, ft1.hashCode(), ft2.hashCode()); // modify the same property and they are equal again modifier.modify(ft1); assertFieldTypeEquals(modifier.property, ft1, ft2); assertEquals("hash code for modified property " + modifier.property, ft1.hashCode(), ft2.hashCode()); } } public void testFreeze() { for (Modifier modifier : modifiers) { MappedFieldType fieldType = createNamedDefaultFieldType(); fieldType.freeze(); try { modifier.modify(fieldType); fail("expected already frozen exception for property " + modifier.property); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("already frozen")); } } } public void testCheckTypeName() { final MappedFieldType fieldType = createNamedDefaultFieldType(); List<String> conflicts = new ArrayList<>(); fieldType.checkCompatibility(fieldType, conflicts, random().nextBoolean()); // no exception assertTrue(conflicts.toString(), conflicts.isEmpty()); MappedFieldType bogus = new MappedFieldType() { @Override public MappedFieldType clone() {return null;} @Override public String typeName() { return fieldType.typeName();} }; try { fieldType.checkCompatibility(bogus, conflicts, random().nextBoolean()); fail("expected bad types exception"); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("Type names equal")); } assertTrue(conflicts.toString(), conflicts.isEmpty()); MappedFieldType other = new MappedFieldType() { @Override public MappedFieldType clone() {return null;} @Override public String typeName() { return "othertype";} }; try { fieldType.checkCompatibility(other, conflicts, random().nextBoolean()); fail(); } catch (IllegalArgumentException e) { assertTrue(e.getMessage(), e.getMessage().contains("cannot be changed from type")); } assertTrue(conflicts.toString(), conflicts.isEmpty()); } public void testCheckCompatibility() { MappedFieldType ft1 = createNamedDefaultFieldType(); MappedFieldType ft2 = createNamedDefaultFieldType(); assertCompatible("default", ft1, ft2, true); assertCompatible("default", ft1, ft2, false); assertCompatible("default", ft2, ft1, true); assertCompatible("default", ft2, ft1, false); for (Modifier modifier : modifiers) { ft1 = createNamedDefaultFieldType(); ft2 = createNamedDefaultFieldType(); modifier.normalizeOther(ft1); modifier.modify(ft2); if (modifier.strictOnly) { String[] conflicts = { "mapper [foo] is used by multiple types", "update [" + modifier.property + "]" }; assertCompatible(modifier.property, ft1, ft2, false); assertNotCompatible(modifier.property, ft1, ft2, true, conflicts); assertCompatible(modifier.property, ft2, ft1, false); // always symmetric when not strict if (modifier.symmetric) { assertNotCompatible(modifier.property, ft2, ft1, true, conflicts); } else { assertCompatible(modifier.property, ft2, ft1, true); } } else { // not compatible whether strict or not String conflict = "different [" + modifier.property + "]"; assertNotCompatible(modifier.property, ft1, ft2, true, conflict); assertNotCompatible(modifier.property, ft1, ft2, false, conflict); if (modifier.symmetric) { assertNotCompatible(modifier.property, ft2, ft1, true, conflict); assertNotCompatible(modifier.property, ft2, ft1, false, conflict); } else { assertCompatible(modifier.property, ft2, ft1, true); assertCompatible(modifier.property, ft2, ft1, false); } } } } }