AMashenkov commented on code in PR #6241:
URL: https://github.com/apache/ignite-3/pull/6241#discussion_r2209926940


##########
modules/runner/src/test/java/org/apache/ignite/internal/configuration/compatibility/framework/Comp2.java:
##########
@@ -0,0 +1,447 @@
+package org.apache.ignite.internal.configuration.compatibility.framework;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import 
org.apache.ignite.internal.configuration.compatibility.GenerateConfigurationSnapshot;
+import 
org.apache.ignite.internal.configuration.compatibility.framework.ConfigNode.NodeReference;
+import 
org.apache.ignite.internal.configuration.compatibility.framework.ConfigurationTreeComparator.ComparisonContext;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.util.CollectionUtils;
+
+public class Comp2 {
+
+    private static final IgniteLogger LOG = 
Loggers.forClass(GenerateConfigurationSnapshot.class);
+
+    private final ComparisonContext comparisonContext;
+
+    public Comp2() {
+        this.comparisonContext = new ComparisonContext();
+    }
+
+    public Comp2(ComparisonContext comparisonContext) {
+        this.comparisonContext = comparisonContext;
+    }
+
+    public void ensureCompatible(List<ConfigNode> previousRoots, 
List<ConfigNode> currentRoots) {
+        Map<String, Map<String, ConfigNode>> previousRootsByKind = 
groupByKind(previousRoots);
+        Map<String, Map<String, ConfigNode>> currentRootsByKind = 
groupByKind(currentRoots);
+
+        // Compare configuration kinds
+        if (!previousRootsByKind.keySet().equals(currentRootsByKind.keySet())) 
{
+            String error = format("Configuration kind do not match. Expected: 
{} but got {}",
+                    previousRootsByKind.keySet(),
+                    currentRootsByKind.keySet()
+            );
+
+            throw new IllegalStateException(error);
+        }
+
+        // Then compare roots one by one
+        for (Entry<String, Map<String, ConfigNode>> entry : 
previousRootsByKind.entrySet()) {
+            Map<String, ConfigNode> prev = entry.getValue();
+            Map<String, ConfigNode> current = 
currentRootsByKind.get(entry.getKey());
+
+            compareConfigRoots(prev, current);
+        }
+    }
+
+    private void compareConfigRoots(Map<String, ConfigNode> previousRoots, 
Map<String, ConfigNode> currentRoots) {
+        Set<String> removed = 
CollectionUtils.difference(previousRoots.keySet(), currentRoots.keySet());
+
+        // Check config roots
+        if (!removed.isEmpty()) {
+            String error = format("Incompatible change. Some of the root keys 
has been removed.\n"
+                            + "Removed root keys: {}\n"
+                            + "Previous root keys: {}\n"
+                            + "Current root keys: {}\n",
+                    removed, previousRoots.keySet(), currentRoots.keySet());
+
+            throw new IllegalStateException(error);
+        }
+
+        for (Map.Entry<String, ConfigNode> e : previousRoots.entrySet()) {
+            ConfigNode current = currentRoots.get(e.getKey());
+            if (current == null) {
+                // adding a configuration root is a compatible change.
+                continue;
+            }
+
+            ConfigNode previous = e.getValue();
+            ensureCompareCompatible(previous, current);
+        }
+    }
+
+    public void ensureCompareCompatible(ConfigNode previous, ConfigNode 
current) {
+        // List of paths to support polymorphic nodes.
+        // Converts configuration trees to a map of paths
+        Map<String, List<Path>> previousNodes = buildPaths(previous);
+        Map<String, List<Path>> currentNodes = buildPaths(current);
+
+        if (LOG.isInfoEnabled()) {
+            LOG.info("Previous: {}", pathsToString(previousNodes));
+        }
+
+        if (LOG.isInfoEnabled()) {
+            LOG.info("Current: {}", pathsToString(previousNodes));
+        }
+
+        Set<String> removed = 
CollectionUtils.difference(previousNodes.keySet(), currentNodes.keySet());
+        Set<String> added = CollectionUtils.difference(currentNodes.keySet(), 
previousNodes.keySet());
+        Set<String> unchanged = 
CollectionUtils.intersect(previousNodes.keySet(), currentNodes.keySet());
+
+        List<String> errors = new ArrayList<>();
+
+        validateRemovals(removed, comparisonContext, errors);
+        validateAdded(currentNodes, added, errors);
+        validateRemaining(previousNodes, currentNodes, unchanged, errors);
+
+        reportErrors(errors);
+    }
+
+    private static void validateRemovals(Set<String> keys, ComparisonContext 
comparisonContext, List<String> errors) {
+        LOG.info("Removed keys: {}", keys);
+
+        Set<String> removedFiltered = keys.stream().filter(r -> 
!comparisonContext.shouldIgnore(r)).collect(Collectors.toSet());
+        if (!removedFiltered.isEmpty()) {
+            errors.add("These properties has been removed: " + keys);
+        }
+    }
+
+    private static void validateAdded(Map<String, List<Path>> currentNodes, 
Set<String> keys, List<String> errors) {
+        LOG.info("Added keys: {}", keys);
+
+        Set<String> nonDefaults = new HashSet<>();
+
+        for (String key : keys) {
+            List<Path> addedPaths = currentNodes.get(key);
+
+            for (Path path : addedPaths) {
+                for (ConfigNode node : path.nodes) {
+                    if (node.isValue() && !node.hasDefault()) {
+                        nonDefaults.add(node.path());
+                    }
+                }
+            }
+        }
+
+        if (!nonDefaults.isEmpty()) {
+            errors.add("Adding properties without default values is 
incompatible change: " + nonDefaults);
+        }
+    }
+
+    private static void validateRemaining(
+            Map<String, List<Path>> previousNodes,
+            Map<String, List<Path>> currentNodes,
+            Set<String> keys,
+            List<String> errors
+    ) {
+
+        LOG.info("Remained keys: {}", keys);
+
+        for (String key : keys) {
+            List<Path> previous = previousNodes.get(key);
+            List<Path> current = currentNodes.get(key);
+
+            if (previous.size() == 1 && current.size() == 1 || previous.size() 
== current.size()) {
+                // Both nodes are non-polymorphic or polymorphic nodes has not 
been moved between a base class and its subclasses. 
+                for (int i = 0; i < previous.size(); i++) {
+                    validatePath(previous.get(i), current.get(i), errors);
+                }
+            } else if (previous.size() == 1 && noPolymorphicNodes(previous)) {
+                // From non-polymorphic to a polymorphic one
+                validateNonPolymorphicToPolymorphic(previous.get(0), current, 
errors);
+            } else if (current.size() == 1 && noPolymorphicNodes(current)) {
+                // From non-polymorphic to a polymorphic one
+                validatePolymorphicToNonPolymorphic(previous, current.get(0), 
errors);
+            } else {
+                // From one polymorphic case to another polymorphic
+                validatePolymorphicToPolymorphic(previous, current, errors);
+            }
+        }
+
+        reportErrors(errors);
+    }
+
+    private static void validatePath(Path previousPath, Path currentPath, 
List<String> errors) {
+        for (int i = 0; i < previousPath.nodes.size(); i++) {
+            ConfigNode previous = previousPath.nodes.get(i);
+            ConfigNode current = currentPath.nodes.get(i);
+
+            boolean match = ConfigurationTreeComparator.match(previous, 
current, null);
+            if (!match) {
+                errors.add("Node has incompatible changes: " + current.path());
+            }
+        }
+    }
+
+    private static void reportErrors(List<String> errors) {
+        if (!errors.isEmpty()) {
+            StringBuilder sb = new StringBuilder("Incompatible 
changes").append(System.lineSeparator());
+            for (String message : errors) {
+                sb.append('\t').append(message).append(System.lineSeparator());
+            }
+
+            throw new IllegalStateException(sb.toString());
+        }
+    }
+
+    private static boolean noPolymorphicNodes(List<Path> paths) {
+        return paths.stream().allMatch(p -> p.subclass.isEmpty());
+    }
+
+    private static void validateNonPolymorphicToPolymorphic(Path previous, 
List<Path> currentPaths, List<String> errors) {
+        // Extracting a base class + subclasses is compatible 
+        // unless there are incompatible changes between nodes.
+        for (Path current : currentPaths) {
+            validatePath(previous, current, errors);
+        }
+    }
+
+    private static void validatePolymorphicToNonPolymorphic(List<Path> 
previousPaths, Path current, List<String> errors) {
+        // Converting a base class with its subclasses to a simple config is 
compatible
+        // unless there are incompatible changes between nodes.
+        for (Path previous : previousPaths) {
+            validatePath(previous, current, errors);
+        }
+    }
+
+    private static void validatePolymorphicToPolymorphic(List<Path> 
previousPaths, List<Path> currentPaths, List<String> errors) {
+        // Moving a node w/ default value from a subclass to its base class is 
incompatible change. 
+        // Moving a node w/o default value from a subclass to its base class 
is valid only if there is a single subclass
+
+        Optional<Path> previousBaseClass = previousPaths.stream().filter(p -> 
p.subclass.isEmpty()).findFirst();
+        Optional<Path> currentBaseClass = currentPaths.stream().filter(p -> 
p.subclass.isEmpty()).findFirst();
+
+        if (previousBaseClass.isEmpty() && currentBaseClass.isPresent()) {
+            // Moving a value from a subclass to the base class
+            Path current = currentBaseClass.orElseThrow(() -> new 
IllegalStateException("Unexpected"));
+            ConfigNode valueNode = current.nodes.get(current.nodes.size() - 1);
+
+            // A value w/o default and there are more than 2 subclasses
+            if (!valueNode.hasDefault() && currentPaths.size() > 2) {
+                errors.add("Unable to move non-default node from a subclass to 
its base class: " + valueNode.path());
+            }
+
+        } else if (previousBaseClass.isPresent() && 
currentBaseClass.isEmpty()) {
+            // Moving a value from a base class to a subclass
+            Path previous = previousBaseClass.orElseThrow(() -> new 
IllegalStateException("Unexpected"));
+            ConfigNode valueNode = previous.nodes.get(previous.nodes.size() - 
1);
+
+            // A value w/o default and there are more than 2 subclasses 
+            if (!valueNode.hasDefault() && previousPaths.size() > 2) {
+                errors.add("Unable to move non-default node from a base class 
to a subclass: " + valueNode.path());
+            }
+        }
+    }
+
+    private static Map<String, Map<String, ConfigNode>> 
groupByKind(List<ConfigNode> nodes) {
+        Map<String, Map<String, ConfigNode>> out = new LinkedHashMap<>();
+
+        for (ConfigNode node : nodes) {
+            Map<String, ConfigNode> byKind = out.computeIfAbsent(node.kind(), 
(k) -> new LinkedHashMap<>());
+            ConfigNode prev = byKind.put(node.name(), node);
+            assert prev == null : "Duplicate name";
+        }
+
+        return out;
+    }
+
+    private static Map<String, List<Path>> buildPaths(ConfigNode node) {
+        Map<String, List<Path>> paths = new LinkedHashMap<>();
+
+        collect(node, new CurrentPath(), paths);
+
+        for (List<Path> path : paths.values()) {
+            path.sort(Comparator.comparing(a -> a.subclass));
+        }
+
+        return paths;
+    }
+
+    /**
+     * Converts a configuration tree into lists of paths from a root to leafs.
+     * 
+     * <pre>
+     *    A
+     *  /  \
+     * B    C
+     *     / \  
+     *    D   E
+     * </pre>
+     * 
+     * Gets converted to:
+     * <pre>
+     *     A.B: [A, B], 
+     *     A.C.D: [A, C, D], 
+     *     A.C.E: {A, C, E}

Review Comment:
   ```suggestion
        *     A.C.E: [A, C, E]
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscr...@ignite.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to