This is an automated email from the ASF dual-hosted git repository.
zstan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new eb0f19d085c IGNITE-25747 Configuration compatibility. Support
annotated values validation. (#6225)
eb0f19d085c is described below
commit eb0f19d085c440e9d7a4a50d242d796c2f412644
Author: Max Zhuravkov <[email protected]>
AuthorDate: Tue Jul 15 18:48:50 2025 +0300
IGNITE-25747 Configuration compatibility. Support annotated values
validation. (#6225)
---
.../AnnotationCompatibilityValidator.java | 45 +++
.../compatibility/framework/ConfigAnnotation.java | 47 ++-
.../framework/ConfigAnnotationValue.java | 101 ++++++
.../framework/ConfigAnnotationsValidator.java | 178 +++++++++
.../compatibility/framework/ConfigNode.java | 11 +-
.../framework/ConfigNodeSerializer.java | 3 +
.../ConfigurationAnnotationValidatorSelfTest.java | 400 +++++++++++++++++++++
.../framework/ConfigurationTreeComparator.java | 39 +-
.../ConfigurationTreeComparatorSelfTest.java | 4 +-
.../framework/ConfigurationTreeScanner.java | 166 ++++++++-
...seAnnotationCompatibilityValidatorSelfTest.java | 48 +++
.../DefaultAnnotationCompatibilityValidator.java | 56 +++
...ExceptKeysAnnotationCompatibilityValidator.java | 39 ++
.../annotations/ExceptKeysValidatorTest.java | 73 ++++
.../OneOfAnnotationCompatibilityValidator.java | 46 +++
.../framework/annotations/OneOfValidatorTest.java | 96 +++++
.../RangeAnnotationCompatibilityValidator.java | 46 +++
.../framework/annotations/RangeValidatorTest.java | 101 ++++++
.../compatibility/configuration/snapshot.bin | Bin 3636 -> 4020 bytes
19 files changed, 1458 insertions(+), 41 deletions(-)
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/AnnotationCompatibilityValidator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/AnnotationCompatibilityValidator.java
new file mode 100644
index 00000000000..bbfaf70b878
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/AnnotationCompatibilityValidator.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Annotation compatibility validator.
+ */
+@FunctionalInterface
+public interface AnnotationCompatibilityValidator {
+ /** Validates compatibility between annotation revisions. */
+ void validate(ConfigAnnotation candidate, ConfigAnnotation current,
List<String> errors);
+
+ /**
+ * Reads a value of the given annotation property. Note that all int
values are stored as {@code long} values.
+ */
+ static <T> T getValue(ConfigAnnotation annotation, String name,
Function<ConfigAnnotationValue, T> parse) {
+ ConfigAnnotationValue value = annotation.properties().get(name);
+ try {
+ return parse.apply(value);
+ } catch (Exception e) {
+ String errorMessage = format("Unable to read annotation property.
Property: {}, annotation: {}", name, annotation.name());
+ throw new IllegalStateException(errorMessage, e);
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotation.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotation.java
index b8f841cac52..c3017860400 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotation.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotation.java
@@ -17,7 +17,10 @@
package org.apache.ignite.internal.configuration.compatibility.framework;
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -29,31 +32,53 @@ public class ConfigAnnotation {
@JsonProperty
private String name;
@JsonProperty
- private Map<String, String> properties;
+ private Map<String, ConfigAnnotationValue> properties = new
LinkedHashMap<>();
+ @SuppressWarnings("unused")
ConfigAnnotation() {
// Default constructor for Jackson deserialization.
}
- ConfigAnnotation(String name) {
- this(name, Map.of());
- }
-
- ConfigAnnotation(String name, Map<String, String> properties) {
+ ConfigAnnotation(String name, Map<String, ConfigAnnotationValue>
properties) {
this.name = name;
this.properties = properties;
}
- @Override
- public int hashCode() {
- return Objects.hashCode(name);
+ /**
+ * Returns annotation type name.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns values of annotation properties.
+ */
+ public Map<String, ConfigAnnotationValue> properties() {
+ return properties;
}
@Override
public boolean equals(Object o) {
- // TODO https://issues.apache.org/jira/browse/IGNITE-25747 Validate
annotations properly.
- return o != null && getClass() == o.getClass() &&
name.equals(((ConfigAnnotation) o).name);
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ConfigAnnotation that = (ConfigAnnotation) o;
+ return Objects.equals(name, that.name) && Objects.equals(properties,
that.properties);
+ }
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, properties);
+ }
+
+ String digest() {
+ // This method is used for metadata comparison,
+ // so we exclude values from the comparison.
+ return name + (properties == null || properties.isEmpty() ? ""
+ : properties.entrySet().stream()
+ .map(e -> format("{}=<{}>", e.getKey(),
e.getValue().typeName()))
+ .collect(Collectors.joining(",", "(", ")")));
}
@Override
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotationValue.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotationValue.java
new file mode 100644
index 00000000000..ebde01b8464
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotationValue.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Annotation property value.
+ */
+public class ConfigAnnotationValue {
+ @JsonProperty
+ private String typeName;
+ @JsonProperty
+ private Object value;
+
+ @SuppressWarnings("unused")
+ public ConfigAnnotationValue() {
+ // Default constructor for Jackson deserialization.
+ }
+
+ private ConfigAnnotationValue(String typeName, Object value) {
+ this.typeName = typeName;
+ this.value = value;
+ }
+
+ /**
+ * Creates a value for a non-array property.
+ *
+ * @param className Class name.
+ * @param value Value.
+ * @return Value.
+ */
+ public static ConfigAnnotationValue createValue(String className, Object
value) {
+ return new ConfigAnnotationValue(className, value);
+ }
+
+
+ /**
+ * Creates a value for an array property.
+ *
+ * @param elementClassName Element type name.
+ * @param elements Elements.
+ * @return Value.
+ */
+ public static ConfigAnnotationValue createArray(String elementClassName,
List<Object> elements) {
+ return new ConfigAnnotationValue(elementClassName + "[]", elements);
+ }
+
+ /**
+ * Returns a value.
+ */
+ public Object value() {
+ return value;
+ }
+
+ /**
+ * Returns a type name.
+ */
+ public String typeName() {
+ return typeName;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ConfigAnnotationValue that = (ConfigAnnotationValue) o;
+ return Objects.equals(typeName, that.typeName) &&
Objects.equals(value, that.value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(typeName, value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return typeName + ":" + value;
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotationsValidator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotationsValidator.java
new file mode 100644
index 00000000000..f96eb325825
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigAnnotationsValidator.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.validation.ExceptKeys;
+import org.apache.ignite.configuration.validation.OneOf;
+import org.apache.ignite.configuration.validation.Range;
+import
org.apache.ignite.internal.configuration.compatibility.framework.annotations.DefaultAnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.annotations.ExceptKeysAnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.annotations.OneOfAnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.annotations.RangeAnnotationCompatibilityValidator;
+
+/**
+ * Configuration annotation validator.
+ */
+final class ConfigAnnotationsValidator {
+
+ private static final DefaultAnnotationCompatibilityValidator
DEFAULT_VALIDATOR = new DefaultAnnotationCompatibilityValidator();
+
+ private final Map<String, AnnotationCompatibilityValidator> validators;
+
+ /** Constructor. */
+ ConfigAnnotationsValidator() {
+ this(Map.of(
+ Range.class.getName(), new
RangeAnnotationCompatibilityValidator(),
+ ExceptKeys.class.getName(), new
ExceptKeysAnnotationCompatibilityValidator(),
+ OneOf.class.getName(), new
OneOfAnnotationCompatibilityValidator()
+ ));
+ }
+
+ /** Constructor. */
+ ConfigAnnotationsValidator(Map<String, AnnotationCompatibilityValidator>
validators) {
+ this.validators = Map.copyOf(validators);
+ }
+
+ /**
+ * Validates annotations.
+ */
+ void validate(ConfigNode candidate, ConfigNode node, List<String> errors) {
+ List<ConfigAnnotation> currentAnnotations = node.annotations();
+ List<ConfigAnnotation> candidateAnnotations = candidate.annotations();
+
+ Map<String, ConfigAnnotation> currentMap = new HashMap<>();
+ for (ConfigAnnotation annotation : currentAnnotations) {
+ currentMap.put(annotation.name(), annotation);
+ }
+
+ Map<String, ConfigAnnotation> candidateMap = new HashMap<>();
+ for (ConfigAnnotation annotation : candidateAnnotations) {
+ candidateMap.put(annotation.name(), annotation);
+ }
+
+ Set<String> newAnnotations = new TreeSet<>();
+
+ for (String currentKey : currentMap.keySet()) {
+ if (!candidateMap.containsKey(currentKey)) {
+ newAnnotations.add(currentKey);
+ }
+ }
+
+ if (!newAnnotations.isEmpty()) {
+ errors.add("Adding annotations is not allowed. New annotations: "
+ newAnnotations);
+ }
+
+ Set<String> removedAnnotations = new TreeSet<>();
+
+ for (Map.Entry<String, ConfigAnnotation> entry :
candidateMap.entrySet()) {
+ ConfigAnnotation candidateAnnotation = entry.getValue();
+ ConfigAnnotation currentAnnotation =
currentMap.get(entry.getKey());
+
+ if (currentAnnotation == null) {
+ removedAnnotations.add(candidateAnnotation.name());
+ continue;
+ }
+
+ validateStructure(candidateAnnotation, currentAnnotation, errors);
+ }
+
+ if (!removedAnnotations.isEmpty()) {
+ errors.add("Removing annotations is not allowed. Removed
annotations: " + removedAnnotations);
+ }
+ }
+
+ private void validateStructure(ConfigAnnotation candidate,
ConfigAnnotation current, List<String> errors) {
+ assert candidate.name().equals(current.name()) : "Annotation name
mismatch";
+
+ Set<String> currentProperties = current.properties().keySet();
+ Set<String> candidateProperties = candidate.properties().keySet();
+
+ Set<String> removed = candidateProperties.stream()
+ .filter(c -> !currentProperties.contains(c))
+ .collect(Collectors.toSet());
+
+ Set<String> added = currentProperties.stream()
+ .filter(c -> !candidateProperties.contains(c))
+ .collect(Collectors.toSet());
+
+ if (!removed.isEmpty()) {
+ errors.add(candidate.name() + " removed properties " + new
TreeSet<>(removed));
+ }
+
+ if (!added.isEmpty()) {
+ errors.add(candidate.name() + " added properties " + new
TreeSet<>(added));
+ }
+
+ Set<String> changedTypes = new TreeSet<>();
+
+ for (Map.Entry<String, ConfigAnnotationValue> entry :
candidate.properties().entrySet()) {
+ ConfigAnnotationValue currentValue =
current.properties().get(entry.getKey());
+ if (currentValue == null) {
+ // Already an error, just skip it.
+ continue;
+ }
+
+ ConfigAnnotationValue candidateValue = entry.getValue();
+
+ if (!Objects.equals(candidateValue.typeName(),
currentValue.typeName())) {
+ changedTypes.add(entry.getKey());
+ }
+ }
+
+ if (!changedTypes.isEmpty()) {
+ errors.add(candidate.name() + " properties with changed types " +
changedTypes);
+ }
+
+ validateSpecificAnnotation(candidate, current, errors);
+ }
+
+ private void validateSpecificAnnotation(ConfigAnnotation candidate,
ConfigAnnotation current, List<String> errors) {
+ AnnotationCompatibilityValidator validator =
validators.getOrDefault(candidate.name(), DEFAULT_VALIDATOR);
+
+ List<String> newErrors = new ArrayList<>();
+ validator.validate(candidate, current, newErrors);
+
+ errors.addAll(newErrors);
+
+ // Report additional error iff the annotation has properties and there
is no custom validator.
+ if (!newErrors.isEmpty()) {
+ boolean hasProperties = !candidate.properties().isEmpty() ||
!current.properties().isEmpty();
+ if (hasProperties && validator == DEFAULT_VALIDATOR) {
+ String error = format("Annotation requires a custom
compatibility validator: {}. "
+ + "Consider using {} if every change should be
treated as incompatible "
+ + "or implement an {} for this annotation",
+ candidate.name(),
+
DefaultAnnotationCompatibilityValidator.class.getName(),
+ AnnotationCompatibilityValidator.class.getName()
+ );
+
+ errors.add(error);
+ }
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNode.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNode.java
index 019d746999a..b964f9cedbd 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNode.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNode.java
@@ -52,10 +52,12 @@ public class ConfigNode {
// Non-serializable fields.
@JsonIgnore
- @Nullable private ConfigNode parent;
+ @Nullable
+ private ConfigNode parent;
@JsonIgnore
private EnumSet<Flags> flags;
+ @SuppressWarnings("unused")
ConfigNode() {
// Default constructor for Jackson deserialization.
}
@@ -238,7 +240,8 @@ public class ConfigNode {
/**
* Constructs the full path of this node in the configuration tree.
*/
- String path() {
+ @JsonIgnore
+ public String path() {
String name = name();
return parent == null ? name : parent.path() + '.' + name;
@@ -259,7 +262,7 @@ public class ConfigNode {
// Avoid actual class name from being compared for non-value nodes.
Predicate<Entry<String, String>> filter = isValue()
? e -> true
- : e -> !e.getKey().equals(Attributes.CLASS);
+ : e -> !e.getKey().equals(Attributes.CLASS);
String attributes = this.attributes.entrySet().stream()
.filter(filter)
@@ -268,7 +271,7 @@ public class ConfigNode {
return path() + ": ["
+ attributes
- + ", annotations=" +
annotations().stream().map(ConfigAnnotation::toString).collect(Collectors.joining(",",
"[", "]"))
+ + ", annotations=" +
annotations().stream().map(ConfigAnnotation::digest).collect(Collectors.joining(",",
"[", "]"))
+ ", flags=" + flagsHexString
+ (childNodeMap.isEmpty() ? "" : ", children=" +
childNodeMap.size())
+ ']';
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNodeSerializer.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNodeSerializer.java
index ef2b370168b..16033713ecb 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNodeSerializer.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigNodeSerializer.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.configuration.compatibility.framework;
+import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
@@ -42,6 +43,8 @@ public class ConfigNodeSerializer {
*/
public static List<ConfigNode> readAsJson(IgniteDataInput in) throws
IOException {
ObjectMapper objectMapper = new ObjectMapper();
+ // To support {Object val} where val can be both long and int,
deserialize always ints as longs.
+ objectMapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true);
MappingIterator<ConfigNode> objectMappingIterator =
objectMapper.readerFor(ConfigNode.class).readValues(in);
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationAnnotationValidatorSelfTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationAnnotationValidatorSelfTest.java
new file mode 100644
index 00000000000..354b088e91a
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationAnnotationValidatorSelfTest.java
@@ -0,0 +1,400 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode.Flags;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests case for {@link ConfigAnnotationsValidator}.
+ */
+public class ConfigurationAnnotationValidatorSelfTest extends
BaseIgniteAbstractTest {
+
+ @Test
+ public void addingAnnotationBreaksCompatibility() {
+ ConfigNode candidate = newNode(new ConfigAnnotation("a", Map.of()));
+ ConfigNode current = newNode(new ConfigAnnotation("a", Map.of()), new
ConfigAnnotation("b", Map.of()));
+
+ List<String> errors = new ArrayList<>();
+ ConfigAnnotationsValidator validator = new
ConfigAnnotationsValidator(Map.of());
+
+ validator.validate(candidate, current, errors);
+
+ assertEquals(List.of("Adding annotations is not allowed. New
annotations: [b]"), errors);
+ }
+
+ @Test
+ public void removingAnnotationBreaksCompatibility() {
+ ConfigNode candidate = newNode(new ConfigAnnotation("a", Map.of()),
new ConfigAnnotation("b", Map.of()));
+ ConfigNode current = newNode(new ConfigAnnotation("a", Map.of()));
+
+ ConfigAnnotationsValidator validator = new
ConfigAnnotationsValidator(Map.of());
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(candidate, current, errors);
+
+ assertEquals(List.of("Removing annotations is not allowed. Removed
annotations: [b]"), errors);
+ }
+
+ @Test
+ public void testConvertAnnotation() {
+ class SomeClass {
+ @AllTypes
+ @SuppressWarnings("unused")
+ public int allTypes;
+ }
+
+ ConfigAnnotation annotation = getAnnotation(SomeClass.class,
"allTypes", AllTypes.class.getName(), AllTypes.class);
+ assertEquals(AllTypes.class.getName(), annotation.name());
+
+ Map<String, Object> expected = new HashMap<>(Map.of(
+ "bool", true,
+ "int8", 8L, // store integer types as longs
+ "int16", 16L,
+ "int32", 32L,
+ "int64", 64L,
+ "f32", 32.0f,
+ "f64", 64.0d,
+ "string", "str",
+ "enumElement", Abc.B.name(),
+ "classElement", Integer.class.getName()
+ ));
+
+ for (var e : new HashMap<>(expected).entrySet()) {
+ expected.put(e.getKey() + "s", List.of(e.getValue()));
+ }
+
+ Map<String, Object> actual = new HashMap<>();
+ for (String p : annotation.properties().keySet()) {
+ ConfigAnnotationValue value = annotation.properties().get(p);
+ actual.put(p, value.value());
+ }
+
+ assertEquals(new TreeMap<>(expected), new TreeMap<>(actual));
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface AllTypes {
+ @SuppressWarnings("unused")
+ boolean bool() default true;
+
+ @SuppressWarnings("unused")
+ byte int8() default 8;
+
+ @SuppressWarnings("unused")
+ short int16() default 16;
+
+ @SuppressWarnings("unused")
+ int int32() default 32;
+
+ @SuppressWarnings("unused")
+ long int64() default 64;
+
+ @SuppressWarnings("unused")
+ float f32() default 32.0f;
+
+ @SuppressWarnings("unused")
+ double f64() default 64.0d;
+
+ @SuppressWarnings("unused")
+ String string() default "str";
+
+ @SuppressWarnings("unused")
+ Abc enumElement() default Abc.B;
+
+ @SuppressWarnings("unused")
+ Class<?> classElement() default Integer.class;
+
+ // Arrays
+
+ @SuppressWarnings("unused")
+ boolean[] bools() default true;
+
+ @SuppressWarnings("unused")
+ byte[] int8s() default 8;
+
+ @SuppressWarnings("unused")
+ short[] int16s() default 16;
+
+ @SuppressWarnings("unused")
+ int[] int32s() default 32;
+
+ @SuppressWarnings("unused")
+ long[] int64s() default 64;
+
+ @SuppressWarnings("unused")
+ float[] f32s() default 32.0f;
+
+ @SuppressWarnings("unused")
+ double[] f64s() default 64.0d;
+
+ @SuppressWarnings("unused")
+ String[] strings() default "str";
+
+ @SuppressWarnings("unused")
+ Abc[] enumElements() default Abc.B;
+
+ @SuppressWarnings("unused")
+ Class<?>[] classElements() default Integer.class;
+ }
+
+ private enum Abc {
+ A, B, C
+ }
+
+ @ParameterizedTest
+ @MethodSource("annotationValidationRules")
+ public void basicAnnotationValidationRules(AnnotationValidationRuleArgs
args) {
+ class SomeClass {
+ @Base
+ @BasePlusField
+ @BaseArray
+ @BaseTypeChange
+ @BaseArrayTypeChange
+ @BaseMultipleChanges(f3 = "")
+ @SuppressWarnings("unused")
+ public String f1;
+ }
+
+ ConfigAnnotation candidate = getAnnotation(SomeClass.class, "f1",
"AnnotationType", args.ann1);
+ ConfigAnnotation current = getAnnotation(SomeClass.class, "f1",
"AnnotationType", args.ann2);
+
+ ConfigAnnotationsValidator validator = new
ConfigAnnotationsValidator(Map.of("AnnotationType",
+ (candidate1, current1, errors) -> {
+ }));
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(newNode(candidate), newNode(current), errors);
+
+ expectErrors(args.errors, errors);
+ }
+
+ private static Stream<AnnotationValidationRuleArgs>
annotationValidationRules() {
+ return Stream.of(
+ // No changes
+ new AnnotationValidationRuleArgs(Base.class, Base.class,
Set.of()),
+
+ // Adding field
+ new AnnotationValidationRuleArgs(Base.class,
BasePlusField.class,
+ Set.of("AnnotationType added properties [f2]")),
+
+ // Removing field
+ new AnnotationValidationRuleArgs(BasePlusField.class,
Base.class,
+ Set.of("AnnotationType removed properties [f2]")),
+
+ // Changing field type
+ new AnnotationValidationRuleArgs(Base.class,
BaseTypeChange.class,
+ Set.of("AnnotationType properties with changed types
[f1]")),
+
+ // Changing array field type
+ new AnnotationValidationRuleArgs(BaseArray.class,
BaseArrayTypeChange.class,
+ Set.of("AnnotationType properties with changed types
[f1]")),
+
+ // Multiple errors
+
+ new AnnotationValidationRuleArgs(BasePlusField.class,
BaseMultipleChanges.class,
+ Set.of(
+ "AnnotationType properties with changed types
[f1]",
+ "AnnotationType added properties [f3]",
+ "AnnotationType removed properties [f2]"
+ )
+ )
+ );
+ }
+
+ private static class AnnotationValidationRuleArgs {
+ final Class<? extends Annotation> ann1;
+
+ final Class<? extends Annotation> ann2;
+
+ final Set<String> errors;
+
+ AnnotationValidationRuleArgs(
+ Class<? extends Annotation> ann1,
+ Class<? extends Annotation> ann2,
+ Set<String> errors) {
+ this.ann1 = ann1;
+ this.ann2 = ann2;
+ this.errors = errors;
+ }
+
+ @Override
+ public String toString() {
+ return ann1.getSimpleName() + " -> " + ann2.getSimpleName() + " :
" + errors;
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface Base {
+ @SuppressWarnings("unused")
+ String f1() default "";
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface BaseTypeChange {
+ @SuppressWarnings("unused")
+ int f1() default 0;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface BaseArray {
+ @SuppressWarnings("unused")
+ int[] f1() default 0;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface BaseArrayTypeChange {
+ @SuppressWarnings("unused")
+ boolean[] f1() default {true};
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface BasePlusField {
+ @SuppressWarnings("unused")
+ String f1() default "";
+
+ @SuppressWarnings("unused")
+ String f2() default "";
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface BaseMultipleChanges {
+ @SuppressWarnings("unused")
+ int f1() default 0;
+
+ @SuppressWarnings("unused")
+ String f3();
+ }
+
+ @ParameterizedTest
+ @MethodSource("noValidationRules")
+ public void noValidator(AnnotationValidationRuleArgs args) {
+ class SomeClass {
+ @Base
+ @BaseValueChange
+ @SuppressWarnings("unused")
+ public String f1;
+ }
+
+ ConfigAnnotation candidate = getAnnotation(SomeClass.class, "f1",
"AnnotationType", args.ann1);
+ ConfigAnnotation current = getAnnotation(SomeClass.class, "f1",
"AnnotationType", args.ann2);
+
+ ConfigAnnotationsValidator validator = new
ConfigAnnotationsValidator(Map.of());
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(newNode(candidate), newNode(current), errors);
+
+ expectErrors(args.errors, errors);
+ }
+
+ private static Stream<AnnotationValidationRuleArgs> noValidationRules() {
+ return Stream.of(
+ // No changes
+ new AnnotationValidationRuleArgs(Base.class, Base.class,
Set.of()),
+ // Value changes
+ new AnnotationValidationRuleArgs(Base.class,
BaseValueChange.class,
+ Set.of(
+ "Annotation requires a custom compatibility
validator: AnnotationType",
+ "AnnotationType changed values [f1]"
+ )
+ )
+ );
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface BaseValueChange {
+ @SuppressWarnings("unused")
+ String f1() default "x";
+ }
+
+ @Test
+ public void callsSpecificValidator() {
+ class SomeClass {
+ @InvalidAnnotation
+ @SuppressWarnings("unused")
+ public String f1;
+ }
+
+ ConfigAnnotation candidate = getAnnotation(SomeClass.class, "f1",
InvalidAnnotation.class.getName(), InvalidAnnotation.class);
+ ConfigAnnotation current = getAnnotation(SomeClass.class, "f1",
InvalidAnnotation.class.getName(), InvalidAnnotation.class);
+
+ Map<String, AnnotationCompatibilityValidator> validators =
Map.of(InvalidAnnotation.class.getName(),
+ (candidate1, current1, errors) -> errors.add("Invalid"));
+
+ ConfigAnnotationsValidator validator = new
ConfigAnnotationsValidator(validators);
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(newNode(candidate), newNode(current), errors);
+
+ assertEquals(List.of("Invalid"), errors);
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface InvalidAnnotation {
+ }
+
+ private static ConfigNode newNode(ConfigAnnotation... annotations) {
+ return new ConfigNode(null, Map.of(), Arrays.asList(annotations),
EnumSet.noneOf(Flags.class));
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static void expectErrors(Collection<String> expected, List<String>
actual) {
+ Matcher[] matchers =
expected.stream().map(CoreMatchers::containsString).toArray(Matcher[]::new);
+ assertThat(actual, CoreMatchers.hasItems(matchers));
+ }
+
+ private static <A extends Annotation> ConfigAnnotation
getAnnotation(Class<?> clazz,
+ String field,
+ String annotationName,
+ Class<A> annotationClass
+ ) {
+ try {
+ Field f = clazz.getField(field);
+ A annotation = f.getAnnotation(annotationClass);
+ if (annotation == null) {
+ throw new IllegalStateException(format("No annotation: {}
found. Class: {}, field: {}", annotationClass, clazz, field));
+ }
+ return ConfigurationTreeScanner.extractAnnotation(annotationName,
annotation);
+ } catch (NoSuchFieldException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparator.java
index a42ada46b95..73d4b4b9b41 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparator.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparator.java
@@ -34,6 +34,9 @@ import org.apache.ignite.configuration.KeyIgnorer;
* Compares two configuration trees (snapshot and current).
*/
public class ConfigurationTreeComparator {
+
+ private static final ConfigAnnotationsValidator ANNOTATION_VALIDATOR = new
ConfigAnnotationsValidator();
+
/**
* Validates the current configuration is compatible with the snapshot.
*/
@@ -108,9 +111,15 @@ public class ConfigurationTreeComparator {
*/
private ConfigNode find(ConfigNode node, Collection<ConfigNode>
candidates) {
for (ConfigNode candidate : candidates) {
- if (match(node, candidate)) {
- return candidate;
+ if (!match(node, candidate)) {
+ continue;
}
+
+ // node is a snapshot node
+ // candidate is a current configuration node.
+ validateAnnotations(node, candidate);
+
+ return candidate;
}
throw new IllegalStateException("No match found for node: " + node
+ " in candidates: \n\t"
@@ -141,9 +150,29 @@ public class ConfigurationTreeComparator {
&& matchNames(candidate, node)
&& validateFlags(candidate, node)
&&
candidate.deletedPrefixes().containsAll(node.deletedPrefixes())
- && (!node.isValue() || Objects.equals(candidate.type(),
node.type())) // Value node types can be changed.
- // TODO https://issues.apache.org/jira/browse/IGNITE-25747
Validate annotations properly.
- && candidate.annotations().containsAll(node.annotations()); //
Annotations can't be removed.
+ && (!node.isValue() || Objects.equals(candidate.type(),
node.type())); // Value node types can be changed.
+ }
+
+ private static void validateAnnotations(ConfigNode candidate, ConfigNode
node) {
+ List<String> errors = new ArrayList<>();
+
+ ANNOTATION_VALIDATOR.validate(candidate, node, errors);
+
+ if (errors.isEmpty()) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Configuration compatibility issues for ")
+ .append(node.path())
+ .append(':')
+ .append(System.lineSeparator());
+
+ for (var error : errors) {
+ sb.append("\t\t").append(error).append(System.lineSeparator());
+ }
+
+ throw new IllegalStateException(sb.toString());
}
private static boolean matchNames(ConfigNode candidate, ConfigNode node) {
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparatorSelfTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparatorSelfTest.java
index 6b3bcacd024..ab6e421e470 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparatorSelfTest.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeComparatorSelfTest.java
@@ -164,8 +164,8 @@ public class ConfigurationTreeComparatorSelfTest {
new ConfigNode(
root2,
Map.of(ConfigNode.Attributes.NAME, "child"),
- List.of(new
ConfigAnnotation(Deprecated.class.getName())),
- EnumSet.of(Flags.IS_VALUE))
+ List.of(),
+ EnumSet.of(Flags.IS_VALUE, Flags.IS_DEPRECATED))
));
// Adding deprecation is compatible change.
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeScanner.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeScanner.java
index 5db3da36ebd..b5795826ffe 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeScanner.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/ConfigurationTreeScanner.java
@@ -17,29 +17,39 @@
package org.apache.ignite.internal.configuration.compatibility.framework;
-import static java.util.function.Predicate.not;
-
import java.lang.annotation.Annotation;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Array;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.ignite.configuration.ConfigurationModule;
import org.apache.ignite.configuration.annotation.AbstractConfiguration;
import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.ConfigValue;
import org.apache.ignite.configuration.annotation.ConfigurationExtension;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.InjectedName;
+import org.apache.ignite.configuration.annotation.InjectedValue;
import org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.PolymorphicConfig;
+import org.apache.ignite.configuration.annotation.PolymorphicId;
import org.apache.ignite.configuration.annotation.PublicName;
import org.apache.ignite.configuration.annotation.Value;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode.Attributes;
@@ -51,27 +61,20 @@ import
org.apache.ignite.internal.configuration.util.ConfigurationUtil;
support named lists. See {@link
org.apache.ignite.configuration.annotation.NamedConfigValue} annotation.
TODO: https://issues.apache.org/jira/browse/IGNITE-25572
support polymorphic nodes. See {@link
org.apache.ignite.configuration.annotation.PolymorphicConfig} annotation.
- TODO https://issues.apache.org/jira/browse/IGNITE-25747
- support {@link org.apache.ignite.configuration.validation.Range} annotation.
- support {@link org.apache.ignite.configuration.validation.Endpoint}
annotation.
- support {@link org.apache.ignite.configuration.validation.PowerOfTwo}
annotation.
- support {@link org.apache.ignite.configuration.validation.OneOf} annotation.
- support {@link org.apache.ignite.configuration.validation.NotBlank}
annotation.
- support {@link org.apache.ignite.configuration.validation.Immutable}
annotation. ???
- support {@link org.apache.ignite.configuration.validation.ExceptKeys}
annotation.
- support {@link org.apache.ignite.configuration.validation.CamelCaseKeys}
annotation.
- support {@link
org.apache.ignite.internal.network.configuration.MulticastAddress} annotation.
???
- support {@link
org.apache.ignite.internal.network.configuration.SslConfigurationValidator}
annotation. ???
*/
/**
* Provides method to extract metadata from project configuration classes.
*/
public class ConfigurationTreeScanner {
- private static final Set<Class<?>> SUPPORTED_FIELD_ANNOTATIONS = Set.of(
+ private static final Set<Class<?>> SUPPORTED_ANNOTATIONS = Set.of(
Value.class,
- Deprecated.class, // See flags.
- PublicName.class
+ Deprecated.class,
+ NamedConfigValue.class,
+ PublicName.class,
+ PolymorphicId.class,
+ InjectedName.class,
+ InjectedValue.class
);
/**
@@ -134,9 +137,14 @@ public class ConfigurationTreeScanner {
*/
private static List<ConfigAnnotation> collectAdditionalAnnotations(Field
field) {
return Arrays.stream(field.getDeclaredAnnotations())
- .map(Annotation::annotationType)
- .filter(not(SUPPORTED_FIELD_ANNOTATIONS::contains))
- .map(a -> new ConfigAnnotation(a.getName()))
+ .flatMap(a -> {
+ if (SUPPORTED_ANNOTATIONS.contains(a.annotationType())) {
+ return Stream.empty();
+ } else {
+ ConfigAnnotation configAnnotation =
extractAnnotation(a.annotationType().getName(), a);
+ return Stream.of(configAnnotation);
+ }
+ })
.collect(Collectors.toList());
}
@@ -232,5 +240,125 @@ public class ConfigurationTreeScanner {
return polymorphicExtensions.getOrDefault(polymorphicClass,
Set.of());
}
}
+
+ /** Creates {@link ConfigAnnotation} from the given java annotation. */
+ public static ConfigAnnotation extractAnnotation(String name, Annotation
annotation) {
+ Class<?> type = annotation.annotationType();
+ Repeatable repeatable = type.getAnnotation(Repeatable.class);
+ if (repeatable != null) {
+ throw new IllegalStateException("Repeatable annotations are not
supported: " + annotation);
+ }
+
+ Map<String, ConfigAnnotationValue> properties = new HashMap<>();
+
+ for (Method method : type.getMethods()) {
+ // Skip methods inherited from the object class such as equals,
hashCode, etc.
+ if (BuiltinMethod.METHODS.contains(new BuiltinMethod(method))) {
+ continue;
+ }
+
+ String propertyName = method.getName();
+ Class<?> returnType = method.getReturnType();
+ ConfigAnnotationValue propertyValue;
+
+ Object result;
+ try {
+ result = method.invoke(annotation);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Failed invoke annotation
method: " + method, e);
+ }
+
+ if (returnType.isArray()) {
+ Class<?> componentType = returnType.getComponentType();
+
+ List<Object> elements = convertArray(result, componentType,
annotation);
+ propertyValue =
ConfigAnnotationValue.createArray(componentType.getName(), elements);
+ } else {
+ Object convertedValue = convertValue(result, returnType,
annotation);
+ propertyValue =
ConfigAnnotationValue.createValue(returnType.getName(), convertedValue);
+ }
+
+ properties.put(propertyName, propertyValue);
+ }
+
+ return new ConfigAnnotation(name, properties);
+ }
+
+ private static <T> List<Object> convertArray(Object elements, Class<?>
elementType, Annotation annotation) {
+ int length = Array.getLength(elements);
+ List<Object> list = new ArrayList<>(length);
+
+ for (int i = 0; i < length; i++) {
+ Object element = Array.get(elements, i);
+ Object convertedElement = convertValue(element, elementType,
annotation);
+
+ list.add(convertedElement);
+ }
+
+ return list;
+ }
+
+ private static Object convertValue(Object value, Class<?> returnType,
Annotation annotation) {
+ if (returnType == byte.class || returnType == short.class ||
returnType == int.class || returnType == long.class) {
+ // Store integer types as longs because jackson deserializes longs
that fit into INT as ints by default,
+ // it is to store read ints as longs to make validation easier.
+ Number val = (Number) value;
+ return val.longValue();
+ } else if (value instanceof Float || value instanceof Double) {
+ return value;
+ } else if (returnType == String.class || returnType == boolean.class) {
+ return value;
+ } else if (returnType.isEnum()) {
+ return value.toString();
+ } else if (returnType == Class.class) {
+ Class<?> clazz = (Class<?>) value;
+ return clazz.getName();
+ } else {
+ throw new IllegalArgumentException("Supported annotation property
type: " + returnType + ". Annotation: " + annotation);
+ }
+ }
+
+ private static final class BuiltinMethod {
+
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface EmptyAnnotation {
+ }
+
+ private static final Set<BuiltinMethod> METHODS;
+
+ static {
+ // Collect methods that are present on all annotation classes, so
we can exclude them from processing.
+ METHODS = Arrays.stream(EmptyAnnotation.class.getMethods())
+ .map(BuiltinMethod::new)
+ .collect(Collectors.toSet());
+ }
+
+ private final String name;
+
+ private final Class<?> returnType;
+
+ private final List<Object> parameterTypes;
+
+ private BuiltinMethod(Method method) {
+ this.name = method.getName();
+ this.returnType = method.getReturnType();
+ this.parameterTypes = Arrays.asList(method.getParameterTypes());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ BuiltinMethod that = (BuiltinMethod) o;
+ return Objects.equals(name, that.name) &&
Objects.equals(returnType, that.returnType) && Objects.equals(
+ parameterTypes, that.parameterTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, returnType, parameterTypes);
+ }
+ }
}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/BaseAnnotationCompatibilityValidatorSelfTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/BaseAnnotationCompatibilityValidatorSelfTest.java
new file mode 100644
index 00000000000..db56eb8ca88
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/BaseAnnotationCompatibilityValidatorSelfTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeScanner;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+
+/**
+ * Annotation compatibility validator tests.
+ */
+abstract class BaseAnnotationCompatibilityValidatorSelfTest extends
BaseIgniteAbstractTest {
+ protected static ConfigAnnotation getAnnotation(Class<?> clazz,
+ String field,
+ String annotationName,
+ Class<? extends Annotation> annotationClass
+ ) {
+ try {
+ Field f = clazz.getField(field);
+ Annotation annotation = f.getAnnotation(annotationClass);
+ if (annotation == null) {
+ throw new IllegalStateException(format("No annotation: {}
found. Class: {}, field: {}", annotationClass, clazz, field));
+ }
+ return ConfigurationTreeScanner.extractAnnotation(annotationName,
annotation);
+ } catch (NoSuchFieldException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/DefaultAnnotationCompatibilityValidator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/DefaultAnnotationCompatibilityValidator.java
new file mode 100644
index 00000000000..57dfdb4a38b
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/DefaultAnnotationCompatibilityValidator.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotationValue;
+
+/**
+ * Default validator. All changes made to an annotation are incompatible.
+ */
+public class DefaultAnnotationCompatibilityValidator implements
AnnotationCompatibilityValidator {
+ @Override
+ public void validate(ConfigAnnotation candidate, ConfigAnnotation current,
List<String> errors) {
+ Map<String, ConfigAnnotationValue> candidateValues =
candidate.properties();
+ Map<String, ConfigAnnotationValue> currentValues =
current.properties();
+
+ Set<String> changedValues = new TreeSet<>();
+
+ for (Map.Entry<String, ConfigAnnotationValue> e :
candidateValues.entrySet()) {
+ ConfigAnnotationValue value = currentValues.get(e.getKey());
+ if (value == null) {
+ // Handled by base validator.
+ continue;
+ }
+
+ if (!Objects.equals(e.getValue(), value)) {
+ changedValues.add(e.getKey());
+ }
+ }
+
+ if (!changedValues.isEmpty()) {
+ errors.add(candidate.name() + " changed values " + changedValues);
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/ExceptKeysAnnotationCompatibilityValidator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/ExceptKeysAnnotationCompatibilityValidator.java
new file mode 100644
index 00000000000..ed47ef1a9c5
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/ExceptKeysAnnotationCompatibilityValidator.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import java.util.List;
+import org.apache.ignite.configuration.validation.ExceptKeys;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+
+/**
+ * Validator for the {@link ExceptKeys} annotation.
+ */
+public class ExceptKeysAnnotationCompatibilityValidator implements
AnnotationCompatibilityValidator {
+ /** {@inheritDoc} */
+ @Override
+ public void validate(ConfigAnnotation candidate, ConfigAnnotation current,
List<String> errors) {
+ List<String> candidateKeys =
AnnotationCompatibilityValidator.getValue(candidate, "value", (v) ->
(List<String>) v.value());
+ List<String> currentKeys =
AnnotationCompatibilityValidator.getValue(current, "value", (v) ->
(List<String>) v.value());
+
+ if (!candidateKeys.containsAll(currentKeys)) {
+ errors.add("ExceptKeys: changed keys from " + candidateKeys + " to
" + currentKeys);
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/ExceptKeysValidatorTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/ExceptKeysValidatorTest.java
new file mode 100644
index 00000000000..a6acbcc488f
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/ExceptKeysValidatorTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.ignite.configuration.validation.ExceptKeys;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link ExceptKeysAnnotationCompatibilityValidator}.
+ */
+public class ExceptKeysValidatorTest extends
BaseAnnotationCompatibilityValidatorSelfTest {
+
+ @ParameterizedTest
+ @MethodSource("exceptKeysRules")
+ public void exceptKeysAnnotation(String f1, String f2, List<String>
expectedErrors) {
+ class SomeClass {
+ @ExceptKeys({"a", "b", "c"})
+ @SuppressWarnings("unused")
+ public String base;
+
+ @ExceptKeys("a")
+ @SuppressWarnings("unused")
+ public String removeKeys;
+
+ @ExceptKeys({"a", "b", "c", "d", "e"})
+ @SuppressWarnings("unused")
+ public String addKeys;
+ }
+
+ ConfigAnnotation candidate = getAnnotation(SomeClass.class, f1,
ExceptKeys.class.getName(), ExceptKeys.class);
+ ConfigAnnotation current = getAnnotation(SomeClass.class, f2,
ExceptKeys.class.getName(), ExceptKeys.class);
+
+ AnnotationCompatibilityValidator validator = new
ExceptKeysAnnotationCompatibilityValidator();
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(candidate, current, errors);
+
+ assertEquals(Set.copyOf(expectedErrors), Set.copyOf(errors));
+ }
+
+ private static Stream<Arguments> exceptKeysRules() {
+ return Stream.of(
+ Arguments.of("base", "base", List.of()),
+ Arguments.of("base", "removeKeys", List.of()),
+ Arguments.of("base", "addKeys", List.of("ExceptKeys: changed
keys from [a, b, c] to [a, b, c, d, e]"))
+ );
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/OneOfAnnotationCompatibilityValidator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/OneOfAnnotationCompatibilityValidator.java
new file mode 100644
index 00000000000..9332e22369b
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/OneOfAnnotationCompatibilityValidator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import java.util.List;
+import org.apache.ignite.configuration.validation.OneOf;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+
+/**
+ * Validator for the {@link OneOf} annotation.
+ */
+public class OneOfAnnotationCompatibilityValidator implements
AnnotationCompatibilityValidator {
+ /** {@inheritDoc} */
+ @Override
+ public void validate(ConfigAnnotation candidate, ConfigAnnotation current,
List<String> errors) {
+ List<String> candidateKeys =
AnnotationCompatibilityValidator.getValue(candidate, "value", (v) ->
(List<String>) v.value());
+ List<String> currentKeys =
AnnotationCompatibilityValidator.getValue(current, "value", (v) ->
(List<String>) v.value());
+
+ if (!currentKeys.containsAll(candidateKeys)) {
+ errors.add("OneOf: changed keys from " + candidateKeys + " to " +
currentKeys);
+ }
+
+ boolean candidateCaseSensitive =
AnnotationCompatibilityValidator.getValue(candidate, "caseSensitive", (v) ->
(Boolean) v.value());
+ boolean currentCaseSensitive =
AnnotationCompatibilityValidator.getValue(current, "caseSensitive", (v) ->
(Boolean) v.value());
+
+ if (candidateCaseSensitive && !currentCaseSensitive) {
+ errors.add("OneOf: case-insensitive validation can' become
case-sensitive, which is more restrictive");
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/OneOfValidatorTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/OneOfValidatorTest.java
new file mode 100644
index 00000000000..fada75fa6e9
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/OneOfValidatorTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import org.apache.ignite.configuration.validation.OneOf;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link OneOfAnnotationCompatibilityValidator}.
+ */
+public class OneOfValidatorTest extends
BaseAnnotationCompatibilityValidatorSelfTest {
+
+ @ParameterizedTest
+ @MethodSource("oneOfRules")
+ public void oneOfAnnotation(String f1, String f2, List<String>
expectedErrors) {
+ class SomeClass {
+ @OneOf({"a", "b", "c"})
+ @SuppressWarnings("unused")
+ public String base;
+
+ @OneOf("a")
+ @SuppressWarnings("unused")
+ public String removeKeys;
+
+ @OneOf({"a", "b", "c", "d", "e"})
+ @SuppressWarnings("unused")
+ public String addKeys;
+
+ @OneOf(value = {"a", "b", "c"}, caseSensitive = true)
+ @SuppressWarnings("unused")
+ public String caseSensitive;
+
+ @OneOf({"a", "b", "c"})
+ @SuppressWarnings("unused")
+ public String caseInsensitive;
+
+ @OneOf({"a", "b"})
+ @SuppressWarnings("unused")
+ public String caseInsensitiveRemoveKeys;
+ }
+
+ ConfigAnnotation candidate = getAnnotation(SomeClass.class, f1,
OneOf.class.getName(), OneOf.class);
+ ConfigAnnotation current = getAnnotation(SomeClass.class, f2,
OneOf.class.getName(), OneOf.class);
+
+ AnnotationCompatibilityValidator validator = new
OneOfAnnotationCompatibilityValidator();
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(candidate, current, errors);
+
+ assertEquals(expectedErrors, errors);
+ }
+
+ private static Stream<Arguments> oneOfRules() {
+ return Stream.of(
+ Arguments.of("base", "base", List.of()),
+ Arguments.of("base", "removeKeys", List.of("OneOf: changed
keys from [a, b, c] to [a]")),
+ Arguments.of("base", "addKeys", List.of()),
+ Arguments.of("caseInsensitive", "caseSensitive", List.of()),
+ Arguments.of("caseSensitive", "caseInsensitive",
+ List.of(
+ "OneOf: case-insensitive validation can'
become case-sensitive, which is more restrictive"
+ )
+ ),
+ Arguments.of("caseSensitive", "caseInsensitiveRemoveKeys",
+ List.of(
+ "OneOf: changed keys from [a, b, c] to [a, b]",
+ "OneOf: case-insensitive validation can'
become case-sensitive, which is more restrictive"
+ )
+ )
+ );
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/RangeAnnotationCompatibilityValidator.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/RangeAnnotationCompatibilityValidator.java
new file mode 100644
index 00000000000..e75655a7c14
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/RangeAnnotationCompatibilityValidator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import java.util.List;
+import org.apache.ignite.configuration.validation.Range;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+
+/**
+ * Validator for the {@link Range} annotation.
+ */
+public class RangeAnnotationCompatibilityValidator implements
AnnotationCompatibilityValidator {
+ /** {@inheritDoc} */
+ @Override
+ public void validate(ConfigAnnotation candidate, ConfigAnnotation current,
List<String> errors) {
+ long candidateMin =
AnnotationCompatibilityValidator.getValue(candidate, "min", (v) -> (Long)
v.value());
+ long candidateMax =
AnnotationCompatibilityValidator.getValue(candidate, "max", (v) -> (Long)
v.value());
+
+ long currentMin = AnnotationCompatibilityValidator.getValue(current,
"min", (v) -> (Long) v.value());
+ long currentMax = AnnotationCompatibilityValidator.getValue(current,
"max", (v) -> (Long) v.value());
+
+ if (currentMin > candidateMin) {
+ errors.add("Range: min changed from " + candidateMin + " to " +
currentMin);
+ }
+
+ if (currentMax < candidateMax) {
+ errors.add("Range: max changed from " + candidateMax + " to " +
currentMax);
+ }
+ }
+}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/RangeValidatorTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/RangeValidatorTest.java
new file mode 100644
index 00000000000..420b3035e61
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/annotations/RangeValidatorTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.ignite.internal.configuration.compatibility.framework.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.ignite.configuration.validation.Range;
+import
org.apache.ignite.internal.configuration.compatibility.framework.AnnotationCompatibilityValidator;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigAnnotation;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link RangeAnnotationCompatibilityValidator}.
+ */
+public class RangeValidatorTest extends
BaseAnnotationCompatibilityValidatorSelfTest {
+
+ @ParameterizedTest
+ @MethodSource("rangeAnnotationRules")
+ public void rangeAnnotation(String f1, String f2, Set<String>
expectedErrors) {
+ class SomeClass {
+ @Range(min = 10)
+ @SuppressWarnings("unused")
+ public int minBase;
+ @Range(min = 5)
+ @SuppressWarnings("unused")
+ public int minDecreased;
+ @Range(min = 12)
+ @SuppressWarnings("unused")
+ public int minIncreased;
+
+ @Range(max = 20)
+ @SuppressWarnings("unused")
+ public int maxBase;
+ @Range(max = 15)
+ @SuppressWarnings("unused")
+ public int maxDecreased;
+ @Range(max = 22)
+ @SuppressWarnings("unused")
+ public int maxIncreased;
+
+ @Range
+ @SuppressWarnings("unused")
+ public int base;
+ @Range(max = Long.MAX_VALUE)
+ @SuppressWarnings("unused")
+ public int maxToDefault;
+ @Range(min = Long.MIN_VALUE)
+ @SuppressWarnings("unused")
+ public int minToDefault;
+ }
+
+ ConfigAnnotation candidate = getAnnotation(SomeClass.class, f1,
Range.class.getName(), Range.class);
+ ConfigAnnotation current = getAnnotation(SomeClass.class, f2,
Range.class.getName(), Range.class);
+
+ AnnotationCompatibilityValidator validator = new
RangeAnnotationCompatibilityValidator();
+
+ List<String> errors = new ArrayList<>();
+ validator.validate(candidate, current, errors);
+
+ assertEquals(Set.copyOf(expectedErrors), Set.copyOf(errors));
+ }
+
+ private static Stream<Arguments> rangeAnnotationRules() {
+ return Stream.of(
+ Arguments.of("minBase", "minBase", Set.of()),
+ Arguments.of("minBase", "minDecreased", Set.of()),
+ Arguments.of("minBase", "minIncreased", Set.of("Range: min
changed from 10 to 12")),
+
+ Arguments.of("maxBase", "maxBase", Set.of()),
+ Arguments.of("maxBase", "maxIncreased", Set.of()),
+ Arguments.of("maxBase", "maxDecreased", Set.of("Range: max
changed from 20 to 15")),
+
+ Arguments.of("base", "maxToDefault", Set.of()),
+ Arguments.of("maxToDefault", "base", Set.of()),
+
+ Arguments.of("base", "minToDefault", Set.of()),
+ Arguments.of("minToDefault", "base", Set.of())
+ );
+ }
+}
diff --git
a/modules/runner/src/test/resources/compatibility/configuration/snapshot.bin
b/modules/runner/src/test/resources/compatibility/configuration/snapshot.bin
index a527bbbc4d4..a0ed7892a13 100644
Binary files
a/modules/runner/src/test/resources/compatibility/configuration/snapshot.bin
and
b/modules/runner/src/test/resources/compatibility/configuration/snapshot.bin
differ