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/d9869ffd
Tree: http://git-wip-us.apache.org/repos/asf/geode/tree/d9869ffd
Diff: http://git-wip-us.apache.org/repos/asf/geode/diff/d9869ffd

Branch: refs/heads/feature/GEM-1483
Commit: d9869ffd5b317c9e577f3ee2107a8d4bf46a166d
Parents: b616e80
Author: Jared Stewart <jstew...@pivotal.io>
Authored: Wed Jun 7 20:44:56 2017 -0700
Committer: Jared Stewart <jstew...@pivotal.io>
Committed: Wed Jun 14 11:18:36 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/d9869ffd/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/d9869ffd/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/d9869ffd/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;
+  }
+}

Reply via email to