GEODE-3060: Introduce JUnit rule for testing the fully-assembled GFSH
Project: http://git-wip-us.apache.org/repos/asf/geode/repo Commit: http://git-wip-us.apache.org/repos/asf/geode/commit/d001baab Tree: http://git-wip-us.apache.org/repos/asf/geode/tree/d001baab Diff: http://git-wip-us.apache.org/repos/asf/geode/diff/d001baab Branch: refs/heads/feature/GEODE-2804 Commit: d001baabbad18de81cab0788909a4856f882b7a5 Parents: a0175b4 Author: Jared Stewart <jstew...@pivotal.io> Authored: Wed Jun 7 20:44:56 2017 -0700 Committer: Hitesh Khamesra <hkhame...@pivotal.io> Committed: Mon Jun 19 13:47:57 2017 -0700 ---------------------------------------------------------------------- .../cli/commands/StatusLocatorRealGfshTest.java | 50 ++++++++ .../geode/test/dunit/rules/gfsh/GfshRule.java | 116 +++++++++++++++++ .../geode/test/dunit/rules/gfsh/GfshScript.java | 124 +++++++++++++++++++ 3 files changed, 290 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/geode/blob/d001baab/geode-assembly/src/test/java/org/apache/geode/management/internal/cli/commands/StatusLocatorRealGfshTest.java ---------------------------------------------------------------------- diff --git a/geode-assembly/src/test/java/org/apache/geode/management/internal/cli/commands/StatusLocatorRealGfshTest.java b/geode-assembly/src/test/java/org/apache/geode/management/internal/cli/commands/StatusLocatorRealGfshTest.java new file mode 100644 index 0000000..82ee240 --- /dev/null +++ b/geode-assembly/src/test/java/org/apache/geode/management/internal/cli/commands/StatusLocatorRealGfshTest.java @@ -0,0 +1,50 @@ +/* + * 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.geode.management.internal.cli.commands; + +import org.apache.geode.test.dunit.rules.gfsh.GfshRule; +import org.apache.geode.test.dunit.rules.gfsh.GfshScript; +import org.apache.geode.test.junit.categories.DistributedTest; +import org.apache.geode.test.junit.categories.IntegrationTest; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +@Category(DistributedTest.class) +public class StatusLocatorRealGfshTest { + @Rule + public GfshRule gfshRule = new GfshRule(); + + @Test + public void statusLocatorSucceedsWhenConnected() throws Exception { + gfshRule.execute(GfshScript.of("start locator --name=locator1").awaitAtMost(1, TimeUnit.MINUTES) + .expectExitCode(0)); + + gfshRule.execute(GfshScript.of("connect", "status locator --name=locator1") + .awaitAtMost(1, TimeUnit.MINUTES).expectExitCode(0)); + } + + @Test + public void statusLocatorFailsWhenNotConnected() throws Exception { + gfshRule.execute(GfshScript.of("start locator --name=locator1").awaitAtMost(1, TimeUnit.MINUTES) + .expectExitCode(0)); + + gfshRule.execute(GfshScript.of("status locator --name=locator1") + .awaitAtMost(1, TimeUnit.MINUTES).expectExitCode(1)); + } +} http://git-wip-us.apache.org/repos/asf/geode/blob/d001baab/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshRule.java ---------------------------------------------------------------------- diff --git a/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshRule.java b/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshRule.java new file mode 100644 index 0000000..8109377 --- /dev/null +++ b/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshRule.java @@ -0,0 +1,116 @@ +/* + * 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.geode.test.dunit.rules.gfsh; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; + +import org.apache.geode.management.internal.cli.commands.StatusLocatorRealGfshTest; +import org.apache.geode.test.dunit.rules.RequiresGeodeHome; + +/** + * The {@code GfshRule} allows a test to execute Gfsh commands via the actual (fully-assembled) gfsh + * binaries. For a usage example, see {@link StatusLocatorRealGfshTest}. Each call to + * {@link GfshRule#execute(GfshScript)} will invoke the given gfsh script in a forked JVM. The + * {@link GfshRule#after()} method will attempt to clean up all forked JVMs. + */ +public class GfshRule extends ExternalResource { + private TemporaryFolder temporaryFolder = new TemporaryFolder(); + private List<Process> processes = new ArrayList<>(); + private Path gfsh; + + public Process execute(String... commands) { + return execute(GfshScript.of(commands)); + } + + public Process execute(GfshScript gfshScript) { + Process process; + try { + process = gfshScript.toProcessBuilder(gfsh, temporaryFolder.getRoot()).start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + processes.add(process); + gfshScript.awaitIfNecessary(process); + + return process; + } + + @Override + protected void before() throws IOException { + gfsh = new RequiresGeodeHome().getGeodeHome().toPath().resolve("bin/gfsh"); + assertThat(gfsh).exists(); + + temporaryFolder.create(); + } + + /** + * Attempts to stop any started servers/locators via pid file and tears down any remaining gfsh + * JVMs. + */ + @Override + protected void after() { + stopMembersQuietly(); + processes.forEach(Process::destroyForcibly); + processes.forEach((Process process) -> { + try { + // Process.destroyForcibly() may not terminate immediately + process.waitFor(1, TimeUnit.MINUTES); + } catch (InterruptedException ignore) { + // We ignore this exception so that we still attempt the rest of the cleanup. + } + }); + temporaryFolder.delete(); + } + + private void stopMembersQuietly() { + File[] directories = temporaryFolder.getRoot().listFiles(File::isDirectory); + + Predicate<File> isServerDir = (File directory) -> Arrays.stream(directory.list()) + .anyMatch(filename -> filename.endsWith("server.pid")); + + Predicate<File> isLocatorDir = (File directory) -> Arrays.stream(directory.list()) + .anyMatch(filename -> filename.endsWith("locator.pid")); + + Arrays.stream(directories).filter(isServerDir).forEach(this::stopServerInDir); + Arrays.stream(directories).filter(isLocatorDir).forEach(this::stopLocatorInDir); + } + + private void stopServerInDir(File dir) { + GfshScript stopServerScript = new GfshScript("stop server --dir=" + dir.getAbsolutePath()) + .awaitQuietlyAtMost(1, TimeUnit.MINUTES); + + execute(stopServerScript); + } + + private void stopLocatorInDir(File dir) { + GfshScript stopServerScript = new GfshScript("stop locator --dir=" + dir.getAbsolutePath()) + .awaitQuietlyAtMost(1, TimeUnit.MINUTES); + + execute(stopServerScript); + } +} http://git-wip-us.apache.org/repos/asf/geode/blob/d001baab/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshScript.java ---------------------------------------------------------------------- diff --git a/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshScript.java b/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshScript.java new file mode 100644 index 0000000..3ee1402 --- /dev/null +++ b/geode-assembly/src/test/java/org/apache/geode/test/dunit/rules/gfsh/GfshScript.java @@ -0,0 +1,124 @@ +/* + * 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.geode.test.dunit.rules.gfsh; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +public class GfshScript { + private final String[] commands; + private Integer timeout; + private TimeUnit timeoutTimeUnit; + private boolean awaitQuietly = false; + private Integer expectedExitValue; + + public GfshScript(String... commands) { + this.commands = commands; + } + + public static GfshScript of(String... commands) { + return new GfshScript(commands); + } + + + public GfshScript expectExitCode(int expectedExitCode) { + this.expectedExitValue = expectedExitCode; + + return this; + } + + /** + * Will cause the thread that executes {@link GfshScript#awaitIfNecessary} to wait, if necessary, + * until the subprocess executing this Gfsh script has terminated, or the specified waiting time + * elapses. + * + * @throws RuntimeException if the current thread is interrupted while waiting. + * @throws AssertionError if the specified waiting time elapses before the process exits. + */ + public GfshScript awaitAtMost(int timeout, TimeUnit timeUnit) { + this.timeout = timeout; + this.timeoutTimeUnit = timeUnit; + + return this; + } + + /** + * Will cause the thread that executes {@link GfshScript#awaitIfNecessary} to wait, if necessary, + * until the subprocess executing this Gfsh script has terminated, or the specified waiting time + * elapses. + */ + public GfshScript awaitQuietlyAtMost(int timeout, TimeUnit timeUnit) { + this.awaitQuietly = true; + + return awaitAtMost(timeout, timeUnit); + } + + + protected ProcessBuilder toProcessBuilder(Path gfshPath, File workingDir) { + String[] gfshCommands = new String[commands.length + 1]; + gfshCommands[0] = gfshPath.toAbsolutePath().toString(); + + for (int i = 0; i < commands.length; i++) { + gfshCommands[i + 1] = "-e " + commands[i]; + } + + return new ProcessBuilder(gfshCommands).inheritIO().directory(workingDir); + } + + protected void awaitIfNecessary(Process process) { + if (shouldAwaitQuietly()) { + awaitQuietly(process); + } else if (shouldAwaitLoudly()) { + awaitLoudly(process); + } + + if (expectedExitValue != null) { + assertThat(process.exitValue()).isEqualTo(expectedExitValue); + } + } + + private void awaitQuietly(Process process) { + try { + process.waitFor(timeout, timeoutTimeUnit); + } catch (InterruptedException ignore) { + // ignore since we are waiting *quietly* + } + } + + private void awaitLoudly(Process process) { + boolean exited; + try { + exited = process.waitFor(timeout, timeoutTimeUnit); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + assertThat(exited).isTrue(); + } + + private boolean shouldAwait() { + return timeoutTimeUnit != null; + } + + private boolean shouldAwaitQuietly() { + return shouldAwait() && awaitQuietly; + } + + private boolean shouldAwaitLoudly() { + return shouldAwait() && !awaitQuietly; + } +}