This is an automated email from the ASF dual-hosted git repository. amashenkov pushed a commit to branch ignite-18171 in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit a1bb57677bbec17fd7ddfa4e0cee016c34fc60dc Author: amashenkov <[email protected]> AuthorDate: Tue Nov 22 15:24:57 2022 +0300 Added test template. --- .../ignite/internal/ItNodeStartStopTest.java | 274 +++++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ItNodeStartStopTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ItNodeStartStopTest.java new file mode 100644 index 0000000000..9bfdd00d12 --- /dev/null +++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ItNodeStartStopTest.java @@ -0,0 +1,274 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; +import org.apache.ignite.internal.testframework.WorkDirectory; +import org.apache.ignite.internal.testframework.WorkDirectoryExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; + +@ExtendWith(WorkDirectoryExtension.class) +public class ItNodeStartStopTest extends BaseIgniteAbstractTest { + /** Work directory. */ + @WorkDirectory + private static Path WORK_DIR; + + /** Nodes configurations. */ + private final Map<String, String> nodesCfg = Map.of( + "C", "", + "M", "", + "D", "", + "D2", "" + ); // Node name -> config. + + /** Cluster nodes. */ + private final List<String> clusterNodes = new ArrayList<>(); // TODO: replace with Map<String, Ignite> (nodeName->node) ? + + /** Runs after each sequence. */ + @AfterEach + public void after() { +// clusterNodes.forEach(node -> IgnitionManager.stop(node.name())); + clusterNodes.clear(); + } + + /** + * Test factory. + * + * @return Tests. + */ + @TestFactory + public Stream<DynamicTest> factory() { + return new NodesStartStopGenerator( + this::startNode, + this::stopNode, + testExecutor(this::testStartStopNodes), // Wrap test method with logging executor. + nodesCfg.keySet(), + ItNodeStartStopTest::isValidNode + ).build(); + } + + private static boolean isValidNode(String nodeName, Set<String> grid) { + return (!grid.isEmpty() || "C".equals(nodeName)) // CMG node always starts first. + && (!"D2".equals(nodeName) || grid.contains("D")); // Data nodes are interchangeable. + } + + private void testStartStopNodes() { + System.out.println("Grid state: " + String.join("", String.join(", ", clusterNodes))); + } + + private void startNode(String nodeName) { + System.out.println("Starting node: " + nodeName); +// CompletableFuture<Ignite> node = IgnitionManager.start(nodeName, nodesCfg.get(nodeName), WORK_DIR.resolve(nodeName)); +// +// clusterNodes.add(node.join()); + clusterNodes.add(nodeName); + } + + private void stopNode(String nodeName) { + System.out.println("Stopping node: " + nodeName); +// Node node = clusterNodes.stream().filter(n -> nodeName.equals(n.name())).findFirst().orElseThrow(); +// IgnitionManager.stop(nodeName); + + clusterNodes.remove(nodeName); + } + + /** + * Test sequence generator. + */ + private Consumer<TestInfo> testExecutor(Runnable testRunnable) { + return (info) -> { + try { + setupBase(info, WORK_DIR); + + testRunnable.run(); + + } finally { + tearDownBase(info); + } + }; + } + + /** + * Filter out grids that where already seen before. + */ + static class VisitedFilter implements Predicate<Set<String>> { + + private final Set<Set<String>> visitedGrids = new HashSet<>(); + + @Override + public boolean test(Set<String> g) { + return visitedGrids.add(new HashSet<>(g)); + } + } + + public static class NodesStartStopGenerator { + + private final Consumer<String> nodeStarter; + private final Consumer<String> nodeStopper; + private final Consumer<TestInfo> testMethodBody; + + private final Set<String> currentGrid = new TreeSet<>(); + private final Predicate<Set<String>> gridFilter = new VisitedFilter(); // Duplicates filter. + private final BiPredicate<String, Set<String>> nodeFilter; + + private final Set<String> gridNodes; + + /** + * Creates test sequence generator. + * + * @param nodeStarter Function, starts node by node name. + * @param nodeStopper Function, stops node by node name. + * @param testMethodBody Test method body executor. + * @param nodes Node names. + * @param nodeFilter Node filter accepts node name to start and current grid state. + */ + NodesStartStopGenerator( + Consumer<String> nodeStarter, + Consumer<String> nodeStopper, + Consumer<TestInfo> testMethodBody, + Set<String> nodes, + BiPredicate<String, Set<String>> nodeFilter + ) { + this.nodeStarter = nodeStarter; + this.nodeStopper = nodeStopper; + this.testMethodBody = testMethodBody; + this.nodeFilter = nodeFilter; + this.gridNodes = nodes; + } + + /** + * Generates test sequence for all valid grid configurations that can be created with given nodes. + * + * @return Tests sequence. + */ + public Stream<DynamicTest> build() { + List<Executable> actionSequence = new ArrayList<>(); + + generate(actionSequence::add, gridNodes); + + return actionSequence.stream() + .map(action -> { + if (action instanceof TestExecutable) { + TestExecutable namedAction = (TestExecutable) action; + + return DynamicTest.dynamicTest(namedAction.getDisplayName(), namedAction); + } else { + try { // Execute instantly and proceed with the next. + action.execute(); + } catch (Throwable e) { + fail(e); + } + + return null; + } + }) + .filter(Objects::nonNull); + } + + /** Generates tests execution sequence recursively. */ + private void generate(Consumer<Executable> actionCollector, Set<String> availableNodes) { + for (String nodeName : availableNodes) { + if (!nodeFilter.test(nodeName, currentGrid)) { + continue; // Skip node from adding to the current grid. + } + + String gridStateString = currentGrid.stream().map(Objects::toString).collect(Collectors.joining(", ")); + String prevGridState = '[' + gridStateString + "]"; + String nextGridState = currentGrid.isEmpty() ? '[' + nodeName + ']' : '[' + gridStateString + ", " + nodeName + ']'; + + actionCollector.accept(() -> nodeStarter.accept(nodeName)); + currentGrid.add(nodeName); + + actionCollector.accept(new TestExecutable(prevGridState + " -> " + nextGridState, testMethodBody)); + + if (availableNodes.size() > 1) { + Set<String> otherNodes = new HashSet<>(availableNodes); + otherNodes.remove(nodeName); + if (gridFilter.test(otherNodes)) { // Avoid generating duplicate subsequences. + generate(actionCollector, otherNodes); + } + } + + currentGrid.remove(nodeName); + actionCollector.accept(() -> nodeStopper.accept(nodeName)); + + if (!currentGrid.isEmpty()) { + actionCollector.accept(new TestExecutable(nextGridState + " -> " + prevGridState, testMethodBody)); + } + } + } + } + + static class TestExecutable implements TestInfo, Executable { + private final Consumer<TestInfo> delegate; + + private final String name; + + TestExecutable(String name, Consumer<TestInfo> delegate) { + this.name = name; + this.delegate = delegate; + } + + @Override + public void execute() throws Throwable { + delegate.accept(this); + } + + @Override + public String getDisplayName() { + return name; + } + + @Override + public Set<String> getTags() { + return Set.of(); + } + + @Override + public Optional<Class<?>> getTestClass() { + return Optional.of(ItNodeStartStopTest.class); + } + + @Override + public Optional<Method> getTestMethod() { + return Optional.empty(); + } + } +}
