This is an automated email from the ASF dual-hosted git repository.

samt pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new a821214  CASSANDRA-15942 Improve tooling testing framework
a821214 is described below

commit a8212149aba7a85139de9758988f6e01f7bc8d91
Author: Bereng <berenguerbl...@gmail.com>
AuthorDate: Tue Jul 14 16:44:58 2020 +0200

    CASSANDRA-15942 Improve tooling testing framework
    
    Patch by Berenguer Blasi; reviewed by Robert Stupp
    and Sam Tunnicliffe for CASSANDRA-15942
---
 .../config/DatabaseDescriptorRefTest.java          |   2 +-
 test/unit/org/apache/cassandra/cql3/CQLTester.java |  66 +++-
 .../org/apache/cassandra/tools/BulkLoaderTest.java |  28 +-
 .../apache/cassandra/tools/ClearSnapshotTest.java  |  91 ++---
 .../cassandra/tools/CompactionStressTest.java      |  41 +-
 .../org/apache/cassandra/tools/GetVersionTest.java |   6 +-
 .../apache/cassandra/tools/LoaderOptionsTest.java  |   2 +-
 .../{ToolsTester.java => OfflineToolUtils.java}    |  66 +---
 .../tools/SSTableExpiredBlockersTest.java          |  10 +-
 .../apache/cassandra/tools/SSTableExportTest.java  |  11 +-
 .../cassandra/tools/SSTableLevelResetterTest.java  |  11 +-
 .../cassandra/tools/SSTableMetadataViewerTest.java |  10 +-
 .../cassandra/tools/SSTableOfflineRelevelTest.java |  10 +-
 .../tools/SSTableRepairedAtSetterTest.java         |  11 +-
 .../cassandra/tools/StandaloneSSTableUtilTest.java |  11 +-
 .../cassandra/tools/StandaloneScrubberTest.java    |  11 +-
 .../cassandra/tools/StandaloneSplitterTest.java    |   8 +-
 .../cassandra/tools/StandaloneUpgraderTest.java    |  11 +-
 .../cassandra/tools/StandaloneVerifierTest.java    |  11 +-
 .../org/apache/cassandra/tools/ToolRunner.java     | 437 +++++++++++++++++++++
 20 files changed, 676 insertions(+), 178 deletions(-)

diff --git 
a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java 
b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
index 576862b..8f731e2 100644
--- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
@@ -49,7 +49,7 @@ import static org.junit.Assert.fail;
  * unexpected threads.
  *
  * {@link DatabaseDescriptor#toolInitialization()} is tested via unit tests 
extending
- * {@link org.apache.cassandra.tools.ToolsTester}.
+ * {@link org.apache.cassandra.tools.OfflineToolUtils}.
  */
 public class DatabaseDescriptorRefTest
 {
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java 
b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index 619fdad..0455f97 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -22,9 +22,12 @@ import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
 import java.net.ServerSocket;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
+import java.rmi.server.RMISocketFactory;
 import java.util.*;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -34,6 +37,12 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+import javax.management.MBeanServerConnection;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXConnectorServer;
+import javax.management.remote.JMXServiceURL;
+
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
@@ -80,6 +89,7 @@ import org.apache.cassandra.transport.ProtocolVersion;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.JMXServerUtils;
 import org.apache.cassandra.security.ThreadAwareSecurityManager;
 
 import static junit.framework.Assert.assertNotNull;
@@ -103,6 +113,11 @@ public abstract class CQLTester
     public static final String RACK1 = "rack1";
 
     private static org.apache.cassandra.transport.Server server;
+    private static JMXConnectorServer jmxServer;
+    protected static String jmxHost;
+    protected static int jmxPort;
+    protected static MBeanServerConnection jmxConnection;
+
     protected static final int nativePort;
     protected static final InetAddress nativeAddr;
     protected static final Set<InetAddressAndPort> remoteAddrs = new 
HashSet<>();
@@ -240,6 +255,43 @@ public abstract class CQLTester
         AuditLogManager.instance.initialize();
         isServerPrepared = true;
     }
+    
+    /**
+     * Starts the JMX server. It's safe to call this method multiple times.
+     */
+    public static void startJMXServer() throws Exception
+    {
+        if (jmxServer != null)
+            return;
+
+        InetAddress loopback = InetAddress.getLoopbackAddress();
+        jmxHost = loopback.getHostAddress();
+        try (ServerSocket sock = new ServerSocket())
+        {
+            sock.bind(new InetSocketAddress(loopback, 0));
+            jmxPort = sock.getLocalPort();
+        }
+
+        jmxServer = JMXServerUtils.createJMXServer(jmxPort, true);
+        jmxServer.start();
+    }
+    
+    public static void createMBeanServerConnection() throws Exception
+    {
+        assert jmxServer != null : "jmxServer not started";
+
+        Map<String, Object> env = new HashMap<>();
+        env.put("com.sun.jndi.rmi.factory.socket", 
RMISocketFactory.getDefaultSocketFactory());
+        JMXConnector jmxc = JMXConnectorFactory.connect(getJMXServiceURL(), 
env);
+        jmxConnection =  jmxc.getMBeanServerConnection();
+    }
+
+    public static JMXServiceURL getJMXServiceURL() throws MalformedURLException
+    {
+        assert jmxServer != null : "jmxServer not started";
+
+        return new 
JMXServiceURL(String.format("service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", 
jmxHost, jmxPort));
+    }
 
     public static void cleanupAndLeaveDirs() throws IOException
     {
@@ -399,7 +451,19 @@ public abstract class CQLTester
             }
         });
     }
-
+    
+    public static List<String> buildNodetoolArgs(List<String> args)
+    {
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add("bin/nodetool");
+        allArgs.add("-p");
+        allArgs.add(Integer.toString(jmxPort));
+        allArgs.add("-h");
+        allArgs.add(jmxHost);
+        allArgs.addAll(args);
+        return allArgs;
+    }
+    
     // lazy initialization for all tests that require Java Driver
     protected static void requireNetwork() throws ConfigurationException
     {
diff --git a/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java 
b/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
index 6ed38a0..354511a 100644
--- a/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
+++ b/test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
@@ -24,15 +24,22 @@ import org.junit.runner.RunWith;
 import com.datastax.driver.core.exceptions.NoHostAvailableException;
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class BulkLoaderTest extends ToolsTester
+public class BulkLoaderTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
-    public void testBulkLoader_NoArgs()
+    public void testBulkLoader_NoArgs() throws Exception
     {
-        runTool(1, "org.apache.cassandra.tools.BulkLoader");
+        ToolRunner tool = 
runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader");
+        assertEquals(1, tool.getExitCode());
+        assertTrue(!tool.getStderr().isEmpty());
+        
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -40,13 +47,14 @@ public class BulkLoaderTest extends ToolsTester
         assertKeyspaceNotLoaded();
         assertServerNotLoaded();
     }
-
+    
     @Test
     public void testBulkLoader_WithArgs() throws Exception
     {
         try
         {
-            runTool(0, "org.apache.cassandra.tools.BulkLoader", "-d", 
"127.9.9.1", sstableDirName("legacy_sstables", "legacy_ma_simple"));
+            runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader", 
"-d", "127.9.9.1", OfflineToolUtils.sstableDirName("legacy_sstables", 
"legacy_ma_simple"))
+                   .waitAndAssertOnCleanExit();
             fail();
         }
         catch (RuntimeException e)
@@ -69,7 +77,8 @@ public class BulkLoaderTest extends ToolsTester
     {
         try
         {
-            runTool(0, "org.apache.cassandra.tools.BulkLoader", "-d", 
"127.9.9.1", "--port", "9042", sstableDirName("legacy_sstables", 
"legacy_ma_simple"));
+            runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader", 
"-d", "127.9.9.1", "--port", "9042", 
OfflineToolUtils.sstableDirName("legacy_sstables", "legacy_ma_simple"))
+                  .waitAndAssertOnCleanExit();
             fail();
         }
         catch (RuntimeException e)
@@ -92,7 +101,8 @@ public class BulkLoaderTest extends ToolsTester
     {
         try
         {
-            runTool(0, "org.apache.cassandra.tools.BulkLoader", "-d", 
"127.9.9.1:9042", "--port", "9041", sstableDirName("legacy_sstables", 
"legacy_ma_simple"));
+            runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader", 
"-d", "127.9.9.1:9042", "--port", "9041", 
OfflineToolUtils.sstableDirName("legacy_sstables", "legacy_ma_simple"))
+                  .waitAndAssertOnCleanExit();
             fail();
         }
         catch (RuntimeException e)
@@ -115,7 +125,7 @@ public class BulkLoaderTest extends ToolsTester
     {
         try
         {
-            runTool(1, "org.apache.cassandra.tools.BulkLoader", "-d", 
"127.9.9.1", "--port", "9041", sstableDirName("legacy_sstables", 
"legacy_ma_simple"));
+            runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader", 
"-d", "127.9.9.1", "--port", "9041", 
OfflineToolUtils.sstableDirName("legacy_sstables", "legacy_ma_simple"));
         }
         catch (RuntimeException e)
         {
@@ -128,7 +138,7 @@ public class BulkLoaderTest extends ToolsTester
     {
         try
         {
-            runTool(1, "org.apache.cassandra.tools.BulkLoader", "-d", 
"127.9.9.1:9041", sstableDirName("legacy_sstables", "legacy_ma_simple"));
+            runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader", 
"-d", "127.9.9.1:9041", OfflineToolUtils.sstableDirName("legacy_sstables", 
"legacy_ma_simple"));
         }
         catch (RuntimeException e)
         {
diff --git a/test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java 
b/test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java
index a73493b..7e70467 100644
--- a/test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java
+++ b/test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java
@@ -20,102 +20,85 @@ package org.apache.cassandra.tools;
 
 import java.io.IOException;
 import java.util.Map;
+
 import javax.management.openmbean.TabularData;
 
-import org.apache.commons.lang3.ArrayUtils;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import org.apache.cassandra.SchemaLoader;
-import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.service.EmbeddedCassandraService;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-public class ClearSnapshotTest extends ToolsTester
+public class ClearSnapshotTest extends CQLTester
 {
-    private static EmbeddedCassandraService cassandra;
-    private static String initialJmxPortValue;
     private static NodeProbe probe;
-    private static final int JMX_PORT = 7188;
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
 
     @BeforeClass
-    public static void setup() throws IOException
+    public static void setup() throws Exception
     {
-        // Set system property to enable JMX port on localhost for embedded 
server
-        initialJmxPortValue = System.getProperty("cassandra.jmx.local.port");
-        System.setProperty("cassandra.jmx.local.port", 
String.valueOf(JMX_PORT));
-
-        SchemaLoader.prepareServer();
-        // CASSANDRA-15776 - size_estimates and table_estimates get truncated 
on startup, and auto snapshot is true by default for tests
-        // set it false so the test state doesn't see those snapshots
-        DatabaseDescriptor.setAutoSnapshot(false);
-        cassandra = new EmbeddedCassandraService();
-        cassandra.start();
-
-        probe = new NodeProbe("127.0.0.1", JMX_PORT);
+        startJMXServer();
+        probe = new NodeProbe(jmxHost, jmxPort);
     }
 
     @AfterClass
     public static void teardown() throws IOException
     {
-        cassandra.stop();
-        if (initialJmxPortValue != null)
-        {
-            System.setProperty("cassandra.jmx.local.port", 
initialJmxPortValue);
-        }
-
         probe.close();
     }
 
-    private String[] constructParamaterArray(final String command, final 
String... commandParams)
-    {
-        String[] baseCommandLine = {"-p", String.valueOf(JMX_PORT), command};
-        return ArrayUtils.addAll(baseCommandLine, commandParams);
-    }
-
     @Test
     public void testClearSnapshot_NoArgs() throws IOException
     {
-        runTool(2, "org.apache.cassandra.tools.NodeTool",
-                constructParamaterArray("clearsnapshot"));
+        ToolRunner tool = runner.invokeNodetool("clearsnapshot");
+        assertEquals(2, tool.getExitCode());
+        assertTrue("Tool stderr: " +  tool.getStderr(), 
tool.getStderr().contains("Specify snapshot name or --all"));
+        
+        runner.invokeNodetool("clearsnapshot", 
"--all").waitAndAssertOnCleanExit();
     }
 
     @Test
     public void testClearSnapshot_AllAndName() throws IOException
     {
-        runTool(2, "org.apache.cassandra.tools.NodeTool",
-                constructParamaterArray("clearsnapshot", "-t", "some-name", 
"--all"));
+        ToolRunner tool = runner.invokeNodetool("clearsnapshot", "-t", 
"some-name", "--all");
+        assertEquals(2, tool.getExitCode());
+        assertTrue("Tool stderr: " +  tool.getStderr(), 
tool.getStderr().contains("Specify only one of snapshot name or --all"));
     }
 
     @Test
     public void testClearSnapshot_RemoveByName() throws IOException
     {
-         runTool(0,"org.apache.cassandra.tools.NodeTool",
-                 constructParamaterArray("snapshot","-t","some-name"));
-
-         Map<String, TabularData> snapshots_before = 
probe.getSnapshotDetails();
-         Assert.assertTrue(snapshots_before.containsKey("some-name"));
-
-         runTool(0,"org.apache.cassandra.tools.NodeTool",
-                 constructParamaterArray("clearsnapshot","-t","some-name"));
-         Map<String, TabularData> snapshots_after = probe.getSnapshotDetails();
-         Assert.assertFalse(snapshots_after.containsKey("some-name"));
+        ToolRunner tool = 
runner.invokeNodetool("snapshot","-t","some-name").waitAndAssertOnCleanExit();
+        assertTrue(!tool.getStdout().isEmpty());
+        
+        Map<String, TabularData> snapshots_before = probe.getSnapshotDetails();
+        Assert.assertTrue(snapshots_before.containsKey("some-name"));
+        
+        tool = 
runner.invokeNodetool("clearsnapshot","-t","some-name").waitAndAssertOnCleanExit();
+        assertTrue(!tool.getStdout().isEmpty());
+        
+        Map<String, TabularData> snapshots_after = probe.getSnapshotDetails();
+        Assert.assertFalse(snapshots_after.containsKey("some-name"));
     }
 
     @Test
     public void testClearSnapshot_RemoveMultiple() throws IOException
     {
-        runTool(0,"org.apache.cassandra.tools.NodeTool",
-                constructParamaterArray("snapshot","-t","some-name"));
-        runTool(0,"org.apache.cassandra.tools.NodeTool",
-                constructParamaterArray("snapshot","-t","some-other-name"));
-
+        ToolRunner tool = 
runner.invokeNodetool("snapshot","-t","some-name").waitAndAssertOnCleanExit();
+        assertTrue(!tool.getStdout().isEmpty());
+        tool = 
runner.invokeNodetool("snapshot","-t","some-other-name").waitAndAssertOnCleanExit();
+        assertTrue(!tool.getStdout().isEmpty());
+        
         Map<String, TabularData> snapshots_before = probe.getSnapshotDetails();
         Assert.assertTrue(snapshots_before.size() == 2);
 
-        runTool(0,"org.apache.cassandra.tools.NodeTool",
-                constructParamaterArray("clearsnapshot","--all"));
+        tool = 
runner.invokeNodetool("clearsnapshot","--all").waitAndAssertOnCleanExit();
+        assertTrue(!tool.getStdout().isEmpty());
+        
         Map<String, TabularData> snapshots_after = probe.getSnapshotDetails();
         Assert.assertTrue(snapshots_after.size() == 0);
     }
diff --git a/test/unit/org/apache/cassandra/tools/CompactionStressTest.java 
b/test/unit/org/apache/cassandra/tools/CompactionStressTest.java
index c8b0b97..651e24d 100644
--- a/test/unit/org/apache/cassandra/tools/CompactionStressTest.java
+++ b/test/unit/org/apache/cassandra/tools/CompactionStressTest.java
@@ -26,12 +26,14 @@ import org.junit.runner.RunWith;
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class CompactionStressTest extends ToolsTester
+public class CompactionStressTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testNoArgs()
     {
-        runTool(0, "org.apache.cassandra.stress.CompactionStress");
+        
runner.invokeClassAsTool("org.apache.cassandra.stress.CompactionStress").waitAndAssertOnCleanExit();
     }
 
     @Test
@@ -41,20 +43,27 @@ public class CompactionStressTest extends ToolsTester
         File file = new 
File(classLoader.getResource("blogpost.yaml").getFile());
         String profileFile = file.getAbsolutePath();
 
-        runTool(0,
-                "org.apache.cassandra.stress.CompactionStress",
-                "write",
-                "-d", "build/test/cassandra",
-                "-g", "0",
-                "-p", profileFile,
-                "-t", "4");
-
-        runTool(0,
-                "org.apache.cassandra.stress.CompactionStress",
-                "compact",
-                "-d", "build/test/cassandra",
-                "-p", profileFile,
-                "-t", "4");
+        
runner.invokeClassAsTool("org.apache.cassandra.stress.CompactionStress",
+                                 "write",
+                                 "-d",
+                                 "build/test/cassandra",
+                                 "-g",
+                                 "0",
+                                 "-p",
+                                 profileFile,
+                                 "-t",
+                                 "4")
+              .waitAndAssertOnCleanExit();
+
+        
runner.invokeClassAsTool("org.apache.cassandra.stress.CompactionStress",
+                                 "compact",
+                                 "-d",
+                                 "build/test/cassandra",
+                                 "-p",
+                                 profileFile,
+                                 "-t",
+                                 "4")
+              .waitAndAssertOnCleanExit();
     }
 
 }
diff --git a/test/unit/org/apache/cassandra/tools/GetVersionTest.java 
b/test/unit/org/apache/cassandra/tools/GetVersionTest.java
index 84e2f49..c5f5282 100644
--- a/test/unit/org/apache/cassandra/tools/GetVersionTest.java
+++ b/test/unit/org/apache/cassandra/tools/GetVersionTest.java
@@ -24,12 +24,14 @@ import org.junit.runner.RunWith;
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class GetVersionTest extends ToolsTester
+public class GetVersionTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testGetVersion()
     {
-        runTool(0, "org.apache.cassandra.tools.GetVersion");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.GetVersion").waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java 
b/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java
index 67258ad..6377c59 100644
--- a/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java
+++ b/test/unit/org/apache/cassandra/tools/LoaderOptionsTest.java
@@ -23,7 +23,7 @@ import org.junit.Test;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
 
-import static org.apache.cassandra.tools.ToolsTester.sstableDirName;
+import static org.apache.cassandra.tools.OfflineToolUtils.sstableDirName;
 import static org.junit.Assert.*;
 
 // LoaderOptionsTester for custom configuration
diff --git a/test/unit/org/apache/cassandra/tools/ToolsTester.java 
b/test/unit/org/apache/cassandra/tools/OfflineToolUtils.java
similarity index 80%
rename from test/unit/org/apache/cassandra/tools/ToolsTester.java
rename to test/unit/org/apache/cassandra/tools/OfflineToolUtils.java
index 0bb9beb..9383f7d 100644
--- a/test/unit/org/apache/cassandra/tools/ToolsTester.java
+++ b/test/unit/org/apache/cassandra/tools/OfflineToolUtils.java
@@ -23,9 +23,7 @@ import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.lang.management.ThreadInfo;
 import java.lang.management.ThreadMXBean;
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.security.Permission;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -35,20 +33,20 @@ import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import org.apache.commons.io.FileUtils;
+
 import org.junit.BeforeClass;
 
 import org.slf4j.LoggerFactory;
 
 import static 
org.apache.cassandra.utils.FBUtilities.preventIllegalAccessWarnings;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
- * Base unit test class for standalone tools
+ * Helper class for running a tool and doing in-process checks
  */
-public abstract class ToolsTester
+public abstract class OfflineToolUtils
 {
     static
     {
@@ -193,69 +191,13 @@ public abstract class ToolsTester
             fail(clazz + " has not been loaded");
     }
 
-    public void runTool(int expectedExitCode, String clazz, String... args)
-    {
-        try
-        {
-            // install security manager to get informed about the exit-code
-            System.setSecurityManager(new SecurityManager()
-            {
-                public void checkExit(int status)
-                {
-                    throw new SystemExitException(status);
-                }
-
-                public void checkPermission(Permission perm)
-                {
-                }
-
-                public void checkPermission(Permission perm, Object context)
-                {
-                }
-            });
-
-            try
-            {
-                Class.forName(clazz).getDeclaredMethod("main", 
String[].class).invoke(null, (Object) args);
-            }
-            catch (InvocationTargetException e)
-            {
-                Throwable cause = e.getCause();
-                if (cause instanceof Error)
-                    throw (Error) cause;
-                if (cause instanceof RuntimeException)
-                    throw (RuntimeException) cause;
-                throw e;
-            }
-
-            assertEquals("Unexpected exit code", expectedExitCode, 0);
-        }
-        catch (SystemExitException e)
-        {
-            assertEquals("Unexpected exit code", expectedExitCode, e.status);
-        }
-        catch (InvocationTargetException e)
-        {
-            throw new RuntimeException(e.getTargetException());
-        }
-        catch (Exception e)
-        {
-            throw new RuntimeException(e);
-        }
-        finally
-        {
-            // uninstall security manager
-            System.setSecurityManager(null);
-        }
-    }
-
     @BeforeClass
     public static void setupTester()
     {
         System.setProperty("cassandra.partitioner", 
"org.apache.cassandra.dht.Murmur3Partitioner");
 
         // may start an async appender
-        LoggerFactory.getLogger(ToolsTester.class);
+        LoggerFactory.getLogger(OfflineToolUtils.class);
 
         ThreadMXBean threads = ManagementFactory.getThreadMXBean();
         initialThreads = 
Arrays.asList(threads.getThreadInfo(threads.getAllThreadIds()));
diff --git 
a/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java 
b/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java
index 1c09174..ad2dc3e 100644
--- a/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java
+++ b/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class SSTableExpiredBlockersTest extends ToolsTester
+public class SSTableExpiredBlockersTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testSSTableExpiredBlockers_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.SSTableExpiredBlockers");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExpiredBlockers").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -42,7 +46,7 @@ public class SSTableExpiredBlockersTest extends ToolsTester
     public void testSSTableExpiredBlockers_WithArgs()
     {
         // returns exit code 1, since no sstables are there
-        runTool(1, "org.apache.cassandra.tools.SSTableExpiredBlockers", 
"system_schema", "tables");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExpiredBlockers", 
"system_schema", "tables").getExitCode());
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/SSTableExportTest.java 
b/test/unit/org/apache/cassandra/tools/SSTableExportTest.java
index 0e49bb5..54af60c 100644
--- a/test/unit/org/apache/cassandra/tools/SSTableExportTest.java
+++ b/test/unit/org/apache/cassandra/tools/SSTableExportTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class SSTableExportTest extends ToolsTester
+public class SSTableExportTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testSSTableExport_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.SSTableExport");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport").getExitCode());
         assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class SSTableExportTest extends ToolsTester
     @Test
     public void testSSTableExport_WithArgs() throws Exception
     {
-        runTool(0, "org.apache.cassandra.tools.SSTableExport", 
findOneSSTable("legacy_sstables", "legacy_ma_simple"));
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport",findOneSSTable("legacy_sstables",
 "legacy_ma_simple"))
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java 
b/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java
index b70fcef..947a988 100644
--- a/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java
+++ b/test/unit/org/apache/cassandra/tools/SSTableLevelResetterTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class SSTableLevelResetterTest extends ToolsTester
+public class SSTableLevelResetterTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testSSTableLevelResetter_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.SSTableLevelResetter");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableLevelResetter").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class SSTableLevelResetterTest extends ToolsTester
     @Test
     public void testSSTableLevelResetter_WithArgs()
     {
-        runTool(0, "org.apache.cassandra.tools.SSTableLevelResetter", 
"--really-reset", "system_schema", "tables");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableLevelResetter", 
"--really-reset", "system_schema", "tables")
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git 
a/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java 
b/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
index 8b782e3..9370315 100644
--- a/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
+++ b/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class SSTableMetadataViewerTest extends ToolsTester
+public class SSTableMetadataViewerTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testSSTableOfflineRelevel_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.SSTableMetadataViewer");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableMetadataViewer").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,7 @@ public class SSTableMetadataViewerTest extends ToolsTester
     @Test
     public void testSSTableOfflineRelevel_WithArgs()
     {
-        runTool(0, "org.apache.cassandra.tools.SSTableMetadataViewer", "ks", 
"tab");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableMetadataViewer", 
"ks", "tab").waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
diff --git 
a/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java 
b/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java
index 0c6eecf..1d155bc 100644
--- a/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java
+++ b/test/unit/org/apache/cassandra/tools/SSTableOfflineRelevelTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class SSTableOfflineRelevelTest extends ToolsTester
+public class SSTableOfflineRelevelTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testSSTableOfflineRelevel_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.SSTableOfflineRelevel");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableOfflineRelevel").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -42,7 +46,7 @@ public class SSTableOfflineRelevelTest extends ToolsTester
     public void testSSTableOfflineRelevel_WithArgs()
     {
         // Note: SSTableOfflineRelevel exits with code 1 if no sstables to 
relevel have been found
-        runTool(1, "org.apache.cassandra.tools.SSTableOfflineRelevel", 
"system_schema", "tables");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableOfflineRelevel", 
"system_schema", "tables").getExitCode());
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git 
a/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java 
b/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
index 9dece5a..737b0eb 100644
--- a/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
+++ b/test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class SSTableRepairedAtSetterTest extends ToolsTester
+public class SSTableRepairedAtSetterTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testSSTableRepairedAtSetter_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.SSTableRepairedAtSetter");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableRepairedAtSetter").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class SSTableRepairedAtSetterTest extends ToolsTester
     @Test
     public void testSSTableRepairedAtSetter_WithArgs() throws Exception
     {
-        runTool(0, "org.apache.cassandra.tools.SSTableRepairedAtSetter", 
"--really-set", "--is-repaired", findOneSSTable("legacy_sstables", 
"legacy_ma_simple"));
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableRepairedAtSetter", 
"--really-set", "--is-repaired", findOneSSTable("legacy_sstables", 
"legacy_ma_simple"))
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
diff --git 
a/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java 
b/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java
index 2026a79..834f537 100644
--- a/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java
+++ b/test/unit/org/apache/cassandra/tools/StandaloneSSTableUtilTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class StandaloneSSTableUtilTest extends ToolsTester
+public class StandaloneSSTableUtilTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testStandaloneSSTableUtil_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.StandaloneSSTableUtil");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneSSTableUtil").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class StandaloneSSTableUtilTest extends ToolsTester
     @Test
     public void testStandaloneSSTableUtil_WithArgs()
     {
-        runTool(0, "org.apache.cassandra.tools.StandaloneSSTableUtil", 
"--debug", "-c", "system_schema", "tables");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneSSTableUtil", 
"--debug", "-c", "system_schema", "tables")
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java 
b/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java
index 6aef5eb..a99f657 100644
--- a/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java
+++ b/test/unit/org/apache/cassandra/tools/StandaloneScrubberTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class StandaloneScrubberTest extends ToolsTester
+public class StandaloneScrubberTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testStandaloneScrubber_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.StandaloneScrubber");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneScrubber").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class StandaloneScrubberTest extends ToolsTester
     @Test
     public void testStandaloneScrubber_WithArgs()
     {
-        runTool(0, "org.apache.cassandra.tools.StandaloneScrubber", "--debug", 
"system_schema", "tables");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneScrubber", 
"--debug", "system_schema", "tables")
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java 
b/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
index c0f8593..753a258 100644
--- a/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
+++ b/test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
@@ -24,9 +24,13 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class StandaloneSplitterTest extends ToolsTester
+public class StandaloneSplitterTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @BeforeClass
     public static void before()
     {
@@ -39,7 +43,7 @@ public class StandaloneSplitterTest extends ToolsTester
     @Test
     public void testStandaloneSplitter_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.StandaloneSplitter");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneSplitter").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java 
b/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
index f703a49..1bfbbd2 100644
--- a/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
+++ b/test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class StandaloneUpgraderTest extends ToolsTester
+public class StandaloneUpgraderTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testStandaloneUpgrader_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.StandaloneUpgrader");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class StandaloneUpgraderTest extends ToolsTester
     @Test
     public void testStandaloneUpgrader_WithArgs()
     {
-        runTool(0, "org.apache.cassandra.tools.StandaloneUpgrader", "--debug", 
"system_schema", "tables");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", 
"--debug", "system_schema", "tables")
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java 
b/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
index 38418f3..e33a154 100644
--- a/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
+++ b/test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
@@ -23,13 +23,17 @@ import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 
+import static org.junit.Assert.assertEquals;
+
 @RunWith(OrderedJUnit4ClassRunner.class)
-public class StandaloneVerifierTest extends ToolsTester
+public class StandaloneVerifierTest extends OfflineToolUtils
 {
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
     @Test
     public void testStandaloneVerifier_NoArgs()
     {
-        runTool(1, "org.apache.cassandra.tools.StandaloneVerifier");
+        assertEquals(1, 
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier").getExitCode());
         assertNoUnexpectedThreadsStarted(null, null);
         assertSchemaNotLoaded();
         assertCLSMNotLoaded();
@@ -41,7 +45,8 @@ public class StandaloneVerifierTest extends ToolsTester
     @Test
     public void testStandaloneVerifier_WithArgs()
     {
-        runTool(0, "org.apache.cassandra.tools.StandaloneVerifier", "--debug", 
"system_schema", "tables");
+        
runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", 
"--debug", "system_schema", "tables")
+              .waitAndAssertOnCleanExit();
         assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, 
OPTIONAL_THREADS_WITH_SCHEMA);
         assertSchemaLoaded();
         assertServerNotLoaded();
diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java 
b/test/unit/org/apache/cassandra/tools/ToolRunner.java
new file mode 100644
index 0000000..61e32b0
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java
@@ -0,0 +1,437 @@
+/*
+ * 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.cassandra.tools;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.security.Permission;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.commons.io.IOUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.OfflineToolUtils.SystemExitException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ToolRunner implements AutoCloseable
+{
+    protected static final Logger logger = 
LoggerFactory.getLogger(ToolRunner.class);
+    
+    private final List<String> allArgs = new ArrayList<>();
+    private Process process;
+    private final ByteArrayOutputStream errBuffer = new 
ByteArrayOutputStream();
+    private final ByteArrayOutputStream outBuffer = new 
ByteArrayOutputStream();
+    private InputStream stdin;
+    private boolean stdinAutoClose;
+    private long defaultTimeoutMillis = TimeUnit.SECONDS.toMillis(30);
+    private Thread ioWatcher;
+    private Map<String, String> envs;
+    private boolean runOutOfProcess = true;
+
+    public ToolRunner(List<String> args)
+    {
+        this.allArgs.addAll(args);
+    }
+    
+    public ToolRunner(List<String> args, boolean runOutOfProcess)
+    {
+        this.allArgs.addAll(args);
+        this.runOutOfProcess = runOutOfProcess;
+    }
+
+    public ToolRunner withStdin(InputStream stdin, boolean autoClose)
+    {
+        this.stdin = stdin;
+        this.stdinAutoClose = autoClose;
+        return this;
+    }
+
+    public ToolRunner withEnvs(Map<String, String> envs)
+    {
+        Preconditions.checkArgument(runOutOfProcess, "Not supported");
+        this.envs = envs;
+        return this;
+    }
+
+    public ToolRunner start()
+    {
+        if (process != null)
+            throw new IllegalStateException("Process already started. Create a 
new ToolRunner instance for each invocation.");
+
+        logger.debug("Starting {} with args {}", runOutOfProcess ? "process" : 
"class" , argsToLogString());
+
+        try
+        {
+            if (runOutOfProcess)
+            {
+                ProcessBuilder pb = new ProcessBuilder(allArgs);
+                if (envs != null)
+                    pb.environment().putAll(envs);
+                process = pb.start();
+            }
+            else
+            {
+                PrintStream originalSysOut = System.out;
+                PrintStream originalSysErr = System.err;
+                InputStream originalSysIn = System.in;
+                originalSysOut.flush();
+                originalSysErr.flush();
+                ByteArrayOutputStream toolOut = new ByteArrayOutputStream();
+                ByteArrayOutputStream toolErr = new ByteArrayOutputStream();
+                
+                System.setIn(stdin == null ? originalSysIn : stdin);
+                int exit = 0;
+                try (PrintStream newOut = new PrintStream(toolOut); 
PrintStream newErr = new PrintStream(toolErr);)
+                {
+                    System.setOut(newOut);
+                    System.setErr(newErr);
+                    String clazz = allArgs.get(0);
+                    String[] clazzArgs = allArgs.subList(1, 
allArgs.size()).toArray(new String[0]);
+                    exit = runClassAsTool(clazz, clazzArgs);
+                }
+                
+                final int exitCode = exit;
+                System.setOut(originalSysOut);
+                System.setErr(originalSysErr);
+                System.setIn(originalSysIn);
+                
+                process = new Process() {
+
+                    @Override
+                    public void destroy()
+                    {
+                    }
+
+                    @Override
+                    public int exitValue()
+                    {
+                        return exitCode;
+                    }
+
+                    @Override
+                    public InputStream getErrorStream()
+                    {
+                        return new ByteArrayInputStream(toolErr.toByteArray());
+                    }
+
+                    @Override
+                    public InputStream getInputStream()
+                    {
+                        return new ByteArrayInputStream(toolOut.toByteArray());
+                    }
+
+                    @Override
+                    public OutputStream getOutputStream()
+                    {
+                        if (stdin == null)
+                            return null;
+                        
+                        ByteArrayOutputStream out = null;
+                        try
+                        {
+                            out = new ByteArrayOutputStream(stdin.available());
+                            IOUtils.copy(stdin, out);
+                        }
+                        catch(IOException e)
+                        {
+                            throw new RuntimeException("Failed to get stdin", 
e);
+                        }
+                        return out;
+                    }
+
+                    @Override
+                    public int waitFor() throws InterruptedException
+                    {
+                        return exitValue();
+                    }
+                    
+                };
+            }
+            
+            ioWatcher = new Thread(this::watchIO);
+            ioWatcher.setDaemon(true);
+            ioWatcher.start();
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException("Failed to start " + allArgs, e);
+        }
+
+        return this;
+    }
+
+    private void watchIO()
+    {
+        OutputStream in = process.getOutputStream();
+        InputStream err = process.getErrorStream();
+        InputStream out = process.getInputStream();
+        while (true)
+        {
+            boolean errHandled;
+            boolean outHandled;
+            try
+            {
+                if (stdin != null)
+                {
+                    IOUtils.copy(stdin, in);
+                    if (stdinAutoClose)
+                    {
+                        in.close();
+                        stdin = null;
+                    }
+                }
+                errHandled = IOUtils.copy(err, errBuffer) > 0;
+                outHandled = IOUtils.copy(out, outBuffer) > 0;
+            }
+            catch(IOException e1)
+            {
+                logger.error("Error trying to use in/err/out from process");
+                Thread.currentThread().interrupt();
+                break;
+            }
+            if (!errHandled && !outHandled)
+            {
+                if (!process.isAlive())
+                    return;
+                try
+                {
+                    Thread.sleep(50L);
+                }
+                catch (InterruptedException e)
+                {
+                    Thread.currentThread().interrupt();
+                    break;
+                }
+            }
+        }
+    }
+    
+    public int runClassAsTool(String clazz, String... args)
+    {
+        try
+        {
+            // install security manager to get informed about the exit-code
+            System.setSecurityManager(new SecurityManager()
+            {
+                public void checkExit(int status)
+                {
+                    throw new SystemExitException(status);
+                }
+
+                public void checkPermission(Permission perm)
+                {
+                }
+
+                public void checkPermission(Permission perm, Object context)
+                {
+                }
+            });
+
+            try
+            {
+                Class.forName(clazz).getDeclaredMethod("main", 
String[].class).invoke(null, (Object) args);
+            }
+            catch (InvocationTargetException e)
+            {
+                Throwable cause = e.getCause();
+                if (cause instanceof Error)
+                    throw (Error) cause;
+                if (cause instanceof RuntimeException)
+                    throw (RuntimeException) cause;
+                throw e;
+            }
+
+            return 0;
+        }
+        catch (SystemExitException e)
+        {
+            return e.status;
+        }
+        catch (InvocationTargetException e)
+        {
+            throw new RuntimeException(e.getTargetException());
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
+        finally
+        {
+            // uninstall security manager
+            System.setSecurityManager(null);
+        }
+    }
+
+    public boolean isRunning()
+    {
+        return process != null && process.isAlive();
+    }
+
+    public boolean waitFor()
+    {
+        return waitFor(defaultTimeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    public boolean waitFor(long time, TimeUnit timeUnit)
+    {
+        try
+        {
+            if (!process.waitFor(time, timeUnit))
+                return false;
+            ioWatcher.join();
+            return true;
+        }
+        catch (InterruptedException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public ToolRunner waitAndAssertOnExitCode()
+    {
+        assertTrue(String.format("Tool %s didn't terminate",
+                           argsToLogString()),
+                   waitFor());
+        return assertOnExitCode();
+    }
+    
+    public ToolRunner waitAndAssertOnCleanExit()
+    {
+        return waitAndAssertOnExitCode().assertEmptyStdErr();
+    }
+    
+    public ToolRunner assertEmptyStdErr()
+    {
+        assertTrue(getStderr().isEmpty());
+        return this;
+    }
+
+    public ToolRunner assertOnExitCode()
+    {
+        int code = getExitCode();
+        if (code != 0)
+            fail(String.format("%s%nexited with code 
%d%nstderr:%n%s%nstdout:%n%s",
+                               argsToLogString(),
+                               code,
+                               getStderr(),
+                               getStdout()));
+        return this;
+    }
+
+    public String argsToLogString()
+    {
+        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", 
"]"));
+    }
+
+    public int getExitCode()
+    {
+        return process.exitValue();
+    }
+
+    public String getStdout()
+    {
+        return outBuffer.toString();
+    }
+
+    public String getStderr()
+    {
+        return errBuffer.toString();
+    }
+
+    public void forceKill()
+    {
+        try
+        {
+            process.exitValue();
+            // process no longer alive - just ignore that fact
+        }
+        catch (IllegalThreadStateException e)
+        {
+            process.destroyForcibly();
+        }
+    }
+
+    @Override
+    public void close()
+    {
+        forceKill();
+    }
+    
+    static class Runners
+    {
+        protected ToolRunner invokeNodetool(String... args)
+        {
+            return invokeNodetool(Arrays.asList(args));
+        }
+
+        protected ToolRunner invokeNodetool(List<String> args)
+        {
+            return invokeTool(buildNodetoolArgs(args), true);
+        }
+
+        private static List<String> buildNodetoolArgs(List<String> args)
+        {
+            return CQLTester.buildNodetoolArgs(args);
+        }
+        
+        protected ToolRunner invokeClassAsTool(String... args)
+        {
+            return invokeClassAsTool(Arrays.asList(args));
+        }
+        
+        protected ToolRunner invokeClassAsTool(List<String> args)
+        {
+            return invokeTool(args, false);
+        }
+
+        protected ToolRunner invokeTool(String... args)
+        {
+            return invokeTool(Arrays.asList(args));
+        }
+
+        protected ToolRunner invokeTool(List<String> args)
+        {
+            return invokeTool(args, true);
+        }
+
+        protected ToolRunner invokeTool(List<String> args, boolean 
runOutOfProcess)
+        {
+            ToolRunner runner = new ToolRunner(args, runOutOfProcess);
+            runner.start().waitFor();
+            return runner;
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to