This is an automated email from the ASF dual-hosted git repository.
amashenkov 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 3c7abf3a914 IGNITE-26006 Configuration compatibility. Correctly handle
adding new fields in compatibility test (#6305)
3c7abf3a914 is described below
commit 3c7abf3a914eb83fe75a4fe9086d6045c99950e5
Author: Max Zhuravkov <[email protected]>
AuthorDate: Thu Jul 24 17:27:38 2025 +0300
IGNITE-26006 Configuration compatibility. Correctly handle adding new
fields in compatibility test (#6305)
---
.../ConfigurationCompatibilityTest.java | 27 +-
.../GenerateConfigurationSnapshot.java | 54 ++++
.../compatibility/framework/ConfigNode.java | 109 ++++++-
.../framework/ConfigNodeSerializer.java | 7 +-
.../framework/ConfigurationTreeComparator.java | 335 +++++++++++++--------
.../ConfigurationTreeComparatorSelfTest.java | 167 ++++++++--
.../compatibility/configuration/snapshot.bin | Bin 4039 -> 4067 bytes
7 files changed, 508 insertions(+), 191 deletions(-)
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/ConfigurationCompatibilityTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/ConfigurationCompatibilityTest.java
index 61c343a442a..bfe018dd630 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/ConfigurationCompatibilityTest.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/ConfigurationCompatibilityTest.java
@@ -37,14 +37,11 @@ import
org.apache.ignite.internal.configuration.ConfigurationModules;
import org.apache.ignite.internal.configuration.ServiceLoaderModulesProvider;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNodeSerializer;
-import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigShuttle;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationSnapshotManager;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeComparator;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeComparator.ComparisonContext;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeScanner;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeScanner.ScanContext;
-import org.apache.ignite.internal.logger.IgniteLogger;
-import org.apache.ignite.internal.logger.Loggers;
import org.apache.ignite.internal.testframework.IgniteAbstractTest;
import org.apache.ignite.internal.util.io.IgniteUnsafeDataInput;
import org.apache.ignite.internal.util.io.IgniteUnsafeDataOutput;
@@ -58,11 +55,8 @@ import org.junit.jupiter.params.provider.MethodSource;
* Tests for configuration compatibility.
*/
public class ConfigurationCompatibilityTest extends IgniteAbstractTest {
- private static final String DEFAULT_FILE_NAME = "snapshot.bin";
+ static final String DEFAULT_FILE_NAME = "snapshot.bin";
private static final String SNAPSHOTS_RESOURCE_LOCATION =
"compatibility/configuration/";
- private static final Path DEFAULT_SNAPSHOT_FILE = Path.of("modules",
"runner", "build", "work", DEFAULT_FILE_NAME);
-
- private static final IgniteLogger LOG =
Loggers.forClass(ConfigurationCompatibilityTest.class);
/**
* This test ensures that the current configuration can be serialized and
deserialized correctly.
@@ -93,7 +87,7 @@ public class ConfigurationCompatibilityTest extends
IgniteAbstractTest {
* This test ensures that the current configuration metadata wasn't
changed. If the test fails, it means that the current configuration
* metadata has changed, then current snapshot should be renamed to the
latest release version, and a new snapshot should be created.
*
- * @see #main(String[]) method for generating a new snapshot.
+ * @see GenerateConfigurationSnapshot#main(String[]) method for generating
a new snapshot.
*/
@Test
void testConfigurationChanged() throws IOException {
@@ -126,7 +120,7 @@ public class ConfigurationCompatibilityTest extends
IgniteAbstractTest {
return new HashSet<>(modules);
}
- private static List<ConfigNode> loadCurrentConfiguration() {
+ static List<ConfigNode> loadCurrentConfiguration() {
ConfigurationModules modules =
loadConfigurationModules(ConfigurationCompatibilityTest.class.getClassLoader());
ConfigurationModule local = modules.local();
@@ -163,21 +157,6 @@ public class ConfigurationCompatibilityTest extends
IgniteAbstractTest {
return new ConfigurationModules(modules);
}
- /**
- * Generates a snapshot of the current configuration metadata and saves it
to a file.
- */
- public static void main(String[] args) throws IOException {
- List<ConfigNode> configNodes = loadCurrentConfiguration();
-
- ConfigShuttle shuttle = node -> LOG.info(node.toString());
- LOG.info("DUMP TREE:");
- configNodes.forEach(c -> c.accept(shuttle));
-
- ConfigurationSnapshotManager.saveSnapshotToFile(configNodes,
DEFAULT_SNAPSHOT_FILE);
-
- LOG.info("Snapshot saved to: " +
DEFAULT_SNAPSHOT_FILE.toAbsolutePath());
- }
-
/**
* List directory contents for a resource folder. Not recursive. Works for
regular files and also JARs.
*/
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/GenerateConfigurationSnapshot.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/GenerateConfigurationSnapshot.java
new file mode 100644
index 00000000000..b31fcecc17c
--- /dev/null
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/GenerateConfigurationSnapshot.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import static
org.apache.ignite.internal.configuration.compatibility.ConfigurationCompatibilityTest.DEFAULT_FILE_NAME;
+import static
org.apache.ignite.internal.configuration.compatibility.ConfigurationCompatibilityTest.loadCurrentConfiguration;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigShuttle;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationSnapshotManager;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+
+/**
+ * Generates a snapshot for the current configuration.
+ */
+public class GenerateConfigurationSnapshot {
+ private static final Path DEFAULT_SNAPSHOT_FILE = Path.of("modules",
"runner", "build", "work", DEFAULT_FILE_NAME);
+
+ private static final IgniteLogger LOG =
Loggers.forClass(ConfigurationCompatibilityTest.class);
+
+ /**
+ * Generates a snapshot of the current configuration metadata and saves it
to a file.
+ */
+ public static void main(String[] args) throws IOException {
+ List<ConfigNode> configNodes = loadCurrentConfiguration();
+
+ ConfigShuttle shuttle = node -> LOG.info(node.toString());
+ LOG.info("DUMP TREE:");
+ configNodes.forEach(c -> c.accept(shuttle));
+
+ ConfigurationSnapshotManager.saveSnapshotToFile(configNodes,
DEFAULT_SNAPSHOT_FILE);
+
+ LOG.info("Snapshot saved to: " +
DEFAULT_SNAPSHOT_FILE.toAbsolutePath());
+ }
+}
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 0293a69fc81..bed44c810e9 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
@@ -42,7 +42,7 @@ public class ConfigNode {
@JsonProperty
private List<ConfigAnnotation> annotations = new ArrayList<>();
@JsonProperty
- private final Map<String, ConfigNode> childNodeMap = new LinkedHashMap<>();
+ private Map<String, Node> childNodeMap = new LinkedHashMap<>();
@JsonProperty
private String flagsHexString;
@JsonProperty
@@ -52,7 +52,7 @@ public class ConfigNode {
// Non-serializable fields.
@JsonIgnore
- @Nullable
+ @Nullable
private ConfigNode parent;
@JsonIgnore
private EnumSet<Flags> flags;
@@ -153,11 +153,18 @@ public class ConfigNode {
return attributes.get(Attributes.CLASS);
}
+ /**
+ * Returns flags for this node.
+ */
+ public Set<Flags> flags() {
+ return flags;
+ }
+
/**
* Returns the child nodes of this node.
*/
- public Collection<ConfigNode> childNodes() {
- return childNodeMap.values();
+ public Map<String, Node> children() {
+ return childNodeMap;
}
/**
@@ -173,7 +180,10 @@ public class ConfigNode {
void addChildNodes(Collection<ConfigNode> childNodes) {
assert !flags.contains(Flags.IS_VALUE) : "Value node can't have
children.";
- childNodes.forEach(e -> childNodeMap.put(e.name(), e));
+ childNodes.forEach(e -> {
+ e.parent = this;
+ addChildNode(e.name(), new Node(e));
+ });
}
/**
@@ -184,12 +194,11 @@ public class ConfigNode {
addChildNodes(List.of(childNodes));
}
- /**
- * Returns {@code true} if this node is a root node, {@code false}
otherwise.
- */
- @JsonIgnore
- public boolean isRoot() {
- return parent == null;
+ private void addChildNode(String name, Node node) {
+ Node existing = childNodeMap.put(name, node);
+ if (existing != null) {
+ throw new IllegalArgumentException("Child node already exists: " +
name);
+ }
}
/**
@@ -216,6 +225,14 @@ public class ConfigNode {
return flags.contains(Flags.IS_INNER_NODE);
}
+ /**
+ * Returns {@code true} if a configuration value that this node presents
has a default value, otherwise {@code false}.
+ */
+ @JsonIgnore
+ public boolean hasDefault() {
+ return flags.contains(Flags.HAS_DEFAULT);
+ }
+
/**
* Returns {@code true} if this node represents internal part of
configuration, {@code false} otherwise.
*/
@@ -269,8 +286,8 @@ public class ConfigNode {
public void accept(ConfigShuttle visitor) {
visitor.visit(this);
- for (ConfigNode child : childNodes()) {
- child.accept(visitor);
+ for (Node child : children().values()) {
+ child.node().accept(visitor);
}
}
@@ -312,7 +329,8 @@ public class ConfigNode {
IS_DEPRECATED(1 << 2),
IS_INTERNAL(1 << 3),
IS_NAMED_NODE(1 << 4),
- IS_INNER_NODE(1 << 5);
+ IS_INNER_NODE(1 << 5),
+ HAS_DEFAULT(1 << 6);
private final int mask;
@@ -351,4 +369,67 @@ public class ConfigNode {
static String KIND = "kind";
static String CLASS = "class";
}
+
+ /**
+ * Node holds a references to a child config node.
+ */
+ public static class Node {
+ /**
+ * Non-polymorphic node.
+ */
+ @JsonProperty
+ private ConfigNode single;
+
+ @SuppressWarnings("unused")
+ Node() {
+ // Default constructor for Jackson deserialization.
+ }
+
+ Node(ConfigNode single) {
+ this.single = single;
+ }
+
+ /**
+ * Returns a single node if this node is a simple node or throws.
+ */
+ ConfigNode node() {
+ assert single != null : "Use the nodes() method instead.";
+ return single;
+ }
+
+ /**
+ * Returns {@code true} if this node represents a value node.
+ *
+ * @see ConfigNode#isValue()
+ */
+ @JsonIgnore
+ boolean isValue() {
+ return single.isValue();
+ }
+
+ /**
+ * Returns {@code true} if this node has a default value.
+ *
+ * @see ConfigNode#hasDefault()
+ */
+ boolean hasDefault() {
+ return single.hasDefault();
+ }
+
+ /**
+ * Returns legacy names.
+ *
+ * @see ConfigNode#legacyPropertyNames()
+ */
+ Set<String> legacyPropertyNames() {
+ return single.legacyPropertyNames();
+ }
+
+ /**
+ * Returns the full path of this node in the configuration tree.
+ */
+ String path() {
+ return single.path();
+ }
+ }
}
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 16033713ecb..23f3b822ad8 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
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.List;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode.Node;
import org.apache.ignite.internal.util.io.IgniteDataInput;
import org.apache.ignite.internal.util.io.IgniteDataOutput;
@@ -61,9 +62,9 @@ public class ConfigNodeSerializer {
* Recursively set parent links for all children
*/
private static void restoreParentLinks(ConfigNode node) {
- for (ConfigNode child : node.childNodes()) {
- child.init(node);
- restoreParentLinks(child);
+ for (Node child : node.children().values()) {
+ child.node().init(node);
+ restoreParentLinks(child.node());
}
}
}
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 1c4bdb76d5a..ddc68a67560 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
@@ -17,18 +17,21 @@
package org.apache.ignite.internal.configuration.compatibility.framework;
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.ignite.configuration.ConfigurationModule;
import org.apache.ignite.configuration.KeyIgnorer;
+import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode.Node;
/**
* Compares two configuration trees (snapshot and current).
@@ -45,11 +48,11 @@ public class ConfigurationTreeComparator {
List<ConfigNode> actualTrees,
ComparisonContext compContext
) {
- LeafNodesVisitor shuttle = new LeafNodesVisitor(new
Validator(actualTrees), compContext);
+ compContext.reset();
- for (ConfigNode tree : snapshotTrees) {
- tree.accept(shuttle);
- }
+ compareRoots(snapshotTrees, actualTrees, compContext);
+
+ compContext.throwIfNotEmpty();
}
/**
@@ -61,138 +64,52 @@ public class ConfigurationTreeComparator {
assertEquals(dump1, dump2, "Configuration metadata mismatch");
}
- /**
- * Traverses the tree and triggers validation for leaf nodes.
- */
- private static class LeafNodesVisitor implements ConfigShuttle {
- private final Consumer<ConfigNode> validator;
- private final ComparisonContext compContext;
-
- private LeafNodesVisitor(Consumer<ConfigNode> validator,
ComparisonContext compContext) {
- this.validator = validator;
- this.compContext = compContext;
- }
-
- @Override
- public void visit(ConfigNode node) {
- assert node.isRoot() || node.isInnerNode() || node.isNamedNode()
|| node.isValue();
-
- if (node.isValue() && !compContext.shouldIgnore(node.path())) {
- validator.accept(node);
- }
- }
- }
-
- /**
- * Validates value nodes.
- */
- private static class Validator implements Consumer<ConfigNode> {
- private final List<ConfigNode> roots;
-
- private Validator(List<ConfigNode> roots) {
- this.roots = roots;
- }
-
- @Override
- public void accept(ConfigNode leafNode) {
- List<ConfigNode> path = getPath(leafNode);
-
- // Validate path starting from the root.
- Collection<ConfigNode> candidates = roots;
-
- for (ConfigNode node : path) {
- ConfigNode found = find(node, candidates);
- candidates = found.childNodes();
- }
- }
-
- /**
- * Return first node from candidates collection that matches the given
node.
- *
- * @throws IllegalStateException If no match found.
- */
- private ConfigNode find(ConfigNode node, Collection<ConfigNode>
candidates) {
- for (ConfigNode candidate : candidates) {
- 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"
- +
candidates.stream().map(ConfigNode::toString).collect(Collectors.joining("\n\t")));
- }
- }
-
- /**
- * Builds value node path.
- */
- private static List<ConfigNode> getPath(ConfigNode node) {
- List<ConfigNode> path = new ArrayList<>();
- while (node != null) {
- path.add(node);
- node = node.getParent();
- }
-
- Collections.reverse(path);
-
- return path;
- }
-
/**
* Returns {@code true} if given node is compatible with candidate node,
{@code false} otherwise.
*/
- private static boolean match(ConfigNode node, ConfigNode candidate) {
- return Objects.equals(candidate.kind(), node.kind())
- && validateFlags(candidate, node)
- && matchNames(candidate, node)
- &&
candidate.deletedPrefixes().containsAll(node.deletedPrefixes())
- && (!node.isValue() || Objects.equals(candidate.type(),
node.type())); // Value node types can be changed.
+ private static boolean match(ConfigNode candidate, ConfigNode current) {
+ // To make debugging easier.
+ boolean kindMatches = Objects.equals(current.kind(), candidate.kind());
+ boolean nameMatches = matchNames(current, candidate);
+ boolean flagMatch = validateFlags(current, candidate);
+ boolean deletedPrefixesMatch =
current.deletedPrefixes().containsAll(candidate.deletedPrefixes());
+ // Value node types can be changed.
+ boolean nodeTypeMatches = !candidate.isValue() ||
Objects.equals(current.type(), candidate.type());
+ return kindMatches
+ && nameMatches
+ && flagMatch
+ && deletedPrefixesMatch
+ && nodeTypeMatches;
}
- private static void validateAnnotations(ConfigNode candidate, ConfigNode
node) {
- List<String> errors = new ArrayList<>();
-
- ANNOTATION_VALIDATOR.validate(candidate, node, errors);
-
- if (errors.isEmpty()) {
- return;
- }
+ private static void validateAnnotations(ConfigNode candidate, ConfigNode
current, ComparisonContext context) {
+ List<String> annotationErrors = new ArrayList<>();
- StringBuilder sb = new StringBuilder();
- sb.append("Configuration compatibility issues for ")
- .append(node.path())
- .append(':')
- .append(System.lineSeparator());
+ ANNOTATION_VALIDATOR.validate(candidate, current, annotationErrors);
- for (var error : errors) {
- sb.append("\t\t").append(error).append(System.lineSeparator());
+ for (String error : annotationErrors) {
+ context.addError(candidate, error);
}
-
- throw new IllegalStateException(sb.toString());
}
- private static boolean matchNames(ConfigNode candidate, ConfigNode node) {
- return Objects.equals(candidate.name(), node.name())
- || compareUsingLegacyNames(candidate, node);
+ private static boolean matchNames(ConfigNode candidate, ConfigNode
current) {
+ return Objects.equals(candidate.name(), current.name()) ||
compareUsingLegacyNames(candidate, current);
}
- private static boolean compareUsingLegacyNames(ConfigNode candidate,
ConfigNode node) {
- return candidate.legacyPropertyNames().contains(node.name());
+ private static boolean compareUsingLegacyNames(ConfigNode candidate,
ConfigNode current) {
+ return candidate.legacyPropertyNames().contains(current.name());
}
- private static boolean validateFlags(ConfigNode candidate, ConfigNode
node) {
- return node.isRoot() == candidate.isRoot()
- && node.isValue() == candidate.isValue()
- && node.isNamedNode() == candidate.isNamedNode()
- && node.isInnerNode() == candidate.isInnerNode()
- && (!candidate.isInternal() || node.isInternal()) // Public
property\tree can't be hidden.
- && (!node.isDeprecated() || candidate.isDeprecated()); //
Deprecation shouldn't be removed.
+ private static boolean validateFlags(ConfigNode candidate, ConfigNode
current) {
+ // If flags are empty then they should always be compatible.
+ if (candidate.flags().equals(current.flags())) {
+ return true;
+ }
+ return current.isValue() == candidate.isValue()
+ && (!candidate.isInternal() || current.isInternal()) // Public
property\tree can't be hidden.
+ && current.isNamedNode() == candidate.isNamedNode()
+ && current.isInnerNode() == candidate.isInnerNode()
+ && (!current.isDeprecated() || candidate.isDeprecated()); //
Deprecation shouldn't be removed.
}
private static String dumpTree(List<ConfigNode> nodes) {
@@ -232,6 +149,8 @@ public class ConfigurationTreeComparator {
private final KeyIgnorer deletedItems;
+ private final List<String> errors = new ArrayList<>();
+
ComparisonContext(Collection<String> deletedPrefixes) {
this.deletedItems =
KeyIgnorer.fromDeletedPrefixes(deletedPrefixes);
}
@@ -239,5 +158,173 @@ public class ConfigurationTreeComparator {
boolean shouldIgnore(String path) {
return deletedItems.shouldIgnore(path);
}
+
+ void reset() {
+ errors.clear();
+ }
+
+ void addError(Node node, String error) {
+ reportError(node.path(), error);
+ }
+
+ void addError(ConfigNode node, String error) {
+ reportError(node.path(), error);
+ }
+
+ private void reportError(String path, String error) {
+ String message = format("Node: {}: {}", path, error);
+ errors.add(message);
+ }
+
+ private void throwIfNotEmpty() {
+ if (errors.isEmpty()) {
+ return;
+ }
+
+ StringBuilder message = new StringBuilder("There are incompatible
changes:").append(System.lineSeparator());
+ for (String error : errors) {
+
message.append('\t').append(error).append(System.lineSeparator());
+ }
+
+ throw new IllegalStateException(message.toString());
+ }
+ }
+
+
+ private static void compareRoots(List<ConfigNode> candidateRoots,
List<ConfigNode> currentRoots, ComparisonContext context) {
+ List<ConfigNode> removed = new ArrayList<>();
+ List<ConfigNode> added = new ArrayList<>();
+ List<ConfigNode> currentCopy = new ArrayList<>(currentRoots);
+
+ for (ConfigNode root1 : candidateRoots) {
+ boolean matchFound = false;
+
+ for (ConfigNode root2 : new ArrayList<>(currentCopy)) {
+ if (rootsMatch(root1, root2)) {
+ currentCopy.remove(root2);
+ validate(root1, root2, context);
+ matchFound = true;
+ break;
+ }
+ }
+
+ if (!matchFound) {
+ removed.add(root1);
+ }
+ }
+
+ added.addAll(currentCopy);
+
+ // Validate new roots
+ validateNew(added, context);
+ // Reject removed roots.
+ validateRemoved(removed, context);
+ }
+
+ private static boolean rootsMatch(ConfigNode candidate, ConfigNode
current) {
+ boolean nameMatches = Objects.equals(candidate.name(), current.name());
+ boolean kindMatches = Objects.equals(candidate.kind(), current.kind());
+ return nameMatches && kindMatches;
+ }
+
+ private static void validate(Node candidate, Node current,
ComparisonContext context) {
+ compareNodes(candidate.node(), current.node(), context);
+ }
+
+ private static void validate(ConfigNode candidate, ConfigNode current,
ComparisonContext context) {
+ if (!context.errors.isEmpty()) {
+ return;
+ }
+ compareNodes(candidate, current, context);
+ }
+
+ private static void compareNodes(ConfigNode candidate, ConfigNode current,
ComparisonContext context) {
+ if (!match(candidate, current)) {
+ context.addError(candidate, "Node does not match. Previous: " +
candidate + ". Current: " + current);
+ return;
+ }
+
+ validateAnnotations(candidate, current, context);
+
+ compareChildren(candidate.children(), current.children(), context);
+ }
+
+ private static void compareChildren(Map<String, Node> candidate,
Map<String, Node> current, ComparisonContext context) {
+ // Validates matching children.
+ // then validates removed and added ones.
+ List<Node> removed = new ArrayList<>();
+ List<Node> added = new ArrayList<>();
+ Map<String, Node> currentCopy = new HashMap<>(current);
+
+ for (Entry<String, Node> candididateEntry : candidate.entrySet()) {
+ Node candidateNode = candididateEntry.getValue();
+ boolean matchFound = false;
+
+ for (Entry<String, Node> currentEntry : currentCopy.entrySet()) {
+ Node nodeB = currentEntry.getValue();
+ if (equalNames(candididateEntry, currentEntry)) {
+ matchFound = true;
+ currentCopy.remove(currentEntry.getKey());
+ validate(candidateNode, nodeB, context);
+ break;
+ }
+ }
+
+ if (!matchFound) {
+ removed.add(candidateNode);
+ }
+ }
+
+ added.addAll(currentCopy.values());
+
+ validateRemovedChildren(removed, context);
+ validateNewChildren(added, context);
+ }
+
+ private static boolean equalNames(Entry<String, Node> currentEntry,
Entry<String, Node> candidateEntry) {
+ String candidateName = currentEntry.getKey();
+ String currentName = candidateEntry.getKey();
+ if (candidateName.equals(currentName)) {
+ return true;
+ }
+ Node nodeB = candidateEntry.getValue();
+ return nodeB.legacyPropertyNames().stream().anyMatch(n ->
n.equals(candidateName));
+ }
+
+ private static void validateNew(Collection<ConfigNode> nodes,
ComparisonContext context) {
+ // Validate nodes recursively, adding a new node is valid if all its
value nodes have default.
+ for (ConfigNode node : nodes) {
+ if (node.isValue() && !node.hasDefault()) {
+ context.addError(node, "Added a node with no default value");
+ }
+ validateNewChildren(node.children().values(), context);
+ }
+ }
+
+ private static void validateRemoved(Collection<ConfigNode> nodes,
ComparisonContext context) {
+ for (ConfigNode node : nodes) {
+ if (!context.shouldIgnore(node.path())) {
+ context.addError(node, "Node was removed");
+ }
+ }
+ }
+
+ private static void validateNewChildren(Collection<Node> nodes,
ComparisonContext context) {
+ // Validate nodes recursively, adding a new node is valid if all its
value nodes have default.
+ for (Node node : nodes) {
+ if (node.isValue() && !node.hasDefault()) {
+ context.addError(node, "Added a value with no default");
+ } else {
+ validateNew(List.of(node.node()), context);
+ }
+ }
+ }
+
+ private static void validateRemovedChildren(Collection<Node> nodes,
ComparisonContext context) {
+ for (Node node : nodes) {
+ if (!context.shouldIgnore(node.path())) {
+ context.addError(node, "Node was removed");
+ }
+ }
}
}
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 b9b9f43ac8b..4a725dd7c90 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
@@ -30,6 +30,8 @@ import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNo
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode.Flags;
import
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeComparator.ComparisonContext;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Self-test for {@link ConfigurationTreeComparator} checks various
compatibility scenarios.
@@ -40,20 +42,20 @@ public class ConfigurationTreeComparatorSelfTest {
void equalConfigurationCanBeFound() {
ConfigNode root1 = createRoot("root1");
root1.addChildNodes(List.of(
- createChild(root1, "child1"),
- createChild(root1, "child2")
+ createChild("child1"),
+ createChild("child2")
));
ConfigNode root2 = createRoot("root1");
root2.addChildNodes(List.of(
- createChild(root2, "child1"),
- createChild(root2, "child2")
+ createChild("child1"),
+ createChild("child2")
));
ConfigNode anotherRoot = createRoot("root2");
anotherRoot.addChildNodes(List.of(
- createChild(anotherRoot, "child1"),
- createChild(anotherRoot, "child2")
+ createChild("child1"),
+ createChild("child2")
));
assertCompatible(List.of(root1), List.of(anotherRoot, root2));
@@ -63,10 +65,10 @@ public class ConfigurationTreeComparatorSelfTest {
void rootCompatibility() {
{
ConfigNode root1 = ConfigNode.createRoot("root", Object.class,
ConfigurationType.LOCAL, true);
- root1.addChildNodes(createChild(root1, "child"));
+ root1.addChildNodes(createChild("child"));
ConfigNode root2 = ConfigNode.createRoot("root", Object.class,
ConfigurationType.LOCAL, false);
- root2.addChildNodes(createChild(root2, "child"));
+ root2.addChildNodes(createChild("child"));
// Internal root can become public.
assertCompatible(root1, root2);
@@ -75,10 +77,10 @@ public class ConfigurationTreeComparatorSelfTest {
}
{
ConfigNode root1 = ConfigNode.createRoot("root", Object.class,
ConfigurationType.LOCAL, true);
- root1.addChildNodes(createChild(root1, "child"));
+ root1.addChildNodes(createChild("child"));
ConfigNode root2 = ConfigNode.createRoot("root", Object.class,
ConfigurationType.DISTRIBUTED, true);
- root2.addChildNodes(createChild(root2, "child"));
+ root2.addChildNodes(createChild("child"));
// Root types can't be changed both ways.
assertIncompatible(root1, root2);
@@ -129,13 +131,13 @@ public class ConfigurationTreeComparatorSelfTest {
void propertyCantBeRemoved() {
ConfigNode root1 = createRoot("root1");
root1.addChildNodes(List.of(
- createChild(root1, "child1")
+ createChild("child1")
));
ConfigNode root2 = createRoot("root1");
root2.addChildNodes(List.of(
- createChild(root2, "child1"),
- createChild(root1, "child2")
+ createChild("child1"),
+ createChild("child2")
));
// Adding a property is compatible change.
@@ -176,7 +178,7 @@ public class ConfigurationTreeComparatorSelfTest {
root1,
Map.of(ConfigNode.Attributes.NAME, "child"),
List.of(),
- EnumSet.of(Flags.IS_VALUE))
+ EnumSet.of(Flags.IS_VALUE, Flags.HAS_DEFAULT))
));
ConfigNode root2 = createRoot("root1");
@@ -185,7 +187,7 @@ public class ConfigurationTreeComparatorSelfTest {
root2,
Map.of(ConfigNode.Attributes.NAME, "child"),
List.of(),
- EnumSet.of(Flags.IS_VALUE, Flags.IS_DEPRECATED))
+ EnumSet.of(Flags.IS_VALUE, Flags.IS_DEPRECATED,
Flags.HAS_DEFAULT))
));
// Adding deprecation is compatible change.
@@ -194,6 +196,67 @@ public class ConfigurationTreeComparatorSelfTest {
assertIncompatible(root2, root1);
}
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void addFieldToConfiguration(boolean hasDefault) {
+ Set<Flags> newFieldFlags = createFlags(hasDefault);
+
+ ConfigNode root1 = createRoot("root1");
+ root1.addChildNodes(List.of(
+ createChild("child1"),
+ createChild("child2")
+ ));
+
+ ConfigNode root2 = createRoot("root1");
+ root2.addChildNodes(List.of(
+ createChild("child1"),
+ createChild("child2"),
+ createChild("child3", newFieldFlags)
+ ));
+
+ if (hasDefault) {
+ assertCompatible(List.of(root1), List.of(root2));
+ assertIncompatible(List.of(root2), List.of(root1));
+ } else {
+ assertIncompatible(List.of(root1), List.of(root2));
+ assertIncompatible(List.of(root2), List.of(root1));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void addNewRoot(boolean hasDefault) {
+ Set<Flags> fieldFlags = createFlags(hasDefault);
+
+ ConfigNode root1 = createRoot("root1");
+ {
+ root1.addChildNodes(List.of(
+ createChild("child1"),
+ createChild("child2")
+ ));
+ }
+
+ ConfigNode root2 = createRoot("root2");
+ {
+ ConfigNode node = createChild("node1", Set.of());
+ node.addChildNodes(List.of(createChild("f", fieldFlags)));
+
+ root2.addChildNodes(List.of(
+ createChild("child1"),
+ createChild("child2"),
+ node
+ ));
+ }
+
+ if (hasDefault) {
+ assertCompatible(List.of(root1), List.of(root1, root2));
+ assertIncompatible(List.of(root2, root1), List.of(root1));
+ } else {
+ assertIncompatible(List.of(root1), List.of(root1, root2));
+ assertIncompatible(List.of(root2, root1), List.of(root1));
+ }
+ }
+
/**
* Check {@link ConfigurationModule#deletedPrefixes()} functionality.
*/
@@ -340,25 +403,36 @@ public class ConfigurationTreeComparatorSelfTest {
@Test
void testCompatibleRename() {
+ ConfigNode root1 = createRoot("root");
+
ConfigNode oldNode = new ConfigNode(null, Map.of(Attributes.NAME,
"oldTestCount"), List.of(),
EnumSet.of(Flags.IS_VALUE));
+ root1.addChildNodes(oldNode);
+
+ ConfigNode root2 = createRoot("root");
ConfigNode newNode = new ConfigNode(null, Map.of(Attributes.NAME,
"newTestCount"),
List.of(),
EnumSet.of(Flags.IS_VALUE), Set.of("oldTestCount"), List.of());
+ root2.addChildNodes(newNode);
- assertCompatible(oldNode, newNode);
+ assertCompatible(root1, root2);
+ root1 = createRoot("root");
// Equal configs, still compatible
oldNode = new ConfigNode(null, Map.of(Attributes.NAME, "newTestCount"),
List.of(),
EnumSet.of(Flags.IS_VALUE), Set.of("oldTestCount"), List.of());
+ root1.addChildNodes(oldNode);
+
+ root2 = createRoot("root");
newNode = new ConfigNode(null, Map.of(Attributes.NAME, "newTestCount"),
List.of(),
EnumSet.of(Flags.IS_VALUE), Set.of("oldTestCount"), List.of());
+ root2.addChildNodes(newNode);
- assertCompatible(oldNode, newNode);
+ assertCompatible(root1, root2);
// TODO: need to have a possibility to check compatibility between
outdated (officially no more supported) version and current one.
// I.e. previous tree with filled deletedPrefixes and current without
- need to be compatible too, decided to make it later.
@@ -394,6 +468,32 @@ public class ConfigurationTreeComparatorSelfTest {
root2.addChildNodes(newNode2);
assertIncompatible(oldNode1, newNode1);
+
+ // Incorrectly renaming leaf nodes.
+
+ root1 = createRoot("root1");
+ oldNode2 = createChild("oldTestCount");
+ root1.addChildNodes(oldNode2);
+
+ root2 = createRoot("root1");
+ newNode2 = createChild("newTestCount", Set.of(Flags.IS_VALUE,
Flags.HAS_DEFAULT), Set.of("oldTestCount_misspelled"), List.of());
+ root2.addChildNodes(newNode2);
+
+ assertIncompatible(root1, root2);
+
+ // Incorrectly renaming intermediate nodes.
+
+ root1 = createRoot("root1");
+ oldNode2 = createChild("node1", Set.of());
+ oldNode2.addChildNodes(createChild("value"));
+ root1.addChildNodes(oldNode2);
+
+ root2 = createRoot("root1");
+ newNode2 = createChild("node_new", Set.of(),
Set.of("node_misspelled"), List.of());
+ newNode2.addChildNodes(createChild("value"));
+ root2.addChildNodes(newNode2);
+
+ assertIncompatible(root1, root2);
}
/**
@@ -410,7 +510,7 @@ public class ConfigurationTreeComparatorSelfTest {
EnumSet.of(Flags.IS_ROOT));
ConfigNode node1Ver1 = new ConfigNode(root, Map.of(Attributes.NAME,
"oldTestCount"), List.of(),
- EnumSet.of(Flags.IS_VALUE));
+ EnumSet.of(Flags.IS_VALUE, Flags.HAS_DEFAULT));
root.addChildNodes(node1Ver1);
@@ -421,7 +521,7 @@ public class ConfigurationTreeComparatorSelfTest {
ConfigNode node1Ver2 = new ConfigNode(root, Map.of(Attributes.NAME,
"newTestCount"),
List.of(),
- EnumSet.of(Flags.IS_VALUE), Set.of("oldTestCount"), List.of());
+ EnumSet.of(Flags.IS_VALUE, Flags.HAS_DEFAULT),
Set.of("oldTestCount"), List.of());
root.addChildNodes(node1Ver2);
@@ -431,7 +531,7 @@ public class ConfigurationTreeComparatorSelfTest {
EnumSet.of(Flags.IS_ROOT));
ConfigNode node = new ConfigNode(root, Map.of(Attributes.NAME,
"fixed"), List.of(),
- EnumSet.of(Flags.IS_VALUE));
+ EnumSet.of(Flags.IS_VALUE, Flags.HAS_DEFAULT));
root.addChildNodes(node);
@@ -460,12 +560,22 @@ public class ConfigurationTreeComparatorSelfTest {
return ConfigNode.createRoot(name, Object.class,
ConfigurationType.LOCAL, true);
}
- private static ConfigNode createChild(ConfigNode root1, String name) {
+ private static ConfigNode createChild(String name) {
+ return createChild(name, Set.of(), Set.of(), List.of());
+ }
+
+ private static ConfigNode createChild(String name, Set<Flags> flags) {
+ return createChild(name, flags, Set.of(), List.of());
+ }
+
+ private static ConfigNode createChild(String name, Set<Flags> flags,
Set<String> legacyNames, List<String> deletedPrefixes) {
return new ConfigNode(
- root1,
- Map.of(ConfigNode.Attributes.NAME, name, Attributes.CLASS,
int.class.getCanonicalName()),
+ null,
+ Map.of(ConfigNode.Attributes.NAME, name, Attributes.CLASS,
"Object"),
List.of(),
- EnumSet.of(Flags.IS_VALUE)
+ flags.isEmpty() ? EnumSet.noneOf(Flags.class) :
EnumSet.copyOf(flags),
+ legacyNames,
+ deletedPrefixes
);
}
@@ -491,12 +601,17 @@ public class ConfigurationTreeComparatorSelfTest {
private static void assertIncompatible(List<ConfigNode> oldConfig,
List<ConfigNode> newConfig, ComparisonContext compContext) {
try {
- assertCompatible(oldConfig, newConfig, compContext);
- } catch (IllegalStateException ignore) {
+ ConfigurationTreeComparator.ensureCompatible(oldConfig, newConfig,
compContext);
+ } catch (IllegalStateException e) {
// Expected exception
+ System.err.println("Error: " + e.getMessage());
return;
}
fail("Compatibility check passed unexpectedly.");
}
+
+ private static Set<Flags> createFlags(boolean hasDefault) {
+ return hasDefault ? Set.of(Flags.HAS_DEFAULT, Flags.IS_VALUE) :
Set.of(Flags.IS_VALUE);
+ }
}
diff --git
a/modules/runner/src/test/resources/compatibility/configuration/snapshot.bin
b/modules/runner/src/test/resources/compatibility/configuration/snapshot.bin
index 6e100c42880..2d1d8676a82 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